diff --git a/.changeset/violet-wombats-tell.md b/.changeset/violet-wombats-tell.md new file mode 100644 index 0000000000..25812408d3 --- /dev/null +++ b/.changeset/violet-wombats-tell.md @@ -0,0 +1,17 @@ +--- +"@patternfly/elements": major +--- + +``: Reimplemented label API improving accessibility. + +```html + + + + + + + diff --git a/elements/pf-switch/BaseSwitch.css b/elements/pf-switch/BaseSwitch.css index 26fa4864bd..e56b7c7dd8 100644 --- a/elements/pf-switch/BaseSwitch.css +++ b/elements/pf-switch/BaseSwitch.css @@ -19,7 +19,7 @@ svg { cursor: not-allowed; } -:host(:disabled:focus-within) #container { +:host(:disabled:is(:focus,:focus-within)) { outline: none; } diff --git a/elements/pf-switch/BaseSwitch.ts b/elements/pf-switch/BaseSwitch.ts index 826c93b75b..932fe5a550 100644 --- a/elements/pf-switch/BaseSwitch.ts +++ b/elements/pf-switch/BaseSwitch.ts @@ -1,22 +1,20 @@ import { LitElement, html } from 'lit'; import { property } from 'lit/decorators/property.js'; -import styles from './BaseSwitch.css'; +import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js'; + +import styles from './BaseSwitch.css'; /** * Switch */ export abstract class BaseSwitch extends LitElement { static readonly styles = [styles]; - static readonly shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true, }; - static readonly formAssociated = true; declare shadowRoot: ShadowRoot; - #internals = this.attachInternals(); - - #initiallyDisabled = this.hasAttribute('disabled'); + #internals = InternalsController.of(this, { role: 'switch' }); @property({ reflect: true }) label?: string; @@ -24,7 +22,7 @@ export abstract class BaseSwitch extends LitElement { @property({ reflect: true, type: Boolean }) checked = false; - disabled = this.#initiallyDisabled; + @property({ reflect: true, type: Boolean }) disabled = false; get labels(): NodeListOf { return this.#internals.labels as NodeListOf; @@ -32,30 +30,36 @@ export abstract class BaseSwitch extends LitElement { override connectedCallback(): void { super.connectedCallback(); - this.setAttribute('role', 'checkbox'); + this.tabIndex = 0; this.addEventListener('click', this.#onClick); this.addEventListener('keyup', this.#onKeyup); + this.addEventListener('keydown', this.#onKeyDown); this.#updateLabels(); } formDisabledCallback(disabled: boolean) { this.disabled = disabled; - this.requestUpdate(); } override render() { return html` -
- - +
+ +
`; } - override updated() { - this.#internals.ariaChecked = String(this.checked); - this.#internals.ariaDisabled = String(this.disabled); + override willUpdate() { + this.#internals.ariaChecked = String(!!this.checked); + this.#internals.ariaDisabled = String(!!this.disabled); } #onClick(event: Event) { @@ -75,11 +79,17 @@ export abstract class BaseSwitch extends LitElement { } #onKeyup(event: KeyboardEvent) { - switch (event.key) { - case ' ': - case 'Enter': - event.preventDefault(); - this.#toggle(); + if (event.key === ' ' || event.key === 'Enter') { + event.preventDefault(); + event.stopPropagation(); + this.#toggle(); + } + } + + #onKeyDown(event: KeyboardEvent) { + if (event.key === ' ') { + event.preventDefault(); + event.stopPropagation(); } } @@ -95,10 +105,13 @@ export abstract class BaseSwitch extends LitElement { #updateLabels() { const labelState = this.checked ? 'on' : 'off'; - if (this.labels.length > 1) { - for (const label of this.labels) { - label.hidden = label.dataset.state !== labelState; - } - } + this.labels.forEach(label => { + const states = label.querySelectorAll('[data-state]'); + states.forEach(state => { + if (state) { + state.hidden = state.dataset.state !== labelState; + } + }); + }); } } diff --git a/elements/pf-switch/demo/checked.html b/elements/pf-switch/demo/checked.html index cb009030d3..c4e718db5f 100644 --- a/elements/pf-switch/demo/checked.html +++ b/elements/pf-switch/demo/checked.html @@ -3,8 +3,10 @@
Checked with label - - +
diff --git a/elements/pf-switch/demo/disabled.html b/elements/pf-switch/demo/disabled.html index 7cd8459733..28f3862fb0 100644 --- a/elements/pf-switch/demo/disabled.html +++ b/elements/pf-switch/demo/disabled.html @@ -3,13 +3,17 @@
Checked and Disabled - - +
- - +
diff --git a/elements/pf-switch/demo/pf-switch.html b/elements/pf-switch/demo/pf-switch.html index 23d8f022d3..5407c73a3a 100644 --- a/elements/pf-switch/demo/pf-switch.html +++ b/elements/pf-switch/demo/pf-switch.html @@ -4,8 +4,10 @@
Option A - - +
@@ -16,20 +18,28 @@
Form Disabled State - - + - - + - - + - - +
diff --git a/elements/pf-switch/demo/reversed.html b/elements/pf-switch/demo/reversed.html index 3cd351e6c6..d98da7665e 100644 --- a/elements/pf-switch/demo/reversed.html +++ b/elements/pf-switch/demo/reversed.html @@ -2,8 +2,10 @@
Reversed - - +
diff --git a/elements/pf-switch/docs/pf-switch.md b/elements/pf-switch/docs/pf-switch.md index 4dedb27bef..236833b15d 100644 --- a/elements/pf-switch/docs/pf-switch.md +++ b/elements/pf-switch/docs/pf-switch.md @@ -5,16 +5,20 @@ provide a more explicit, visible representation on a setting. - - + {% endrenderOverview %} {% band header="Usage" %} ### Basic {% htmlexample %} - - + {% endhtmlexample %} ### Without label @@ -25,8 +29,10 @@ ### Checked with label {% htmlexample %} - - + {% endhtmlexample %} ### Disabled @@ -35,14 +41,17 @@
Checked and Disabled - - +
-
- - +
{% endhtmlexample %} diff --git a/elements/pf-switch/pf-switch.css b/elements/pf-switch/pf-switch.css index d7703185ae..99f73cfc63 100644 --- a/elements/pf-switch/pf-switch.css +++ b/elements/pf-switch/pf-switch.css @@ -82,7 +82,11 @@ var(--pf-c-switch__toggle--before--Transition, translate .25s ease 0s)); ; } -:host(:focus-within) #container { +:host { + outline: none; +} + +:host(:is(:focus,:focus-within)) #container { outline: var(--pf-c-switch__input--focus__toggle--OutlineWidth, var(--pf-global--BorderWidth--md, 2px)) solid var(--pf-c-switch__input--focus__toggle--OutlineColor, var(--pf-global--primary-color--100, #06c)); diff --git a/elements/pf-switch/test/pf-switch.spec.ts b/elements/pf-switch/test/pf-switch.spec.ts index 3670571b66..992953f6f8 100644 --- a/elements/pf-switch/test/pf-switch.spec.ts +++ b/elements/pf-switch/test/pf-switch.spec.ts @@ -30,10 +30,10 @@ describe('', function() { .to.be.an.instanceOf(PfSwitch); }); it('has accessible role', function() { - expect(snapshot.role).to.equal('checkbox'); + expect(snapshot.role).to.equal('switch'); }); it('has accessible checked field', function() { - expect(snapshot.role).to.equal('checkbox'); + expect(snapshot.role).to.equal('switch'); }); it('requires accessible name', function() { // Double negative - this would fail an accessibility audit, @@ -50,8 +50,10 @@ describe('', function() { const container = await createFixture(html`
- - +
`); element = container.querySelector('pf-switch')!; @@ -59,7 +61,7 @@ describe('', function() { }); it('is accessible', function() { - expect(snapshot.role).to.equal('checkbox'); + expect(snapshot.role).to.equal('switch'); expect(snapshot.name).to.be.ok; expect(snapshot.checked).to.be.false; }); @@ -68,7 +70,7 @@ describe('', function() { expect(snapshot.name).to.equal('Message when off'); }); - describe('clicking the checkbox', function() { + describe('clicking the switch', function() { beforeEach(async function() { element.click(); await element.updateComplete; @@ -87,27 +89,53 @@ describe('', function() { describe('when checked attr is present', function() { let element: PfSwitch; + let snapshot: A11yTreeSnapshot; beforeEach(async function() { element = await createFixture(html` - + `); + + await element.updateComplete; + await nextFrame(); + snapshot = await a11ySnapshot({ selector: '#switch' }); }); - it('should display a check icon', async function() { - // TODO: can we test this without inspecting the private shadowRoot? - const svg = element.shadowRoot.querySelector('svg'); - expect(svg).to.be.ok; - expect(svg?.hasAttribute('hidden')).to.be.false; + + it('should be checked', function() { + expect(element.checked).to.be.true; + expect(snapshot.checked).to.be.true; }); }); + describe('when checked attr is not present', function() { + let element: PfSwitch; + let snapshot: A11yTreeSnapshot; + beforeEach(async function() { + element = await createFixture(html` + + `); + + await element.updateComplete; + await nextFrame(); + snapshot = await a11ySnapshot({ selector: '#switch' }); + }); + + it('should be checked', function() { + expect(element.checked).to.be.false; + expect(snapshot.checked).to.be.false; + }); + }); + + describe('when checked and show-check-icon attrs are present', function() { let element: PfSwitch; beforeEach(async function() { const container = await createFixture(html`
- - +
`); element = container.querySelector('pf-switch')!; @@ -125,8 +153,10 @@ describe('', function() { beforeEach(async function() { element = await createFixture(html` - - + `); }); it('should display a check icon', async function() {