Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(password-field, phrasebook): improve show password announcement #990

Merged
merged 1 commit into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 };
Loading