Skip to content

Commit 7ec70c4

Browse files
feat(chips): add label slot
PiperOrigin-RevId: 652636936
1 parent ef91eb2 commit 7ec70c4

File tree

6 files changed

+193
-82
lines changed

6 files changed

+193
-82
lines changed

chips/demo/stories.ts

Lines changed: 48 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -50,28 +50,22 @@ const assist: MaterialStoryInit<StoryKnobs> = {
5050
const classes = {'scrolling': scrolling};
5151
return html`
5252
<md-chip-set class=${classMap(classes)} aria-label="Assist chips">
53-
<md-assist-chip
54-
label=${label || 'Assist chip'}
55-
?disabled=${disabled}
56-
?elevated=${elevated}></md-assist-chip>
57-
<md-assist-chip
58-
label=${label || 'Assist chip with icon'}
59-
?disabled=${disabled}
60-
?elevated=${elevated}>
53+
<md-assist-chip ?disabled=${disabled} ?elevated=${elevated}>
54+
${label || 'Assist chip'}
55+
</md-assist-chip>
56+
<md-assist-chip ?disabled=${disabled} ?elevated=${elevated}>
6157
<md-icon slot="icon">local_laundry_service</md-icon>
58+
${label || 'Assist chip with icon'}
6259
</md-assist-chip>
6360
<md-assist-chip
64-
label=${label || 'Assist link chip'}
6561
?elevated=${elevated}
6662
href="https://google.com"
67-
target="_blank"
68-
>${GOOGLE_LOGO}</md-assist-chip
69-
>
70-
<md-assist-chip
71-
label=${label || 'Soft-disabled assist chip (focusable)'}
72-
soft-disabled
73-
always-focusable
74-
?elevated=${elevated}></md-assist-chip>
63+
target="_blank">
64+
${GOOGLE_LOGO} ${label || 'Assist link chip'}
65+
</md-assist-chip>
66+
<md-assist-chip soft-disabled always-focusable ?elevated=${elevated}>
67+
${label || 'Soft-disabled assist chip (focusable)'}
68+
</md-assist-chip>
7569
</md-chip-set>
7670
`;
7771
},
@@ -84,26 +78,23 @@ const filters: MaterialStoryInit<StoryKnobs> = {
8478
const classes = {'scrolling': scrolling};
8579
return html`
8680
<md-chip-set class=${classMap(classes)} aria-label="Filter chips">
87-
<md-filter-chip
88-
label=${label || 'Filter chip'}
89-
?disabled=${disabled}
90-
?elevated=${elevated}></md-filter-chip>
91-
<md-filter-chip
92-
label=${label || 'Filter chip with icon'}
93-
?disabled=${disabled}
94-
?elevated=${elevated}>
81+
<md-filter-chip ?disabled=${disabled} ?elevated=${elevated}>
82+
${label || 'Filter chip'}
83+
</md-filter-chip>
84+
<md-filter-chip ?disabled=${disabled} ?elevated=${elevated}>
9585
<md-icon slot="icon">local_laundry_service</md-icon>
86+
${label || 'Filter chip with icon'}
87+
</md-filter-chip>
88+
<md-filter-chip ?disabled=${disabled} ?elevated=${elevated} removable>
89+
${label || 'Removable filter chip'}
9690
</md-filter-chip>
9791
<md-filter-chip
98-
label=${label || 'Removable filter chip'}
99-
?disabled=${disabled}
100-
?elevated=${elevated}
101-
removable></md-filter-chip>
102-
<md-filter-chip
103-
label=${label || 'Soft-disabled filter chip (focusable)'}
10492
soft-disabled
93+
always-focusable
10594
?elevated=${elevated}
106-
removable></md-filter-chip>
95+
removable>
96+
${label || 'Soft-disabled filter chip (focusable)'}
97+
</md-filter-chip>
10798
</md-chip-set>
10899
`;
109100
},
@@ -116,35 +107,28 @@ const inputs: MaterialStoryInit<StoryKnobs> = {
116107
const classes = {'scrolling': scrolling};
117108
return html`
118109
<md-chip-set class=${classMap(classes)} aria-label="Input chips">
119-
<md-input-chip
120-
label=${label || 'Input chip'}
121-
?disabled=${disabled}></md-input-chip>
122-
<md-input-chip
123-
label=${label || 'Input chip with icon'}
124-
?disabled=${disabled}>
110+
<md-input-chip ?disabled=${disabled}>
111+
${label || 'Input chip'}
112+
</md-input-chip>
113+
<md-input-chip ?disabled=${disabled}>
125114
<md-icon slot="icon">local_laundry_service</md-icon>
115+
${label || 'Input chip with icon'}
126116
</md-input-chip>
127-
<md-input-chip
128-
label=${label || 'Input chip with avatar'}
129-
?disabled=${disabled}
130-
avatar>
117+
<md-input-chip ?disabled=${disabled} avatar>
131118
<img
132119
slot="icon"
133120
src="https://lh3.googleusercontent.com/a/default-user=s48" />
121+
${label || 'Input chip with avatar'}
134122
</md-input-chip>
135-
<md-input-chip
136-
label=${label || 'Input link chip'}
137-
href="https://google.com"
138-
target="_blank"
139-
>${GOOGLE_LOGO}</md-input-chip
123+
<md-input-chip href="https://google.com" target="_blank"
124+
>${GOOGLE_LOGO} ${label || 'Input link chip'}</md-input-chip
140125
>
141-
<md-input-chip
142-
label=${label || 'Remove-only input chip'}
143-
?disabled=${disabled}
144-
remove-only></md-input-chip>
145-
<md-input-chip
146-
label=${label || 'Soft-disabled input chip (focusable)'}
147-
soft-disabled></md-input-chip>
126+
<md-input-chip ?disabled=${disabled} remove-only>
127+
${label || 'Remove-only input chip'}
128+
</md-input-chip>
129+
<md-input-chip soft-disabled always-focusable>
130+
${label || 'Soft-disabled input chip (focusable)'}
131+
</md-input-chip>
148132
</md-chip-set>
149133
`;
150134
},
@@ -157,27 +141,25 @@ const suggestions: MaterialStoryInit<StoryKnobs> = {
157141
const classes = {'scrolling': scrolling};
158142
return html`
159143
<md-chip-set class=${classMap(classes)} aria-label="Suggestion chips">
160-
<md-suggestion-chip
161-
label=${label || 'Suggestion chip'}
162-
?disabled=${disabled}
163-
?elevated=${elevated}></md-suggestion-chip>
164-
<md-suggestion-chip
165-
label=${label || 'Suggestion chip with icon'}
166-
?disabled=${disabled}
167-
?elevated=${elevated}>
144+
<md-suggestion-chip ?disabled=${disabled} ?elevated=${elevated}>
145+
${label || 'Suggestion chip'}
146+
</md-suggestion-chip>
147+
<md-suggestion-chip ?disabled=${disabled} ?elevated=${elevated}>
168148
<md-icon slot="icon">local_laundry_service</md-icon>
149+
${label || 'Suggestion chip with icon'}
169150
</md-suggestion-chip>
170151
<md-suggestion-chip
171-
label=${label || 'Suggestion link chip'}
172152
?elevated=${elevated}
173153
href="https://google.com"
174154
target="_blank"
175-
>${GOOGLE_LOGO}</md-suggestion-chip
155+
>${GOOGLE_LOGO} ${label || 'Suggestion link chip'}</md-suggestion-chip
176156
>
177157
<md-suggestion-chip
178-
label=${label || 'Soft-disabled suggestion chip (focusable)'}
179158
soft-disabled
180-
?elevated=${elevated}></md-suggestion-chip>
159+
always-focusable
160+
?elevated=${elevated}>
161+
${label || 'Soft-disabled suggestion chip (focusable)'}
162+
</md-suggestion-chip>
181163
</md-chip-set>
182164
`;
183165
},

