From 9ba3b7ce282b91aa4fe24ce99113d043d0ec902d Mon Sep 17 00:00:00 2001
From: Wattachai Kanawitoon <117723407+wattachai-lseg@users.noreply.github.com>
Date: Mon, 9 Oct 2023 19:35:13 +0700
Subject: [PATCH] fix(password-field, phrasebook): improve show password
announcement
---
.../__snapshots__/PasswordField.md | 41 ++---------
.../__test__/password-field.test.js | 73 ++++++++++++++++++-
packages/elements/src/password-field/index.ts | 52 ++++++++++++-
.../src/locale/de/password-field.ts | 3 +-
.../src/locale/en/password-field.ts | 3 +-
.../src/locale/ja/password-field.ts | 5 +-
.../src/locale/zh-hant/password-field.ts | 3 +-
.../src/locale/zh/password-field.ts | 3 +-
packages/utils/src/accessibility.ts | 1 +
packages/utils/src/accessibility/helpers.ts | 20 ++++-
10 files changed, 155 insertions(+), 49 deletions(-)
diff --git a/packages/elements/src/password-field/__snapshots__/PasswordField.md b/packages/elements/src/password-field/__snapshots__/PasswordField.md
index d70a44306a..91e65b0fd1 100644
--- a/packages/elements/src/password-field/__snapshots__/PasswordField.md
+++ b/packages/elements/src/password-field/__snapshots__/PasswordField.md
@@ -10,48 +10,19 @@
>
-
-```
-
-#### `Can toggle password field`
-
-```html
-
-
-
-
-```
-
-```html
-
-
-
+
```
diff --git a/packages/elements/src/password-field/__test__/password-field.test.js b/packages/elements/src/password-field/__test__/password-field.test.js
index a61038adca..48d7c3b7b1 100644
--- a/packages/elements/src/password-field/__test__/password-field.test.js
+++ b/packages/elements/src/password-field/__test__/password-field.test.js
@@ -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('');
@@ -12,12 +16,75 @@ describe('password-field/PasswordField', function () {
it('Can toggle password field', async function () {
const el = await fixture('');
- 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`
+ );
});
});
diff --git a/packages/elements/src/password-field/index.ts b/packages/elements/src/password-field/index.ts
index 97d5021eaa..8fc16fc7d5 100644
--- a/packages/elements/src/password-field/index.ts
+++ b/packages/elements/src/password-field/index.ts
@@ -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';
@@ -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
@@ -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}"
>
+
${this.liveRegionContent}
`;
}
+ /**
+ * 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
diff --git a/packages/phrasebook/src/locale/de/password-field.ts b/packages/phrasebook/src/locale/de/password-field.ts
index 5ca816d990..095a89bac9 100644
--- a/packages/phrasebook/src/locale/de/password-field.ts
+++ b/packages/phrasebook/src/locale/de/password-field.ts
@@ -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);
diff --git a/packages/phrasebook/src/locale/en/password-field.ts b/packages/phrasebook/src/locale/en/password-field.ts
index 654aef90ec..4589598aaa 100644
--- a/packages/phrasebook/src/locale/en/password-field.ts
+++ b/packages/phrasebook/src/locale/en/password-field.ts
@@ -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);
diff --git a/packages/phrasebook/src/locale/ja/password-field.ts b/packages/phrasebook/src/locale/ja/password-field.ts
index 299ea1fbc4..ac81ae507a 100644
--- a/packages/phrasebook/src/locale/ja/password-field.ts
+++ b/packages/phrasebook/src/locale/ja/password-field.ts
@@ -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);
diff --git a/packages/phrasebook/src/locale/zh-hant/password-field.ts b/packages/phrasebook/src/locale/zh-hant/password-field.ts
index 483b0bdc4c..0a1e8450c6 100644
--- a/packages/phrasebook/src/locale/zh-hant/password-field.ts
+++ b/packages/phrasebook/src/locale/zh-hant/password-field.ts
@@ -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);
diff --git a/packages/phrasebook/src/locale/zh/password-field.ts b/packages/phrasebook/src/locale/zh/password-field.ts
index ed9b5c12c5..965b6f191b 100644
--- a/packages/phrasebook/src/locale/zh/password-field.ts
+++ b/packages/phrasebook/src/locale/zh/password-field.ts
@@ -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);
diff --git a/packages/utils/src/accessibility.ts b/packages/utils/src/accessibility.ts
index cbb28c5806..43597a575f 100644
--- a/packages/utils/src/accessibility.ts
+++ b/packages/utils/src/accessibility.ts
@@ -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';
diff --git a/packages/utils/src/accessibility/helpers.ts b/packages/utils/src/accessibility/helpers.ts
index 2c86a134ed..372b3d5ecb 100644
--- a/packages/utils/src/accessibility/helpers.ts
+++ b/packages/utils/src/accessibility/helpers.ts
@@ -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
@@ -28,4 +46,4 @@ const textFromElementIds = (rootNode: Document | DocumentFragment, ids: string):
return labels.join(SEPARATOR);
};
-export { textFromElementIds };
+export { textFromElementIds, VISUALLY_HIDDEN_STYLE };