Skip to content

Commit

Permalink
feat(chips): add label slot
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 648883417
  • Loading branch information
material-web-copybara authored and copybara-github committed Jul 15, 2024
1 parent ef91eb2 commit 7a3103f
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 82 deletions.
114 changes: 48 additions & 66 deletions chips/demo/stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,28 +50,22 @@ const assist: MaterialStoryInit<StoryKnobs> = {
const classes = {'scrolling': scrolling};
return html`
<md-chip-set class=${classMap(classes)} aria-label="Assist chips">
<md-assist-chip
label=${label || 'Assist chip'}
?disabled=${disabled}
?elevated=${elevated}></md-assist-chip>
<md-assist-chip
label=${label || 'Assist chip with icon'}
?disabled=${disabled}
?elevated=${elevated}>
<md-assist-chip ?disabled=${disabled} ?elevated=${elevated}>
${label || 'Assist chip'}
</md-assist-chip>
<md-assist-chip ?disabled=${disabled} ?elevated=${elevated}>
<md-icon slot="icon">local_laundry_service</md-icon>
${label || 'Assist chip with icon'}
</md-assist-chip>
<md-assist-chip
label=${label || 'Assist link chip'}
?elevated=${elevated}
href="https://google.com"
target="_blank"
>${GOOGLE_LOGO}</md-assist-chip
>
<md-assist-chip
label=${label || 'Soft-disabled assist chip (focusable)'}
soft-disabled
always-focusable
?elevated=${elevated}></md-assist-chip>
target="_blank">
${GOOGLE_LOGO} ${label || 'Assist link chip'}
</md-assist-chip>
<md-assist-chip soft-disabled always-focusable ?elevated=${elevated}>
${label || 'Soft-disabled assist chip (focusable)'}
</md-assist-chip>
</md-chip-set>
`;
},
Expand All @@ -84,26 +78,23 @@ const filters: MaterialStoryInit<StoryKnobs> = {
const classes = {'scrolling': scrolling};
return html`
<md-chip-set class=${classMap(classes)} aria-label="Filter chips">
<md-filter-chip
label=${label || 'Filter chip'}
?disabled=${disabled}
?elevated=${elevated}></md-filter-chip>
<md-filter-chip
label=${label || 'Filter chip with icon'}
?disabled=${disabled}
?elevated=${elevated}>
<md-filter-chip ?disabled=${disabled} ?elevated=${elevated}>
${label || 'Filter chip'}
</md-filter-chip>
<md-filter-chip ?disabled=${disabled} ?elevated=${elevated}>
<md-icon slot="icon">local_laundry_service</md-icon>
${label || 'Filter chip with icon'}
</md-filter-chip>
<md-filter-chip ?disabled=${disabled} ?elevated=${elevated} removable>
${label || 'Removable filter chip'}
</md-filter-chip>
<md-filter-chip
label=${label || 'Removable filter chip'}
?disabled=${disabled}
?elevated=${elevated}
removable></md-filter-chip>
<md-filter-chip
label=${label || 'Soft-disabled filter chip (focusable)'}
soft-disabled
always-focusable
?elevated=${elevated}
removable></md-filter-chip>
removable>
${label || 'Soft-disabled filter chip (focusable)'}
</md-filter-chip>
</md-chip-set>
`;
},
Expand All @@ -116,35 +107,28 @@ const inputs: MaterialStoryInit<StoryKnobs> = {
const classes = {'scrolling': scrolling};
return html`
<md-chip-set class=${classMap(classes)} aria-label="Input chips">
<md-input-chip
label=${label || 'Input chip'}
?disabled=${disabled}></md-input-chip>
<md-input-chip
label=${label || 'Input chip with icon'}
?disabled=${disabled}>
<md-input-chip ?disabled=${disabled}>
${label || 'Input chip'}
</md-input-chip>
<md-input-chip ?disabled=${disabled}>
<md-icon slot="icon">local_laundry_service</md-icon>
${label || 'Input chip with icon'}
</md-input-chip>
<md-input-chip
label=${label || 'Input chip with avatar'}
?disabled=${disabled}
avatar>
<md-input-chip ?disabled=${disabled} avatar>
<img
slot="icon"
src="https://lh3.googleusercontent.com/a/default-user=s48" />
${label || 'Input chip with avatar'}
</md-input-chip>
<md-input-chip
label=${label || 'Input link chip'}
href="https://google.com"
target="_blank"
>${GOOGLE_LOGO}</md-input-chip
<md-input-chip href="https://google.com" target="_blank"
>${GOOGLE_LOGO} ${label || 'Input link chip'}</md-input-chip
>
<md-input-chip
label=${label || 'Remove-only input chip'}
?disabled=${disabled}
remove-only></md-input-chip>
<md-input-chip
label=${label || 'Soft-disabled input chip (focusable)'}
soft-disabled></md-input-chip>
<md-input-chip ?disabled=${disabled} remove-only>
${label || 'Remove-only input chip'}
</md-input-chip>
<md-input-chip soft-disabled always-focusable>
${label || 'Soft-disabled input chip (focusable)'}
</md-input-chip>
</md-chip-set>
`;
},
Expand All @@ -157,27 +141,25 @@ const suggestions: MaterialStoryInit<StoryKnobs> = {
const classes = {'scrolling': scrolling};
return html`
<md-chip-set class=${classMap(classes)} aria-label="Suggestion chips">
<md-suggestion-chip
label=${label || 'Suggestion chip'}
?disabled=${disabled}
?elevated=${elevated}></md-suggestion-chip>
<md-suggestion-chip
label=${label || 'Suggestion chip with icon'}
?disabled=${disabled}
?elevated=${elevated}>
<md-suggestion-chip ?disabled=${disabled} ?elevated=${elevated}>
${label || 'Suggestion chip'}
</md-suggestion-chip>
<md-suggestion-chip ?disabled=${disabled} ?elevated=${elevated}>
<md-icon slot="icon">local_laundry_service</md-icon>
${label || 'Suggestion chip with icon'}
</md-suggestion-chip>
<md-suggestion-chip
label=${label || 'Suggestion link chip'}
?elevated=${elevated}
href="https://google.com"
target="_blank"
>${GOOGLE_LOGO}</md-suggestion-chip
>${GOOGLE_LOGO} ${label || 'Suggestion link chip'}</md-suggestion-chip
>
<md-suggestion-chip
label=${label || 'Soft-disabled suggestion chip (focusable)'}
soft-disabled
?elevated=${elevated}></md-suggestion-chip>
always-focusable
?elevated=${elevated}>
${label || 'Soft-disabled suggestion chip (focusable)'}
</md-suggestion-chip>
</md-chip-set>
`;
},
Expand Down
7 changes: 6 additions & 1 deletion chips/internal/chip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,11 @@ export abstract class Chip extends chipBaseClass {
@property({type: Boolean, attribute: 'always-focusable'})
alwaysFocusable = false;

// TODO(b/350810013): remove the label property.
/**
* The label of the chip.
*
* @deprecated Set text as content of the chip instead.
*/
@property() label = '';

Expand Down Expand Up @@ -149,7 +152,9 @@ export abstract class Chip extends chipBaseClass {
${this.renderLeadingIcon()}
</span>
<span class="label">
<span class="label-text">${this.label}</span>
<span class="label-text" id="label">
${this.label ? this.label : html`<slot></slot>`}
</span>
</span>
<span class="touch"></span>
`;
Expand Down
11 changes: 9 additions & 2 deletions chips/internal/multi-action-chip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,21 @@ const ARIA_LABEL_REMOVE = 'aria-label-remove';
* A chip component with multiple actions.
*/
export abstract class MultiActionChip extends Chip {
get ariaLabelRemove(): string {
get ariaLabelRemove(): string | null {
if (this.hasAttribute(ARIA_LABEL_REMOVE)) {
return this.getAttribute(ARIA_LABEL_REMOVE)!;
}

const {ariaLabel} = this as ARIAMixinStrict;
return `Remove ${ariaLabel || this.label}`;

// TODO(b/350810013): remove `this.label` when label property is removed.
if (ariaLabel || this.label) {
return `Remove ${ariaLabel || this.label}`;
}

return null;
}

set ariaLabelRemove(ariaLabel: string | null) {
const prev = this.ariaLabelRemove;
if (ariaLabel === prev) {
Expand Down
132 changes: 122 additions & 10 deletions chips/internal/multi-action-chip_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ class TestMultiActionChip extends MultiActionChip {

protected primaryId = 'primary';

protected override renderPrimaryAction() {
return html`<button id="primary"></button>`;
protected override renderPrimaryAction(content: unknown) {
return html`<button id="primary">${content}</button>`;
}

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

async function setupTest() {
const chip = new TestMultiActionChip();
env.render(html`${chip}`);
async function setupTest(
template = html`<test-multi-action-chip></test-multi-action-chip>`,
): Promise<TestMultiActionChip> {
const root = env.render(template);
await env.waitForStability();
const chip = root.querySelector<TestMultiActionChip>(
'test-multi-action-chip',
);
if (!chip) {
throw new Error('Failed to query the rendered <test-multi-action-chip>');
}

return chip;
}

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

it('should provide a default "ariaLabelRemove" value', async () => {
const label = 'Label';
const chip = await setupTest(
html`<test-multi-action-chip>${label}</test-multi-action-chip>`,
);

expect(getA11yLabelForChipRemoveButton(chip)).toEqual(`Remove ${label}`);
});

it('should provide a default "ariaLabelRemove" when "ariaLabel" is provided', async () => {
const label = 'Label';
const chip = await setupTest(
html`<test-multi-action-chip aria-label=${'Descriptive label'}>
${label}
</test-multi-action-chip>`,
);

expect(getA11yLabelForChipRemoveButton(chip)).toEqual(
`Remove ${chip.ariaLabel}`,
);
});

it('should allow setting a custom "ariaLabelRemove"', async () => {
const label = 'Label';
const customAriaLabelRemove = 'Remove custom label';
const chip = await setupTest(
html`<test-multi-action-chip
aria-label=${'Descriptive label'}
aria-label-remove=${customAriaLabelRemove}>
${label}
</test-multi-action-chip>`,
);

expect(getA11yLabelForChipRemoveButton(chip)).toEqual(
customAriaLabelRemove,
);
});

// TODO(b/350810013): remove test when label property is removed.
it('should provide a default "ariaLabelRemove" value (using the label property)', async () => {
const chip = await setupTest();
chip.label = 'Label';
await env.waitForStability();

expect(chip.ariaLabelRemove).toEqual(`Remove ${chip.label}`);
expect(getA11yLabelForChipRemoveButton(chip)).toEqual(
`Remove ${chip.label}`,
);
});

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

expect(chip.ariaLabelRemove).toEqual(`Remove ${chip.ariaLabel}`);
expect(getA11yLabelForChipRemoveButton(chip)).toEqual(
`Remove ${chip.ariaLabel}`,
);
});

it('should allow setting a custom "ariaLabelRemove"', async () => {
// TODO(b/350810013): remove test when label property is removed.
it('should allow setting a custom "ariaLabelRemove" (using the label property)', async () => {
const chip = await setupTest();
chip.label = 'Label';
chip.ariaLabel = 'Descriptive label';
const customAriaLabelRemove = 'Remove custom label';
chip.ariaLabelRemove = customAriaLabelRemove;
await env.waitForStability();

expect(chip.ariaLabelRemove).toEqual(customAriaLabelRemove);
expect(getA11yLabelForChipRemoveButton(chip)).toEqual(
customAriaLabelRemove,
);
});
});
});

/**
* Returns the text content of a slot.
*/
function getSlotTextContent(slot: HTMLSlotElement) {
// Remove any newlines, comments, and whitespace from the label slot.
let text = '';
for (const node of slot.assignedNodes() ?? []) {
if (node.nodeType === Node.TEXT_NODE) {
text += node.textContent?.trim() || '';
}
}
return text;
}

/**
* Returns the a11y label of the remove button. If the button has an aria-label,
* it will return that. If it has aria-labelledby, it will return the text
* content of the elements it is labelled by.
*/
function getA11yLabelForChipRemoveButton(chip: TestMultiActionChip): string {
const removeButton = chip.shadowRoot!.querySelector<HTMLButtonElement>(
'button.trailing.action',
)!;

if (removeButton.ariaLabel) {
return removeButton.ariaLabel;
}

// If the remove button is not aria-labelled, it should be aria-labelledby.
const removeButtonAriaLabelledBy =
removeButton.getAttribute('aria-labelledby')!;
const elementsLabelledBy: HTMLElement[] = [];
removeButtonAriaLabelledBy.split(' ').forEach((id) => {
const labelledByElement = chip.shadowRoot?.getElementById(id);
if (!labelledByElement) {
throw new Error(
`Cannot find element with ID "#{id}" in the chip's shadow root`,
);
}
elementsLabelledBy.push(labelledByElement);
});
const textFromAriaLabelledBy: string[] = [];
elementsLabelledBy.forEach((element) => {
const unnamedSlotChildElement =
element.querySelector<HTMLSlotElement>('slot:not([name])');
if (unnamedSlotChildElement) {
textFromAriaLabelledBy.push(getSlotTextContent(unnamedSlotChildElement));
} else {
textFromAriaLabelledBy.push(element.textContent ?? '');
}
});
return textFromAriaLabelledBy.join(' ');
}
Loading

0 comments on commit 7a3103f

Please sign in to comment.