Skip to content

Commit eae4332

Browse files
majornistaMichael JordancaseyisonitnikkimkRajdeep Chandra
authored
fix(card): card component checkbox accessibility name and keyboard access #5168 (#5171)
* fix(card): card component checkbox accessibility name and keyboard access #5168 Per #5168 * fix(grid): fix unit tests in grid, implement WAI-ARIA grid pattern * fix(grid): fix unit tests by emulating reduced motion preference * fix(card): add focus ring to #preview in quiet variant * chore: add changeset * fix(card): card should properly support label attribute label is a documented property, and should add an aria-label property to the Card. * fix(card): add documentation for toggles attribute Include code comments to explain prefers-reduced-motion: reduce style. * fix(card): add toggles example to storybook * chore: update current_golden_images_hash * fix(grid): fix checkbox card selection in grid * chore: update current_golden_images_hash * chore: update current_golden_images_hash * chore: addressing feedback Co-authored-by: Nikki Massaro <[email protected]> * chore: remove inaccessible example with todo * chore: replaced nextframe with waitUntil * chore: updated changeset summary * chore: updated to a cleaner and verbose ts definition * chore: added github issue tracking * ci: updated golden image cache * ci: updated golden image cache * chore: added coveregae for new changes in firstUpdated * chore: added test in action menu to improve coverage --------- Co-authored-by: Michael Jordan <[email protected]> Co-authored-by: Casey Eickhoff <[email protected]> Co-authored-by: Casey Eickhoff <[email protected]> Co-authored-by: Nikki Massaro <[email protected]> Co-authored-by: Rajdeep Chandra <[email protected]> Co-authored-by: Rajdeep Chandra <[email protected]>
1 parent 9e950a2 commit eae4332

File tree

12 files changed

+770
-128
lines changed

12 files changed

+770
-128
lines changed

.changeset/thick-keys-tickle.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@spectrum-web-components/card': minor
3+
'@spectrum-web-components/grid': minor
4+
---
5+
6+
Enhanced the Card component's checkbox functionality with improved screen reader support and keyboard navigation.

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ parameters:
2222
# 3. Commit this change to the PR branch where the changes exist.
2323
current_golden_images_hash:
2424
type: string
25-
default: c128578f6599fde71774dae5f1ebe047423aed95
25+
default: e3b8843dd455793122ec53d4f91733eb1487b99a
2626
wireit_cache_name:
2727
type: string
2828
default: wireit

packages/action-menu/test/index.ts

Lines changed: 151 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
html,
1818
nextFrame,
1919
oneEvent,
20-
waitUntil
20+
waitUntil,
2121
} from '@open-wc/testing';
2222
import { testForLitDevWarnings } from '../../../test/testing-helpers';
2323

