Skip to content

Commit

Permalink
fix(password-field, phrasebook): improve show password announcement (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
wattachai-lseg authored Oct 12, 2023
1 parent 7e36942 commit 0215600
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,48 +10,19 @@
>
<ef-icon
aria-label="Show password"
aria-pressed="false"
icon="eye"
part="icon"
role="button"
tabindex="0"
>
</ef-icon>

```

#### `Can toggle password field`

```html
<input
autocomplete="off"
part="input"
type="text"
<div
aria-live="polite"
class="visually-hidden"
part="live-region"
>
<ef-icon
aria-label="Hide password"
icon="eye-off"
part="icon"
role="button"
tabindex="0"
>
</ef-icon>

```

```html
<input
autocomplete="off"
part="input"
type="password"
>
<ef-icon
aria-label="Show password"
icon="eye"
part="icon"
role="button"
tabindex="0"
>
</ef-icon>
</div>

```

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import '@refinitiv-ui/elements/password-field';
import '@refinitiv-ui/elemental-theme/light/ef-password-field';
import { elementUpdated, expect, fixture } from '@refinitiv-ui/test-helpers';

const getTextContent = (el) => {
return el.textContent?.trim() || '';
};

describe('password-field/PasswordField', function () {
it('Default DOM structure and properties are correct', async function () {
const el = await fixture('<ef-password-field></ef-password-field>');
Expand All @@ -12,12 +16,75 @@ describe('password-field/PasswordField', function () {

it('Can toggle password field', async function () {
const el = await fixture('<ef-password-field></ef-password-field>');
const eyeIconEl = el.shadowRoot.querySelector('[part~=icon]');
const eyeIconEl = el.shadowRoot.querySelector('[part=icon]');
const inputEl = el.shadowRoot.querySelector('[part=input]');
const liveRegionEl = el.shadowRoot.querySelector('[part=live-region]');

const visibleMessage = 'Show password on, password is visible';
const hiddenMessage = 'Show password off, password is hidden';

expect(inputEl.getAttribute('type')).to.equal(
'password',
'Input type should set to "password" by default'
);
expect(eyeIconEl.getAttribute('aria-pressed')).to.equal(
'false',
'aria-pressed of icon should set to "false" by default'
);
expect(getTextContent(liveRegionEl)).to.equal(
'',
'text content of live region should be empty by default'
);

eyeIconEl.focus();
await elementUpdated(el);
expect(getTextContent(liveRegionEl)).to.equal(
hiddenMessage,
`text content of live region should be "${hiddenMessage}" after focusing on show password`
);

eyeIconEl.click();
await elementUpdated(el);
expect(el).shadowDom.to.equalSnapshot();
expect(inputEl.getAttribute('type')).to.equal(
'text',
'Input type should set to "text" after click show password'
);
expect(eyeIconEl.getAttribute('aria-pressed')).to.equal(
'true',
'aria-pressed of icon should set to "true" after toggling show password'
);
expect(getTextContent(liveRegionEl)).to.equal(
visibleMessage,
`text content of live region should be "${visibleMessage}" after toggling show password`
);

eyeIconEl.blur();
await elementUpdated(el);
expect(getTextContent(liveRegionEl)).to.equal(
'',
'text content of live region should be empty after blurring out of show password'
);

eyeIconEl.focus();
await elementUpdated(el);
expect(getTextContent(liveRegionEl)).to.equal(
visibleMessage,
`text content of live region should be "${visibleMessage}" after toggling show password`
);

eyeIconEl.click();
await elementUpdated(el);
expect(el).shadowDom.to.equalSnapshot();
expect(inputEl.getAttribute('type')).to.equal(
'password',
'Input type should back to "Password" after click hide password'
);
expect(eyeIconEl.getAttribute('aria-pressed')).to.equal(
'false',
'aria-pressed of icon should be "false" after toggling show password for the second time'
);
expect(getTextContent(liveRegionEl)).to.equal(
hiddenMessage,
`aria-label of icon should back to "${hiddenMessage}" after toggling show password for the second time`
);
});
});
52 changes: 48 additions & 4 deletions packages/elements/src/password-field/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { PropertyValues, TemplateResult, html } from '@refinitiv-ui/core';
import { CSSResultGroup, PropertyValues, TemplateResult, html, unsafeCSS } from '@refinitiv-ui/core';
import { customElement } from '@refinitiv-ui/core/decorators/custom-element.js';
import { state } from '@refinitiv-ui/core/decorators/state.js';
import { TemplateMap } from '@refinitiv-ui/core/directives/template-map.js';

import '@refinitiv-ui/phrasebook/locale/en/password-field.js';
import { Translate, translate } from '@refinitiv-ui/translate';
import { Translate, TranslateDirectiveResult, translate } from '@refinitiv-ui/translate';
import { VISUALLY_HIDDEN_STYLE } from '@refinitiv-ui/utils/accessibility.js';

import { preload } from '../icon/index.js';
import '../icon/index.js';
Expand Down Expand Up @@ -62,6 +63,21 @@ export class PasswordField extends TextField {
@state()
private isPasswordVisible = false;

/**
* live region content presenting password field visibility state
*/
@state()
private liveRegionContent: TranslateDirectiveResult = '';

/**
* A `CSSResultGroup` that will be used to style the host,
* slotted children and the internal template of the element.
* @returns CSS template
*/
static override get styles(): CSSResultGroup {
return [super.styles, unsafeCSS(VISUALLY_HIDDEN_STYLE)];
}

/**
* Called when the element’s DOM has been updated and rendered for the first time
* @param changedProperties Properties that has changed
Expand Down Expand Up @@ -100,15 +116,43 @@ export class PasswordField extends TextField {
part="icon"
role="button"
tabindex="0"
aria-label="${this.isPasswordVisible ? this.t('HIDE_PASSWORD') : this.t('SHOW_PASSWORD')}"
aria-pressed="${this.isPasswordVisible}"
aria-label="${this.t('SHOW_PASSWORD')}"
icon=${this.isPasswordVisible ? 'eye-off' : 'eye'}
?readonly="${this.readonly}"
?disabled="${this.disabled}"
@tap="${this.togglePasswordVisibility}"
@tap="${this.onTogglePasswordTap}"
@focus="${this.updateLiveRegionContent}"
@blur="${this.updateLiveRegionContent}"
></ef-icon>
<div part="live-region" aria-live="polite" class="visually-hidden">${this.liveRegionContent}</div>
`;
}