chips/internal/chip.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,11 @@ export abstract class Chip extends chipBaseClass {
5858
@property({type: Boolean, attribute: 'always-focusable'})
5959
alwaysFocusable = false;
6060

61+
// TODO(b/350810013): remove the label property.
6162
/**
6263
* The label of the chip.
64+
*
65+
* @deprecated Set text as content of the chip instead.
6366
*/
6467
@property() label = '';
6568

@@ -149,7 +152,9 @@ export abstract class Chip extends chipBaseClass {
149152
${this.renderLeadingIcon()}
150153
</span>
151154
<span class="label">
152-
<span class="label-text">${this.label}</span>
155+
<span class="label-text" id="label">
156+
${this.label ? this.label : html`<slot></slot>`}
157+
</span>
153158
</span>
154159
<span class="touch"></span>
155160
`;

chips/internal/multi-action-chip.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,21 @@ const ARIA_LABEL_REMOVE = 'aria-label-remove';
1616
* A chip component with multiple actions.
1717
*/
1818
export abstract class MultiActionChip extends Chip {
19-
get ariaLabelRemove(): string {
19+
get ariaLabelRemove(): string | null {
2020
if (this.hasAttribute(ARIA_LABEL_REMOVE)) {
2121
return this.getAttribute(ARIA_LABEL_REMOVE)!;
2222
}
2323

2424
const {ariaLabel} = this as ARIAMixinStrict;
25-
return `Remove ${ariaLabel || this.label}`;
25+
26+
// TODO(b/350810013): remove `this.label` when label property is removed.
27+
if (ariaLabel || this.label) {
28+
return `Remove ${ariaLabel || this.label}`;
29+
}
30+
31+
return null;
2632
}
33+
2734
set ariaLabelRemove(ariaLabel: string | null) {
2835
const prev = this.ariaLabelRemove;
2936
if (ariaLabel === prev) {

chips/internal/multi-action-chip_test.ts

Lines changed: 122 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ class TestMultiActionChip extends MultiActionChip {
2929

3030
protected primaryId = 'primary';
3131

32-
protected override renderPrimaryAction() {
33-
return html`<button id="primary"></button>`;
32+
protected override renderPrimaryAction(content: unknown) {
33+
return html`<button id="primary">${content}</button>`;
3434
}
3535

3636
protected override renderTrailingAction(focusListener: EventListener) {
@@ -49,10 +49,18 @@ class TestMultiActionChip extends MultiActionChip {
4949
describe('Multi-action chips', () => {
5050
const env = new Environment();
5151

52-
async function setupTest() {
53-
const chip = new TestMultiActionChip();
54-
env.render(html`${chip}`);
52+
async function setupTest(
53+
template = html`<test-multi-action-chip></test-multi-action-chip>`,
54+
): Promise<TestMultiActionChip> {
55+
const root = env.render(template);
5556
await env.waitForStability();
57+
const chip = root.querySelector<TestMultiActionChip>(
58+
'test-multi-action-chip',
59+
);
60+
if (!chip) {
61+
throw new Error('Failed to query the rendered <test-multi-action-chip>');
62+
}
63+
5664
return chip;
5765
}
5866

@@ -222,28 +230,132 @@ describe('Multi-action chips', () => {
222230
});
223231

224232
it('should provide a default "ariaLabelRemove" value', async () => {
233+
const label = 'Label';
234+
const chip = await setupTest(
235+
html`<test-multi-action-chip>${label}</test-multi-action-chip>`,
236+
);
237+
238+
expect(getA11yLabelForChipRemoveButton(chip)).toEqual(`Remove ${label}`);
239+
});
240+
241+
it('should provide a default "ariaLabelRemove" when "ariaLabel" is provided', async () => {
242+
const label = 'Label';
243+
const chip = await setupTest(
244+
html`<test-multi-action-chip aria-label=${'Descriptive label'}>
245+
${label}
246+
</test-multi-action-chip>`,
247+
);
248+
249+
expect(getA11yLabelForChipRemoveButton(chip)).toEqual(
250+
`Remove ${chip.ariaLabel}`,
251+
);
252+
});
253+
254+
it('should allow setting a custom "ariaLabelRemove"', async () => {
255+
const label = 'Label';
256+
const customAriaLabelRemove = 'Remove custom label';
257+
const chip = await setupTest(
258+
html`<test-multi-action-chip
259+
aria-label=${'Descriptive label'}
260+
aria-label-remove=${customAriaLabelRemove}>
261+
${label}
262+
</test-multi-action-chip>`,
263+
);
264+
265+
expect(getA11yLabelForChipRemoveButton(chip)).toEqual(
266+
customAriaLabelRemove,
267+
);
268+
});
269+
270+
// TODO(b/350810013): remove test when label property is removed.
271+
it('should provide a default "ariaLabelRemove" value (using the label property)', async () => {
225272
const chip = await setupTest();
226273
chip.label = 'Label';
274+
await env.waitForStability();
227275

228-
expect(chip.ariaLabelRemove).toEqual(`Remove ${chip.label}`);
276+
expect(getA11yLabelForChipRemoveButton(chip)).toEqual(
277+
`Remove ${chip.label}`,
278+
);
229279
});
230280

231-
it('should provide a default "ariaLabelRemove" when "ariaLabel" is provided', async () => {
281+
// TODO(b/350810013): remove test when label property is removed.
282+
it('should provide a default "ariaLabelRemove" when "ariaLabel" is provided (using the label property)', async () => {
232283
const chip = await setupTest();
233284
chip.label = 'Label';
234285
chip.ariaLabel = 'Descriptive label';
286+
await env.waitForStability();
235287

236-
expect(chip.ariaLabelRemove).toEqual(`Remove ${chip.ariaLabel}`);
288+
expect(getA11yLabelForChipRemoveButton(chip)).toEqual(
289+
`Remove ${chip.ariaLabel}`,
290+
);
237291
});
238292

239-
it('should allow setting a custom "ariaLabelRemove"', async () => {
293+
// TODO(b/350810013): remove test when label property is removed.
294+
it('should allow setting a custom "ariaLabelRemove" (using the label property)', async () => {
240295
const chip = await setupTest();
241296
chip.label = 'Label';
242297
chip.ariaLabel = 'Descriptive label';
243298
const customAriaLabelRemove = 'Remove custom label';
244299
chip.ariaLabelRemove = customAriaLabelRemove;
300+
await env.waitForStability();
245301

246-
expect(chip.ariaLabelRemove).toEqual(customAriaLabelRemove);
302+
expect(getA11yLabelForChipRemoveButton(chip)).toEqual(
303+
customAriaLabelRemove,
304+
);
247305
});
248306
});
249307
});
308+
309+
/**
310+
* Returns the text content of a slot.
311+
*/
312+
function getSlotTextContent(slot: HTMLSlotElement) {
313+
// Remove any newlines, comments, and whitespace from the label slot.
314+
let text = '';
315+
for (const node of slot.assignedNodes() ?? []) {
316+
if (node.nodeType === Node.TEXT_NODE) {
317+
text += node.textContent?.trim() || '';
318+
}
319+
}
320+
return text;
321+
}
322+
323+
/**
324+
* Returns the a11y label of the remove button. If the button has an aria-label,
325+
* it will return that. If it has aria-labelledby, it will return the text
326+
* content of the elements it is labelled by.
327+
*/
328+
function getA11yLabelForChipRemoveButton(chip: TestMultiActionChip): string {
329+
const removeButton = chip.shadowRoot!.querySelector<HTMLButtonElement>(
330+
'button.trailing.action',
331+
)!;
332+
333+
if (removeButton.ariaLabel) {
334+
return removeButton.ariaLabel;
335+
}
336+
337+
// If the remove button is not aria-labelled, it should be aria-labelledby.
338+
const removeButtonAriaLabelledBy =
339+
removeButton.getAttribute('aria-labelledby')!;
340+
const elementsLabelledBy: HTMLElement[] = [];
341+
removeButtonAriaLabelledBy.split(' ').forEach((id) => {
342+
const labelledByElement = chip.shadowRoot?.getElementById(id);
343+
if (!labelledByElement) {
344+
throw new Error(
345+
`Cannot find element with ID "#{id}" in the chip's shadow root`,
346+
);
347+
}
348+
elementsLabelledBy.push(labelledByElement);
349+
});
350+
const textFromAriaLabelledBy: string[] = [];
351+
elementsLabelledBy.forEach((element) => {
352+
const unnamedSlotChildElement =
353+
element.querySelector<HTMLSlotElement>('slot:not([name])');
354+
if (unnamedSlotChildElement) {
355+
textFromAriaLabelledBy.push(getSlotTextContent(unnamedSlotChildElement));
356+
} else {
357+
textFromAriaLabelledBy.push(element.textContent ?? '');
358+
}
359+
});
360+
return textFromAriaLabelledBy.join(' ');
361+
}

0 commit comments

Comments
 (0)