@@ -43,7 +43,8 @@ import {
4343
a11ySnapshot,
4444
findAccessibilityNode,
4545
sendKeys,
46-
setViewport } from '@web/test-runner-commands';
46+
setViewport,
47+
} from '@web/test-runner-commands';
4748
import { TemplateResult } from '@spectrum-web-components/base';
4849
import { isWebKit } from '@spectrum-web-components/shared';
4950
import { SAFARI_FOCUS_RING_CLASS } from '@spectrum-web-components/picker/src/InteractionController.js';
@@ -107,6 +108,120 @@ export const testActionMenu = (mode: 'sync' | 'async'): void => {
107108

108109
await expect(el).to.be.accessible();
109110
});
111+
112+
describe('accessibility warnings', () => {
113+
let warnSpy: sinon.SinonSpy;
114+
115+
let originalWarn: typeof window.__swc.warn;
116+
117+
beforeEach(() => {
118+
// Create __swc if it doesn't exist
119+
window.__swc = window.__swc || { warn: () => {} };
120+
// Store original warn function
121+
originalWarn = window.__swc.warn;
122+
// Create spy
123+
warnSpy = spy();
124+
window.__swc.warn = warnSpy;
125+
});
126+
127+
afterEach(() => {
128+
// Restore original warn function
129+
window.__swc.warn = originalWarn;
130+
});
131+
132+
it('warns when no accessible label is provided', async () => {
133+
const el = await fixture<ActionMenu>(html`
134+
<sp-action-menu>
135+
<sp-menu-item>Deselect</sp-menu-item>
136+
</sp-action-menu>
137+
`);
138+
139+
await elementUpdated(el);
140+
await nextFrame();
141+
await nextFrame();
142+
143+
expect(warnSpy.called).to.be.true;
144+
expect(warnSpy.firstCall.args[0]).to.equal(el);
145+
expect(warnSpy.firstCall.args[1]).to.equal(
146+
'<sp-action-menu> needs one of the following to be accessible:'
147+
);
148+
expect(warnSpy.firstCall.args[2]).to.equal(
149+
'https://opensource.adobe.com/spectrum-web-components/components/action-menu/#accessibility'
150+
);
151+
expect(warnSpy.firstCall.args[3]).to.deep.equal({
152+
type: 'accessibility',
153+
issues: [
154+
'an <sp-field-label> element with a `for` attribute referencing the `id` of the `<sp-action-menu>`, or',
155+
'value supplied to the "label" attribute, which will be displayed visually as placeholder text',
156+
'text content supplied in a <span> with slot="label", or, text content supplied in a <span> with slot="label-only"',
157+
'which will also be displayed visually as placeholder text.',
158+
],
159+
});
160+
});
161+
162+
it('does not warn when label attribute is provided', async () => {
163+
const el = await fixture<ActionMenu>(html`
164+
<sp-action-menu label="More Actions">
165+
<sp-menu-item>Deselect</sp-menu-item>
166+
</sp-action-menu>
167+
`);
168+
169+
await elementUpdated(el);
170+
171+
expect(warnSpy.called).to.be.false;
172+
});
173+
174+
it('does not warn when label slot is provided', async () => {
175+
const el = await fixture<ActionMenu>(html`
176+
<sp-action-menu>
177+
<span slot="label">More Actions</span>
178+
<sp-menu-item>Deselect</sp-menu-item>
179+
</sp-action-menu>
180+
`);
181+
182+
await elementUpdated(el);
183+
184+
expect(warnSpy.called).to.be.false;
185+
});
186+
187+
it('does not warn when label-only slot is provided', async () => {
188+
const el = await fixture<ActionMenu>(html`
189+
<sp-action-menu>
190+
<span slot="label-only">More Actions</span>
191+
<sp-menu-item>Deselect</sp-menu-item>
192+
</sp-action-menu>
193+
`);
194+
195+
await elementUpdated(el);
196+
197+
expect(warnSpy.called).to.be.false;
198+
});
199+
200+
it('does not warn when aria-label is provided', async () => {
201+
const el = await fixture<ActionMenu>(html`
202+
<sp-action-menu aria-label="More Actions">
203+
<sp-menu-item>Deselect</sp-menu-item>
204+
</sp-action-menu>
205+
`);
206+
207+
await elementUpdated(el);
208+
209+
expect(warnSpy.called).to.be.false;
210+
});
211+
212+
it('does not warn when aria-labelledby is provided', async () => {
213+
const el = await fixture<ActionMenu>(html`
214+
<div id="label-el">More Actions</div>
215+
<sp-action-menu aria-labelledby="label-el">
216+
<sp-menu-item>Deselect</sp-menu-item>
217+
</sp-action-menu>
218+
`);
219+
220+
await elementUpdated(el);
221+
222+
expect(warnSpy.called).to.be.false;
223+
});
224+
});
110225
it('loads - [slot="label"]', async () => {
111226
const el = await fixture<ActionMenu>(html`
112227
<sp-action-menu>
@@ -147,10 +262,9 @@ export const testActionMenu = (mode: 'sync' | 'async'): void => {
147262

148263
await expect(el).to.be.accessible();
149264
});
150-
it('has menuitems in accessibility tree', async() => {
265+
it('has menuitems in accessibility tree', async () => {
151266
const el = await fixture<ActionMenu>(html`
152-
<sp-action-menu
153-
label="More Actions">
267+
<sp-action-menu label="More Actions">
154268
<sp-menu-item>Deselect</sp-menu-item>
155269
<sp-menu-item disabled>Make Work Path</sp-menu-item>
156270
</sp-action-menu>
@@ -161,15 +275,31 @@ export const testActionMenu = (mode: 'sync' | 'async'): void => {
161275
await opened;
162276
await nextFrame();
163277

164-
165-
type NamedNode = { name: string, role: string, disabled: boolean };
166-
const snapshot = (await a11ySnapshot({})) as unknown as NamedNode & {
278+
type NamedNode = { name: string; role: string; disabled: boolean };
279+
const snapshot = (await a11ySnapshot(
280+
{}
281+
)) as unknown as NamedNode & {
167282
children: NamedNode[];
168283
};
169-
const button = findAccessibilityNode<NamedNode>(snapshot, (node) => node.name === 'More Actions');
170-
const menu = findAccessibilityNode<NamedNode>(snapshot, (node) => node.role === 'menu');
171-
const deselect = findAccessibilityNode<NamedNode>(snapshot, (node) => node.role === 'menuitem' && node.name === 'Deselect');
172-
const workPath = findAccessibilityNode<NamedNode>(snapshot, (node) => node.role === 'menuitem' && node.name === 'Make Work Path' && node.disabled);
284+
const button = findAccessibilityNode<NamedNode>(
285+
snapshot,
286+
(node) => node.name === 'More Actions'
287+
);
288+
const menu = findAccessibilityNode<NamedNode>(
289+
snapshot,
290+
(node) => node.role === 'menu'
291+
);
292+
const deselect = findAccessibilityNode<NamedNode>(
293+
snapshot,
294+
(node) => node.role === 'menuitem' && node.name === 'Deselect'
295+
);
296+
const workPath = findAccessibilityNode<NamedNode>(
297+
snapshot,
298+
(node) =>
299+
node.role === 'menuitem' &&
300+
node.name === 'Make Work Path' &&
301+
node.disabled
302+
);
173303
expect(button, 'button').to.not.be.null;
174304
expect(menu, 'menu').to.not.be.null;
175305
expect(deselect, 'first menuitem').to.not.be.null;
@@ -526,14 +656,13 @@ export const testActionMenu = (mode: 'sync' | 'async'): void => {
526656
expect(el.value).to.not.equal(thirdItem.value);
527657
const opened = oneEvent(el, 'sp-opened');
528658
el.focus();
529-
await sendKeys({ press: 'Enter'})
659+
await sendKeys({ press: 'Enter' });
530660
await opened;
531661

532-
await sendKeys({ press: 'Escape'});
533-
await waitUntil(
534-
() => document.activeElement === el,
535-
'focused', { timeout: 300 }
536-
);
662+
await sendKeys({ press: 'Escape' });
663+
await waitUntil(() => document.activeElement === el, 'focused', {
664+
timeout: 300,
665+
});
537666
expect(el.open).to.be.false;
538667
});
539668
it('returns focus on select', async () => {
@@ -545,14 +674,13 @@ export const testActionMenu = (mode: 'sync' | 'async'): void => {
545674
expect(el.value).to.not.equal(thirdItem.value);
546675
const opened = oneEvent(el, 'sp-opened');
547676
el.focus();
548-
await sendKeys({ press: 'Enter'})
677+
await sendKeys({ press: 'Enter' });
549678
await opened;
550679

551680
thirdItem.click();
552-
await waitUntil(
553-
() => document.activeElement === el,
554-
'focused', { timeout: 300 }
555-
);
681+
await waitUntil(() => document.activeElement === el, 'focused', {
682+
timeout: 300,
683+
});
556684
expect(el.open).to.be.false;
557685
});
558686
it('has attribute aria-describedby', async () => {

packages/card/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,18 @@ When leveraging the `asset` attribute, a card can be declared as representing a
232232
</sp-card>
233233
```
234234

235+
#### Toggles
236+
237+
When the `toggles` boolean attribute set to `true`, the card can be toggled between selected and unselected states.
238+
A checkbox will be rendered on hover, focus within, and when the card is selected.
239+
240+
```html
241+
<sp-card toggles variant="quiet" heading="Card Heading" subheading="JPG Photo">
242+
<img alt="" slot="preview" src="https://picsum.photos/200/350" />
243+
<div slot="description">10/15/18</div>
244+
</sp-card>
245+
```
246+
235247
### Accessibility
236248

237249
#### Be concise

0 commit comments

Comments
 (0)