/**
* update live region content describing password visibility state
* @param event event trigging live region content update
* @return void
*/
protected updateLiveRegionContent(event: Event): void {
this.liveRegionContent =
event.type === 'blur'
? ''
: this.isPasswordVisible
? this.t('SHOW_PASSWORD_ON')
: this.t('SHOW_PASSWORD_OFF');
}

/**
* Handle tap events of toggle password icon
* @param event custom event
* @returns {void}
*/
protected onTogglePasswordTap(event: CustomEvent): void {
this.togglePasswordVisibility();
this.updateLiveRegionContent(event);
}

/**
* Toggles password visibility state
* @return void
Expand Down
3 changes: 2 additions & 1 deletion packages/phrasebook/src/locale/de/password-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Phrasebook } from '../../translation.js';

const translations = {
SHOW_PASSWORD: 'Kennwort anzeigen',
HIDE_PASSWORD: 'Kennwort ausblenden'
SHOW_PASSWORD_ON: 'Passwort anzeigen ein, Passwort wird angezeigt',
SHOW_PASSWORD_OFF: 'Passwort anzeigen aus, Passwort wird ausgeblendet'
};

Phrasebook.define('de', 'ef-password-field', translations);
Expand Down
3 changes: 2 additions & 1 deletion packages/phrasebook/src/locale/en/password-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Phrasebook } from '../../translation.js';

const translations = {
SHOW_PASSWORD: 'Show password',
HIDE_PASSWORD: 'Hide password'
SHOW_PASSWORD_ON: 'Show password on, password is visible',
SHOW_PASSWORD_OFF: 'Show password off, password is hidden'
};

Phrasebook.define('en', 'ef-password-field', translations);
Expand Down
5 changes: 3 additions & 2 deletions packages/phrasebook/src/locale/ja/password-field.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Phrasebook } from '../../translation.js';

const translations = {
SHOW_PASSWORD: 'パスワードを表示',
HIDE_PASSWORD: 'パスワードを非表示'
SHOW_PASSWORD: 'パスワード表示',
SHOW_PASSWORD_ON: 'パスワード表示オン、パスワードが表示されています',
SHOW_PASSWORD_OFF: 'パスワード表示オフ、パスワードは表示されていません'
};

Phrasebook.define('ja', 'ef-password-field', translations);
Expand Down
3 changes: 2 additions & 1 deletion packages/phrasebook/src/locale/zh-hant/password-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Phrasebook } from '../../translation.js';

const translations = {
SHOW_PASSWORD: '顯示密碼',
HIDE_PASSWORD: '隱藏密碼'
SHOW_PASSWORD_ON: '密碼顯示開啟,密碼可見',
SHOW_PASSWORD_OFF: '密碼顯示關閉,密碼被隱藏'
};

Phrasebook.define('zh-Hant', 'ef-password-field', translations);
Expand Down
3 changes: 2 additions & 1 deletion packages/phrasebook/src/locale/zh/password-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Phrasebook } from '../../translation.js';

const translations = {
SHOW_PASSWORD: '显示密码',
HIDE_PASSWORD: '隐藏密码'
SHOW_PASSWORD_ON: '密码显示开启,密码可见',
SHOW_PASSWORD_OFF: '密码显示关闭,密码被隐藏'
};

Phrasebook.define('zh', 'ef-password-field', translations);
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/accessibility.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { label } from './accessibility/label.js';
export { description } from './accessibility/description.js';
export { required } from './accessibility/required.js';
export { VISUALLY_HIDDEN_STYLE } from './accessibility/helpers.js';
20 changes: 19 additions & 1 deletion packages/utils/src/accessibility/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,24 @@
*/
const SEPARATOR = ' ';

/**
* a style hiding elements visually with `.visually-hidden` selector.
* These elements would be available to screen readers only.
*/
const VISUALLY_HIDDEN_STYLE = `
.visually-hidden {
position: absolute;
overflow: hidden;
width: 1px;
height: 1px;
clip: rect(0, 0, 0, 0);
clip-path: inset(50%);
margin: -1px;
border: 0;
padding: 0;
}
`;

/**
* Get innerText from element ids
* @param rootNode Root node
Expand All @@ -28,4 +46,4 @@ const textFromElementIds = (rootNode: Document | DocumentFragment, ids: string):
return labels.join(SEPARATOR);
};

export { textFromElementIds };
export { textFromElementIds, VISUALLY_HIDDEN_STYLE };

0 comments on commit 0215600

Please sign in to comment.