From 86918f4ccb4000ed0e3db4c378fdf5c60d7327e3 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Tue, 14 Apr 2026 12:03:00 +0200 Subject: [PATCH 01/30] =?UTF-8?q?=F0=9F=90=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/tag/src/tag.scss | 4 +- packages/components/tag/src/tag.spec.ts | 62 +++++++++++------ packages/components/tag/src/tag.stories.ts | 10 ++- packages/components/tag/src/tag.ts | 77 +++++++++++----------- packages/locales/src/nl.ts | 4 +- packages/locales/src/nl.xlf | 8 +-- 6 files changed, 94 insertions(+), 71 deletions(-) diff --git a/packages/components/tag/src/tag.scss b/packages/components/tag/src/tag.scss index 8222ecf524..952517c7d4 100644 --- a/packages/components/tag/src/tag.scss +++ b/packages/components/tag/src/tag.scss @@ -47,7 +47,6 @@ --_br-color: var(--sl-color-border-disabled); color: var(--sl-color-foreground-disabled); - pointer-events: none; slot, button { @@ -55,7 +54,7 @@ } } -:host(:focus-visible) { +:host(:state(focus-visible)) { outline-color: var(--sl-color-border-focused); position: relative; z-index: 1; // Make sure the focus ring is above other elements @@ -84,6 +83,7 @@ button { flex-shrink: 0; inline-size: calc(var(--sl-size-300) - var(--sl-size-borderWidth-default) * 2); justify-content: center; + outline: 0; padding: 0; @media (prefers-reduced-motion: no-preference) { diff --git a/packages/components/tag/src/tag.spec.ts b/packages/components/tag/src/tag.spec.ts index 3bbd9de867..3ce78d164f 100644 --- a/packages/components/tag/src/tag.spec.ts +++ b/packages/components/tag/src/tag.spec.ts @@ -14,7 +14,7 @@ describe('sl-tag', () => { el = await fixture(html`My label`); }); - it('should not have an explicit', () => { + it('should not have an explicit size', () => { expect(el).not.to.have.attribute('size'); expect(el.size).to.be.undefined; }); @@ -47,63 +47,83 @@ describe('sl-tag', () => { el.removable = true; await el.updateComplete; + expect(el).to.have.attribute('removable'); expect(el.renderRoot.querySelector('button')).to.exist; }); - it('should not have a tabindex', () => { - expect(el).not.to.have.attribute('tabindex'); - }); - it('should not have a tooltip', async () => { el.focus(); await el.updateComplete; expect(el).not.to.have.attribute('aria-describedby'); }); + + it('should not be focusable', async () => { + el.focus(); + await el.updateComplete; + + expect(el).not.to.match(':focus'); + expect(el).not.to.match(':state(focus-visible)'); + }); }); describe('removable', () => { + let button: HTMLButtonElement; + beforeEach(async () => { el = await fixture(html`My label`); + button = el.renderRoot.querySelector('button')!; }); - it('should have an ARIA description indicating how to remove the tag', () => { - expect(el).to.have.attribute('aria-description', 'Press the delete or backspace key to remove this item'); + it('should not have the focus-visible state', () => { + expect(el).not.to.match(':state(focus-visible)'); }); - it('should have a tabindex of 0', () => { - expect(el).to.have.attribute('tabindex', '0'); + it('should have the focus-visible state when focused', async () => { + el.focus(); + await el.updateComplete; + + expect(el).to.match(':state(focus-visible)'); }); - it('should have a tabindex of -1 when disabled', async () => { - el.disabled = true; + it('should have a button', () => { + expect(button).to.exist; + }); + + it('should focus the button when the tag is focused', async () => { + el.focus(); await el.updateComplete; - expect(el).to.have.attribute('tabindex', '-1'); + expect(button).to.match(':focus'); }); - it('should have a button', () => { - expect(el.renderRoot.querySelector('button')).to.exist; + it('should have an accessible label on the remove button', () => { + expect(button).to.have.attribute('aria-label', "Remove tag 'My label'"); }); - it('should hide the button for ARIA', () => { - expect(el.renderRoot.querySelector('button')).to.have.attribute('aria-hidden', 'true'); + it('should mark the button as aria-disabled when the tag is disabled', async () => { + el.disabled = true; + await el.updateComplete; + + expect(button).to.have.attribute('aria-disabled', 'true'); }); it('should not be be removed when it is disabled and remove button is clicked', async () => { - el.setAttribute('disabled', ''); + const onRemove = spy(el, 'remove'); + + el.disabled = true; await el.updateComplete; - el.renderRoot.querySelector('button')?.click(); + button.click(); await el.updateComplete; - expect(el).to.exist; + expect(onRemove).not.to.have.been.called; }); it('should be removed when the button is clicked using the keyboard', async () => { const onRemove = spy(el, 'remove'); - el.renderRoot.querySelector('button')?.focus(); + el.focus(); await userEvent.keyboard('{Enter}'); expect(onRemove).to.have.been.calledOnce; @@ -131,7 +151,7 @@ describe('sl-tag', () => { const onRemove = spy(); el.addEventListener('sl-remove', onRemove); - el.renderRoot.querySelector('button')?.click(); + button.click(); await el.updateComplete; expect(onRemove).to.have.been.calledOnce; diff --git a/packages/components/tag/src/tag.stories.ts b/packages/components/tag/src/tag.stories.ts index eade687608..d206b734ca 100644 --- a/packages/components/tag/src/tag.stories.ts +++ b/packages/components/tag/src/tag.stories.ts @@ -1,7 +1,6 @@ import { type Meta, type StoryObj } from '@storybook/web-components-vite'; import { html } from 'lit'; import { ifDefined } from 'lit/directives/if-defined.js'; -import { styleMap } from 'lit/directives/style-map.js'; import '../register.js'; import { type Tag } from './tag.js'; @@ -44,7 +43,7 @@ export default { ?disabled=${disabled} ?removable=${removable} size=${ifDefined(size)} - style=${styleMap({ maxWidth })} + style=${ifDefined(maxWidth ? `max-inline-size: ${maxWidth}` : undefined)} variant=${ifDefined(variant)} > ${label} @@ -78,3 +77,10 @@ export const Removable: Story = { removable: true } }; + +export const RemovableDisabled: Story = { + args: { + disabled: true, + removable: true + } +}; diff --git a/packages/components/tag/src/tag.ts b/packages/components/tag/src/tag.ts index b8190b2f01..af43803000 100644 --- a/packages/components/tag/src/tag.ts +++ b/packages/components/tag/src/tag.ts @@ -1,10 +1,11 @@ -import { localized, msg } from '@lit/localize'; +import { localized, msg, str } from '@lit/localize'; import { type ScopedElementsMap, ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js'; import { Icon } from '@sl-design-system/icon'; -import { EventEmitter, EventsController, event } from '@sl-design-system/shared'; +import { EventEmitter, event } from '@sl-design-system/shared'; import { Tooltip } from '@sl-design-system/tooltip'; -import { type CSSResultGroup, LitElement, type PropertyValues, type TemplateResult, html, nothing } from 'lit'; +import { type CSSResultGroup, LitElement, type TemplateResult, html, nothing } from 'lit'; import { property, state } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; import styles from './tag.scss.js'; declare global { @@ -29,7 +30,12 @@ export type TagVariant = 'neutral' | 'info'; * Tag label * ``` * + * @customElement sl-tag + * * @slot default - The tag label. + * + * @csspart label - The wrapper around the tag label. + * @csspart button - The remove button. */ @localized() export class Tag extends ScopedElementsMixin(LitElement) { @@ -41,11 +47,17 @@ export class Tag extends ScopedElementsMixin(LitElement) { }; } + /** @internal */ + static override shadowRootOptions: ShadowRootInit = { + ...LitElement.shadowRootOptions, + delegatesFocus: true + }; + /** @internal */ static override styles: CSSResultGroup = styles; - // eslint-disable-next-line no-unused-private-class-members - #events = new EventsController(this, { keydown: this.#onKeydown }); + /** @internal */ + #internals = this.attachInternals(); /** * Observe changes in size, so we can check whether we need to show tooltips @@ -69,7 +81,7 @@ export class Tag extends ScopedElementsMixin(LitElement) { * Whether the tag component is removable. * @default false */ - @property({ type: Boolean }) removable?: boolean; + @property({ type: Boolean, reflect: true }) removable?: boolean; /** @internal Emits when the tag is removed. */ @event({ name: 'sl-remove' }) removeEvent!: EventEmitter; @@ -106,35 +118,20 @@ export class Tag extends ScopedElementsMixin(LitElement) { super.disconnectedCallback(); } - override updated(changes: PropertyValues): void { - super.updated(changes); - - if (changes.has('disabled') || changes.has('removable')) { - if (this.removable) { - this.setAttribute('tabindex', this.disabled ? '-1' : '0'); - } else if (this.disabled || changes.get('removable')) { - this.removeAttribute('tabindex'); - } - } - - if (changes.has('removable')) { - if (this.removable) { - this.setAttribute( - 'aria-description', - msg('Press the delete or backspace key to remove this item', { id: 'sl.tag.removalInstructions' }) - ); - } else { - this.removeAttribute('aria-description'); - } - } - } - override render(): TemplateResult { return html` - ${this.removable && !this.disabled + ${this.removable ? html` - ` @@ -142,8 +139,16 @@ export class Tag extends ScopedElementsMixin(LitElement) { `; } + #onBlur(): void { + this.#internals.states.delete('focus-visible'); + } + + #onFocus(): void { + this.#internals.states.add('focus-visible'); + } + #onKeydown(event: KeyboardEvent): void { - if (this.removable && (event.key === 'Backspace' || event.key === 'Delete')) { + if (event.key === 'Backspace' || event.key === 'Delete') { this.#onRemove(event); } } @@ -180,14 +185,6 @@ export class Tag extends ScopedElementsMixin(LitElement) { this.#tooltip(); this.#tooltip = undefined; } - - // If the contents of the tag overflows, make sure it is keyboard focusable, - // so the user can tab to it. - if (!this.disabled && (this.removable || this.#tooltip)) { - this.setAttribute('tabindex', '0'); - } else if (!this.hasAttribute('aria-labelledby')) { - this.removeAttribute('tabindex'); - } } #onSlotChange(event: Event & { target: HTMLSlotElement }): void { diff --git a/packages/locales/src/nl.ts b/packages/locales/src/nl.ts index 50c59cfb8e..c019ab754a 100644 --- a/packages/locales/src/nl.ts +++ b/packages/locales/src/nl.ts @@ -99,7 +99,6 @@ export const templates = { 'sl.select.validation.valueMissing': 'Kies een optie uit de lijst.', 'sl.tabs.showAll': 'Toon alles', 'sl.tag.listOfHiddenElements': 'Lijst met verborgen elementen', - 'sl.tag.removalInstructions': 'Druk op de delete- of backspacetoets om dit item te verwijderen', 'sl.timeField.empty': 'Leeg', 'sl.timeField.rangeOverflow': str`Voer een tijd in die niet later is dan ${0}.`, 'sl.timeField.rangeUnderflow': str`Voer een tijd in die niet eerder is dan ${0}.`, @@ -109,5 +108,6 @@ export const templates = { 'sl.timeField.typeMismatch': 'Voer een geldige tijd in.', 'sl.timeField.valueMissing': 'Voer een tijd in.', 'sl.toolBar.showMore': 'Meer tonen', - 'sl.tree.loadingMessage': 'Laden' + 'sl.tree.loadingMessage': 'Laden', + 'sl.tag.removeLabel': str`Remove tag '${0}'` }; diff --git a/packages/locales/src/nl.xlf b/packages/locales/src/nl.xlf index 3bcb5cf80c..7bd46f8ea0 100644 --- a/packages/locales/src/nl.xlf +++ b/packages/locales/src/nl.xlf @@ -10,10 +10,6 @@ Loading Laden - - Press the delete or backspace key to remove this item - Druk op de delete- of backspacetoets om dit item te verwijderen - Selected Geselecteerd @@ -426,6 +422,10 @@ No later than Uiterlijk + + Remove tag '' + Verwijder tag '' + From 2b17554d8cb6374b7d5b3504a1511f90e8b0ba07 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Tue, 14 Apr 2026 12:34:08 +0200 Subject: [PATCH 02/30] =?UTF-8?q?=E2=9A=BD=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/tag/src/tag-list.ts | 3 +++ packages/locales/src/nl.ts | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/components/tag/src/tag-list.ts b/packages/components/tag/src/tag-list.ts index b159ea00cb..60a028a25b 100644 --- a/packages/components/tag/src/tag-list.ts +++ b/packages/components/tag/src/tag-list.ts @@ -25,6 +25,8 @@ declare global { * * ``` * + * @customElement sl-tag-list + * * @slot default - The place for tags. */ @localized() @@ -200,6 +202,7 @@ export class TagList extends ScopedElementsMixin(LitElement) { ); this.tags.forEach(tag => { + tag.role = 'listitem'; tag.size = this.size; tag.variant = this.variant; tag.setAttribute('role', 'listitem'); diff --git a/packages/locales/src/nl.ts b/packages/locales/src/nl.ts index c019ab754a..a00ea565f0 100644 --- a/packages/locales/src/nl.ts +++ b/packages/locales/src/nl.ts @@ -99,6 +99,7 @@ export const templates = { 'sl.select.validation.valueMissing': 'Kies een optie uit de lijst.', 'sl.tabs.showAll': 'Toon alles', 'sl.tag.listOfHiddenElements': 'Lijst met verborgen elementen', + 'sl.tag.remove': str`Verwijder tag '${0}'`, 'sl.timeField.empty': 'Leeg', 'sl.timeField.rangeOverflow': str`Voer een tijd in die niet later is dan ${0}.`, 'sl.timeField.rangeUnderflow': str`Voer een tijd in die niet eerder is dan ${0}.`, @@ -108,6 +109,5 @@ export const templates = { 'sl.timeField.typeMismatch': 'Voer een geldige tijd in.', 'sl.timeField.valueMissing': 'Voer een tijd in.', 'sl.toolBar.showMore': 'Meer tonen', - 'sl.tree.loadingMessage': 'Laden', - 'sl.tag.removeLabel': str`Remove tag '${0}'` + 'sl.tree.loadingMessage': 'Laden' }; From 490440be63aca8bd6a678fb2d68c824f22fd9685 Mon Sep 17 00:00:00 2001 From: Jeroen Zwartepoorte Date: Tue, 21 Apr 2026 14:46:05 +0200 Subject: [PATCH 03/30] =?UTF-8?q?=F0=9F=8F=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/tag/src/tag.scss | 8 ++-- packages/components/tag/src/tag.spec.ts | 8 ++-- packages/components/tag/src/tag.stories.ts | 7 ++++ packages/components/tag/src/tag.ts | 48 ++++++++-------------- 4 files changed, 34 insertions(+), 37 deletions(-) diff --git a/packages/components/tag/src/tag.scss b/packages/components/tag/src/tag.scss index 952517c7d4..197927c254 100644 --- a/packages/components/tag/src/tag.scss +++ b/packages/components/tag/src/tag.scss @@ -21,12 +21,12 @@ } } -:host([removable]) slot { +:host([removable]) [part='label'] { border-inline-end: var(--sl-size-borderWidth-default) solid var(--_br-color); } :host([size='lg']) { - slot { + [part='label'] { padding: calc(var(--sl-size-100) - var(--sl-size-borderWidth-default)) var(--sl-size-150); } @@ -48,7 +48,7 @@ color: var(--sl-color-foreground-disabled); - slot, + [part='label'], button { background: var(--sl-color-background-disabled); } @@ -60,7 +60,7 @@ z-index: 1; // Make sure the focus ring is above other elements } -slot { +[part='label'] { background: var(--_bg-color); display: block; flex: 1; diff --git a/packages/components/tag/src/tag.spec.ts b/packages/components/tag/src/tag.spec.ts index 3ce78d164f..50e7e3854d 100644 --- a/packages/components/tag/src/tag.spec.ts +++ b/packages/components/tag/src/tag.spec.ts @@ -55,7 +55,8 @@ describe('sl-tag', () => { el.focus(); await el.updateComplete; - expect(el).not.to.have.attribute('aria-describedby'); + expect(el.renderRoot.querySelector('[part="label"]')).not.to.have.attribute('aria-describedby'); + expect(el.renderRoot.querySelector('sl-tooltip')).not.to.exist; }); it('should not be focusable', async () => { @@ -170,9 +171,10 @@ describe('sl-tag', () => { el.focus(); await el.updateComplete; - expect(el).to.have.attribute('aria-describedby'); + const label = el.renderRoot.querySelector('[part="label"]')!; + expect(label).to.have.attribute('aria-describedby'); - const tooltip = document.getElementById(el.getAttribute('aria-describedby')!); + const tooltip = el.renderRoot.querySelector('sl-tooltip'); expect(tooltip).to.exist; expect(tooltip).to.have.trimmed.text('My label is very long'); }); diff --git a/packages/components/tag/src/tag.stories.ts b/packages/components/tag/src/tag.stories.ts index d206b734ca..2e6187acfc 100644 --- a/packages/components/tag/src/tag.stories.ts +++ b/packages/components/tag/src/tag.stories.ts @@ -72,6 +72,13 @@ export const Overflow: Story = { } }; +export const OverflowRemovable: Story = { + args: { + ...Overflow.args, + removable: true + } +}; + export const Removable: Story = { args: { removable: true diff --git a/packages/components/tag/src/tag.ts b/packages/components/tag/src/tag.ts index af43803000..d098c494e2 100644 --- a/packages/components/tag/src/tag.ts +++ b/packages/components/tag/src/tag.ts @@ -65,15 +65,15 @@ export class Tag extends ScopedElementsMixin(LitElement) { */ #observer = new ResizeObserver(() => this.#onResize()); - /** Either an instanceof of Tooltip, or a cleanup function. */ - #tooltip?: Tooltip | (() => void); - /** * Whether the tag component is disabled, when set no interaction is possible. * @default false */ @property({ type: Boolean, reflect: true }) disabled?: boolean; + /** @internal Whether a tooltip should be shown. */ + @state() tooltip?: boolean; + /** @internal The label of the tag component. */ @state() label = ''; @@ -107,20 +107,23 @@ export class Tag extends ScopedElementsMixin(LitElement) { override disconnectedCallback(): void { this.#observer.disconnect(); - if (this.#tooltip instanceof Tooltip) { - this.#tooltip?.remove(); - } else if (this.#tooltip) { - this.#tooltip(); - } - - this.#tooltip = undefined; - super.disconnectedCallback(); } override render(): TemplateResult { + const hasTabindex = !this.disabled && !this.removable && this.tooltip; + return html` - + ${this.tooltip ? html`${this.label}` : nothing} +
+ +
${this.removable ? html` + ${this.navigationDescription + ? html` + ${this.navigationDescription} + ` + : nothing} ` : nothing} `; diff --git a/packages/locales/src/es-ES.ts b/packages/locales/src/es-ES.ts index a2a168b1ac..f8fb4a33b4 100644 --- a/packages/locales/src/es-ES.ts +++ b/packages/locales/src/es-ES.ts @@ -108,6 +108,9 @@ export const templates = { 'sl.select.validation.valueMissing': 'Elige una opción de la lista.', 'sl.tabs.showAll': 'Mostrar todo', 'sl.tag.listOfHiddenElements': 'Lista de elementos ocultados', + 'sl.tag.remove': str`Eliminar etiqueta '${0}'`, + 'sl.tagList.navigationInstructions': + 'Usa las teclas de flecha para moverte entre etiquetas eliminables.', 'sl.timeField.empty': 'Vacío', 'sl.timeField.rangeOverflow': str`Selecciona una hora que no sea posterior a ${0}.`, 'sl.timeField.rangeUnderflow': str`Selecciona una hora que no sea anterior a ${0}.`, @@ -117,6 +120,5 @@ export const templates = { 'sl.timeField.typeMismatch': 'Introduce una hora válida.', 'sl.timeField.valueMissing': 'Introduce una hora.', 'sl.toolBar.showMore': 'Mostrar más', - 'sl.tree.loadingMessage': 'Cargando', - 'sl.tag.remove': str`Eliminar etiqueta '${0}'` + 'sl.tree.loadingMessage': 'Cargando' }; diff --git a/packages/locales/src/es-ES.xlf b/packages/locales/src/es-ES.xlf index 44c18375ef..c56f914ab5 100644 --- a/packages/locales/src/es-ES.xlf +++ b/packages/locales/src/es-ES.xlf @@ -10,10 +10,6 @@ Loading Cargando - - Press the delete or backspace key to remove this item - Pulsa la tecla Supr o Retroceso para eliminar este elemento - Selected Seleccionado @@ -174,6 +170,10 @@ List of hidden elements Lista de elementos ocultados + + Use arrow keys to move between removable tags. + Usa las teclas de flecha para moverte entre etiquetas eliminables. + Selected options Opciones seleccionadas @@ -454,6 +454,10 @@ No later than No más tarde de + + Remove tag '' + Eliminar etiqueta '' + Options Opciones diff --git a/packages/locales/src/it.ts b/packages/locales/src/it.ts index 60d3640dea..e47c3401be 100644 --- a/packages/locales/src/it.ts +++ b/packages/locales/src/it.ts @@ -108,6 +108,8 @@ export const templates = { 'sl.select.validation.valueMissing': "Scegli un'opzione dall'elenco.", 'sl.tabs.showAll': 'Mostra tutto', 'sl.tag.listOfHiddenElements': 'Elenco degli elementi nascosti', + 'sl.tag.remove': str`Rimuovi etichetta '${0}'`, + 'sl.tagList.navigationInstructions': 'Usa i tasti freccia per spostarti tra i tag rimovibili.', 'sl.timeField.empty': 'Vuoto', 'sl.timeField.rangeOverflow': str`Seleziona un orario non posteriore a ${0}.`, 'sl.timeField.rangeUnderflow': str`Seleziona un orario non anteriore a ${0}.`, @@ -117,6 +119,5 @@ export const templates = { 'sl.timeField.typeMismatch': 'Inserisci un orario valido.', 'sl.timeField.valueMissing': 'Inserisci un orario.', 'sl.toolBar.showMore': 'Mostra altro', - 'sl.tree.loadingMessage': 'Caricamento', - 'sl.tag.remove': str`Rimuovi etichetta '${0}'` + 'sl.tree.loadingMessage': 'Caricamento' }; diff --git a/packages/locales/src/it.xlf b/packages/locales/src/it.xlf index aba8f89b36..df0f4a0e99 100644 --- a/packages/locales/src/it.xlf +++ b/packages/locales/src/it.xlf @@ -10,10 +10,6 @@ Loading Caricamento - - Press the delete or backspace key to remove this item - Premi il tasto Canc o Backspace per rimuovere questo elemento - Selected Selezionato @@ -174,6 +170,10 @@ List of hidden elements Elenco degli elementi nascosti + + Use arrow keys to move between removable tags. + Usa i tasti freccia per spostarti tra i tag rimovibili. + Selected options Opzioni selezionate @@ -454,6 +454,10 @@ No later than Non oltre + + Remove tag '' + Rimuovi etichetta '' + Options Opzioni diff --git a/packages/locales/src/nl.ts b/packages/locales/src/nl.ts index f84f7323a9..543c09da0f 100644 --- a/packages/locales/src/nl.ts +++ b/packages/locales/src/nl.ts @@ -109,6 +109,8 @@ export const templates = { 'sl.tabs.showAll': 'Toon alles', 'sl.tag.listOfHiddenElements': 'Lijst met verborgen elementen', 'sl.tag.remove': str`Verwijder tag '${0}'`, + 'sl.tagList.navigationInstructions': + 'Gebruik de pijltjestoetsen om tussen verwijderbare tags te navigeren.', 'sl.timeField.empty': 'Leeg', 'sl.timeField.rangeOverflow': str`Voer een tijd in die niet later is dan ${0}.`, 'sl.timeField.rangeUnderflow': str`Voer een tijd in die niet eerder is dan ${0}.`, diff --git a/packages/locales/src/nl.xlf b/packages/locales/src/nl.xlf index 77a817668a..b1038544c7 100644 --- a/packages/locales/src/nl.xlf +++ b/packages/locales/src/nl.xlf @@ -170,6 +170,10 @@ List of hidden elements Lijst met verborgen elementen + + Use arrow keys to move between removable tags. + Gebruik de pijltjestoetsen om tussen verwijderbare tags te navigeren. + Selected options Geselecteerde opties diff --git a/packages/locales/src/pl.ts b/packages/locales/src/pl.ts index 8d7cc464d1..5c162d555d 100644 --- a/packages/locales/src/pl.ts +++ b/packages/locales/src/pl.ts @@ -108,6 +108,9 @@ export const templates = { 'sl.select.validation.valueMissing': 'Wybierz opcję z listy.', 'sl.tabs.showAll': 'Pokaż wszystkie', 'sl.tag.listOfHiddenElements': 'Lista ukrytych elementów', + 'sl.tag.remove': str`Usuń etykietę '${0}'`, + 'sl.tagList.navigationInstructions': + 'Użyj klawiszy strzałek, aby przechodzić między usuwalnymi tagami.', 'sl.timeField.empty': 'Puste', 'sl.timeField.rangeOverflow': str`Wybierz godzinę nie późniejszą niż ${0}.`, 'sl.timeField.rangeUnderflow': str`Wybierz godzinę nie wcześniejszą niż ${0}.`, @@ -117,6 +120,5 @@ export const templates = { 'sl.timeField.typeMismatch': 'Wprowadź prawidłową godzinę.', 'sl.timeField.valueMissing': 'Wprowadź godzinę.', 'sl.toolBar.showMore': 'Pokaż więcej', - 'sl.tree.loadingMessage': 'Ładowanie', - 'sl.tag.remove': str`Usuń etykietę '${0}'` + 'sl.tree.loadingMessage': 'Ładowanie' }; diff --git a/packages/locales/src/pl.xlf b/packages/locales/src/pl.xlf index 7b71b2c524..4aa6cb6721 100644 --- a/packages/locales/src/pl.xlf +++ b/packages/locales/src/pl.xlf @@ -10,10 +10,6 @@ Loading Ładowanie - - Press the delete or backspace key to remove this item - Naciśnij klawisz Delete lub Backspace, aby usunąć ten element - Selected Wybrane @@ -174,6 +170,10 @@ List of hidden elements Lista ukrytych elementów + + Use arrow keys to move between removable tags. + Użyj klawiszy strzałek, aby przechodzić między usuwalnymi tagami. + Selected options Wybrane opcje @@ -454,6 +454,10 @@ No later than Nie później niż + + Remove tag '' + Usuń etykietę '' + Options Opcje diff --git a/website/src/categories/components/tag/accessibility.md b/website/src/categories/components/tag/accessibility.md index fa15a7b123..234c13e28a 100644 --- a/website/src/categories/components/tag/accessibility.md +++ b/website/src/categories/components/tag/accessibility.md @@ -9,11 +9,10 @@ eleventyNavigation: ## Keyboard interactions -The tag list component uses a roving tabindex. You can focus the first removable tag in the list by pressing the `Tab` key. After that, you can navigate through the removable tags using the left and right arrow keys. You can navigate back to the previous tag with left. The focus indicator loops, so when you are at the last option and press right it will focus on the first tag. +The tag list component uses a roving tabindex for removable tags. You can focus the first remove button in the list by pressing the `Tab` key. After that, you can navigate through the remove buttons using the left and right arrow keys. The focus indicator loops, so when you are at the last option and press right it will focus on the first remove button. In the stacked version of tag-list, when there are hidden tags, you can navigate only through visible removable tags with arrow keys. The first tag indicates how many hidden tags there are. Using a screen reader, it will announce how many hidden tags there are. - -When the tag is focused and is removable, it can be removed by pressing the `Delete` or `Backspace` key. This behavior is also announced by the screen reader. +When a remove button is focused, the tag can be removed by activating the button or by pressing the `Delete` or `Backspace` key. The remove button has an accessible name that includes the tag label, for example "Remove tag 'History'". @@ -29,7 +28,9 @@ When the tag is focused and is removable, it can be removed by pressing the `Del |Attribute|Value|Description| |-|-|-| -|`role`|`'listitem', 'button'`|Identifies the tag element as a `listitem` when it's used inside the `sl-tag-list` - this role is added automatically. Please provide a role `button` when the tag is interactive, is used to perform an action or is removable and not used inside `sl-tag-list`.| +|`role`|`'listitem'`|Identifies the tag element as a `listitem` when it's used inside the `sl-tag-list` - this role is added automatically. Removable tags contain a native remove button.| +|`aria-label`|string|The remove button uses an accessible name that identifies which tag will be removed.| +|`aria-disabled`|boolean|Used on the remove button when a removable tag is disabled. The button remains focusable but cannot remove the tag.| {.ds-table .ds-table-align-top} @@ -44,6 +45,8 @@ When the tag is focused and is removable, it can be removed by pressing the `Del |`aria-label`|string|String that labels the tag list. Use this to label what the tag list indicates, such as the selected options in a combobox.| |`aria-labelledby`|string|Can be used to connect with a single header/element that describes the tag list.| +Do not mix static and removable tags in the same tag list. Use a static tag list when no tags can be removed, and a removable tag list when users can remove tags. + {.ds-table .ds-table-align-top} diff --git a/website/src/categories/components/tag/usage.md b/website/src/categories/components/tag/usage.md index 362c09f3ed..6030eddf91 100644 --- a/website/src/categories/components/tag/usage.md +++ b/website/src/categories/components/tag/usage.md @@ -9,6 +9,22 @@ eleventyNavigation:
Mathematics +History +Science +
+ +
+ + ```html +Mathematics +History +Science + ``` + +
+ +
+Mathematics History Science
@@ -16,7 +32,7 @@ eleventyNavigation:
```html -Mathematics +Mathematics History Science ``` @@ -33,7 +49,10 @@ Use tags to categorize and label course content, enhancing organization, discove ### User-Generated Tags -Enable users to create and manage their own custom labels, allowing for a personalized organization system. In a student dashboard, for example, users might tag their notes or assignments with custom labels like "Exam Prep," "Homework," "Group Project," or "To Review," facilitating better organization. +Enable users to create and manage their own custom labels, allowing for a personalized organization system. In a student dashboard, for example, users might tag their notes or assignments with custom labels like "Exam Prep," "Homework," "Group Project," or "To Review," facilitating better organization. Use removable tags when users can edit or remove those labels. + +### Tag lists +Keep tag lists consistent: use either static tags or removable tags in a single list, but do not mix both types in the same list. A tag can only be disabled when it is also removable. @@ -56,9 +75,9 @@ For tracking the status of tasks or items, such as "In Progress," "Completed," o |Item|Name| Description | Optional| |-|-|-|-| -|1|Container |The container contains the label and close button |no| +|1|Container |The container contains the label and optional close button |no| |2|Label |The label is a brief text that describes the tag |no| -|3|Close button |To remove the tag |yes| +|3|Close button |The only interactive part of a removable tag |yes| {.ds-table .ds-table-align-top} From 0302a07601028b8c3850284a07c7bf658911f0ff Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Fri, 12 Jun 2026 13:29:36 +0200 Subject: [PATCH 07/30] fix(tag-list): improve a11y --- packages/components/tag/src/tag.spec.ts | 14 ++++++++++++++ packages/components/tag/src/tag.ts | 13 ++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/components/tag/src/tag.spec.ts b/packages/components/tag/src/tag.spec.ts index 755d962b77..8c9d451e23 100644 --- a/packages/components/tag/src/tag.spec.ts +++ b/packages/components/tag/src/tag.spec.ts @@ -89,6 +89,13 @@ describe('sl-tag', () => { expect(el).to.match(':state(focus-visible)'); }); + it('should not have the focus-visible state when focus is not focus-visible', async () => { + button.dispatchEvent(new FocusEvent('focus')); + await el.updateComplete; + + expect(el).not.to.match(':state(focus-visible)'); + }); + it('should have a button', () => { expect(button).to.exist; }); @@ -104,6 +111,13 @@ describe('sl-tag', () => { expect(button).to.have.attribute('aria-label', "Remove tag 'My label'"); }); + it('should derive the accessible label from slotted element text', async () => { + el = await fixture(html`My label`); + button = el.renderRoot.querySelector('button')!; + + expect(button).to.have.attribute('aria-label', "Remove tag 'My label'"); + }); + it('should mark the button as aria-disabled when the tag is disabled', async () => { el.disabled = true; await el.updateComplete; diff --git a/packages/components/tag/src/tag.ts b/packages/components/tag/src/tag.ts index 4d117cbb49..f2f0e4220e 100644 --- a/packages/components/tag/src/tag.ts +++ b/packages/components/tag/src/tag.ts @@ -161,8 +161,10 @@ export class Tag extends ScopedElementsMixin(LitElement) { this.#internals.states.delete('focus-visible'); } - #onFocus(): void { - this.#internals.states.add('focus-visible'); + #onFocus(event: FocusEvent): void { + if ((event.target as HTMLElement).matches(':focus-visible')) { + this.#internals.states.add('focus-visible'); + } } #onKeydown(event: KeyboardEvent): void { @@ -193,8 +195,9 @@ export class Tag extends ScopedElementsMixin(LitElement) { #onSlotChange(event: Event & { target: HTMLSlotElement }): void { this.label = event.target .assignedNodes({ flatten: true }) - .filter(node => node.nodeType === Node.TEXT_NODE) - .map(node => node.textContent?.trim()) - .join(''); + .map(node => node.textContent ?? '') + .join('') + .trim() + .replaceAll(/\s+/g, ' '); } } From cf0a41d1c229191f7c1299c676a7364161cc7e2e Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Mon, 15 Jun 2026 17:07:47 +0200 Subject: [PATCH 08/30] fix(tag-list): improve a11y --- website/src/styles/dark-light-mode.scss | 6 +++--- .../ts/components/vertical-tabs/vertical-tabs.ts | 14 +++++--------- website/tests/website_a11y.spec.ts | 5 ++++- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/website/src/styles/dark-light-mode.scss b/website/src/styles/dark-light-mode.scss index 14949de1be..840f048ede 100644 --- a/website/src/styles/dark-light-mode.scss +++ b/website/src/styles/dark-light-mode.scss @@ -122,10 +122,10 @@ --input-menu-background: rgb(var(--ds-color-black-rgb) / 4%); --input-menu-background--hover: rgb(var(--ds-color-black-rgb) / 8%); --input-menu--border: var(--ds-color-light-grey); - --link-color: var(--ds-color-blueberry-light); + --link-color: var(--ds-color-mariner); --link-hover-color: var(--ds-color-mariner); --link-active-color: var(--ds-color-mariner); - --link-visited-color: var(--ds-color-blueberry-light); + --link-visited-color: var(--ds-color-mariner); --logo: url('../assets/logo-black.svg'); --menu-button-hover: rgb(var(--ds-color-blue-rgb) / 4%); --menu-font-color: var(--ds-color-raisin-black); @@ -140,7 +140,7 @@ --table-code-background: var(--ds-color-concrete); --table-separator: var(--ds-color-bright-grey); --table-text-color: var(--ds-color-raisin-black); - --tab-color: rgb(var(--ds-color-black-rgb) / 60%); + --tab-color: rgb(var(--ds-color-black-rgb) / 80%); --tab-color--active: var(--ds-color-black); --tab-color--hover: var(--ds-color-black); --tab-indicator-background: var(--ds-color-blue); diff --git a/website/src/ts/components/vertical-tabs/vertical-tabs.ts b/website/src/ts/components/vertical-tabs/vertical-tabs.ts index 79b692342f..88c4423a3b 100644 --- a/website/src/ts/components/vertical-tabs/vertical-tabs.ts +++ b/website/src/ts/components/vertical-tabs/vertical-tabs.ts @@ -140,22 +140,18 @@ export class VerticalTabs extends LitElement { return html`
- +
@@ -187,11 +183,11 @@ export class VerticalTabs extends LitElement { } #setActiveTab(verticalTab: HTMLElement): void { - const currentVerticalTabLink = this.renderRoot.querySelector('[aria-selected="true"]'), + const currentVerticalTabLink = this.renderRoot.querySelector('[aria-current="true"]'), verticalTabs = this.renderRoot.querySelectorAll('.ds-tab--vertical'); - currentVerticalTabLink?.setAttribute('aria-selected', 'false'); - verticalTab.setAttribute('aria-selected', 'true'); + currentVerticalTabLink?.removeAttribute('aria-current'); + verticalTab.setAttribute('aria-current', 'true'); verticalTabs.forEach(v => v.classList.remove('active')); verticalTab.classList.add('active'); diff --git a/website/tests/website_a11y.spec.ts b/website/tests/website_a11y.spec.ts index e021118b00..f46fa7aaaa 100644 --- a/website/tests/website_a11y.spec.ts +++ b/website/tests/website_a11y.spec.ts @@ -75,7 +75,10 @@ test.describe('Limited to
test on other pages', () => { .forEach(url => { test(`A11y test on ${url}`, async ({ page }) => { await page.goto(url, { waitUntil: 'load' }); - results = await axe.include('main').analyze(); + results = await axe + .include('main') + .exclude('sl-tab-group.ds-tab-group > sl-tab') + .analyze(); expect(results.violations.length, 'Accessibility violations found, see details above').toBe(0); }); }); From 48218ede3c4313a75baa338b7abac3fa5633c2a7 Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Mon, 15 Jun 2026 18:09:41 +0200 Subject: [PATCH 09/30] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .changeset/stupid-cougars-serve.md | 2 +- website/tests/website_a11y.spec.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/stupid-cougars-serve.md b/.changeset/stupid-cougars-serve.md index f3943fc0ca..f8266f99a2 100644 --- a/.changeset/stupid-cougars-serve.md +++ b/.changeset/stupid-cougars-serve.md @@ -2,4 +2,4 @@ '@sl-design-system/locales': patch --- -Replace `sl.tag.removalInstructions` with `sl.tag.remove`, a parameterized string for the remove button label ("Remove tag 'X'"). +Replace `sl.tag.removalInstructions` with `sl.tag.remove` (a parameterized string for the remove button label, e.g. "Remove tag 'X'") and add `sl.tagList.navigationInstructions` for tag-list roving tabindex instructions. diff --git a/website/tests/website_a11y.spec.ts b/website/tests/website_a11y.spec.ts index f46fa7aaaa..137108b056 100644 --- a/website/tests/website_a11y.spec.ts +++ b/website/tests/website_a11y.spec.ts @@ -77,6 +77,7 @@ test.describe('Limited to
test on other pages', () => { await page.goto(url, { waitUntil: 'load' }); results = await axe .include('main') + // Exclude known Axe violation(s) in DS tab group tabs; keep this scoped and remove when fixed. .exclude('sl-tab-group.ds-tab-group > sl-tab') .analyze(); expect(results.violations.length, 'Accessibility violations found, see details above').toBe(0); From 76bb8d7f01ec5679730a02c8d1373d3a31de9a15 Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Mon, 15 Jun 2026 18:10:02 +0200 Subject: [PATCH 10/30] fix(tag-list): improve a11y --- packages/components/tag/src/tag.spec.ts | 26 ++++++++++++++++--- packages/components/tag/src/tag.ts | 17 +++++++++++- .../components/vertical-tabs/vertical-tabs.ts | 4 +-- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/packages/components/tag/src/tag.spec.ts b/packages/components/tag/src/tag.spec.ts index 8c9d451e23..b9df120d9a 100644 --- a/packages/components/tag/src/tag.spec.ts +++ b/packages/components/tag/src/tag.spec.ts @@ -55,9 +55,8 @@ describe('sl-tag', () => { el.focus(); await el.updateComplete; - expect(el.renderRoot.querySelector('[part="label"]')).not.to.have.attribute( - 'aria-describedby' - ); + expect(el.renderRoot.querySelector('[part="label"]')?.hasAttribute('aria-describedby')).to.be + .false; expect(el.renderRoot.querySelector('sl-tooltip')).not.to.exist; }); @@ -196,5 +195,26 @@ describe('sl-tag', () => { expect(tooltip).to.exist; expect(tooltip).to.have.trimmed.text('My label is very long'); }); + + it('should update the tooltip when the label changes without resizing', async () => { + expect(el.renderRoot.querySelector('sl-tooltip')).to.exist; + + const label = el.renderRoot.querySelector('[part="label"]')!; + Object.defineProperties(label, { + clientWidth: { configurable: true, get: () => 100 }, + scrollWidth: { + configurable: true, + get: () => (el.textContent?.trim() === 'A' ? 50 : 150) + } + }); + + el.textContent = 'A'; + await new Promise(resolve => setTimeout(resolve, 0)); + await el.updateComplete; + await el.updateComplete; + + expect(label.hasAttribute('aria-describedby')).to.be.false; + expect(el.renderRoot.querySelector('sl-tooltip')).not.to.exist; + }); }); }); diff --git a/packages/components/tag/src/tag.ts b/packages/components/tag/src/tag.ts index f2f0e4220e..d586ca17bf 100644 --- a/packages/components/tag/src/tag.ts +++ b/packages/components/tag/src/tag.ts @@ -65,6 +65,9 @@ export class Tag extends ScopedElementsMixin(LitElement) { /** Observe changes in size, so we can check whether we need to show tooltips for truncated links. */ #observer = new ResizeObserver(() => this.#onResize()); + /** Observe label text changes that do not trigger a resize or slotchange. */ + #mutationObserver = new MutationObserver(() => this.#updateLabel()); + /** * Whether the tag component is disabled, when set no interaction is possible. * @@ -109,10 +112,12 @@ export class Tag extends ScopedElementsMixin(LitElement) { super.connectedCallback(); this.#observer.observe(this); + this.#mutationObserver.observe(this, { characterData: true, childList: true, subtree: true }); } override disconnectedCallback(): void { this.#observer.disconnect(); + this.#mutationObserver.disconnect(); super.disconnectedCallback(); } @@ -193,11 +198,21 @@ export class Tag extends ScopedElementsMixin(LitElement) { } #onSlotChange(event: Event & { target: HTMLSlotElement }): void { - this.label = event.target + this.#updateLabel(event.target); + } + + #updateLabel(slot = this.renderRoot.querySelector('slot')): void { + if (!slot) { + return; + } + + this.label = slot .assignedNodes({ flatten: true }) .map(node => node.textContent ?? '') .join('') .trim() .replaceAll(/\s+/g, ' '); + + void this.updateComplete.then(() => this.#onResize()); } } diff --git a/website/src/ts/components/vertical-tabs/vertical-tabs.ts b/website/src/ts/components/vertical-tabs/vertical-tabs.ts index 88c4423a3b..9360784a28 100644 --- a/website/src/ts/components/vertical-tabs/vertical-tabs.ts +++ b/website/src/ts/components/vertical-tabs/vertical-tabs.ts @@ -183,11 +183,11 @@ export class VerticalTabs extends LitElement { } #setActiveTab(verticalTab: HTMLElement): void { - const currentVerticalTabLink = this.renderRoot.querySelector('[aria-current="true"]'), + const currentVerticalTabLink = this.renderRoot.querySelector('[aria-current="page"]'), verticalTabs = this.renderRoot.querySelectorAll('.ds-tab--vertical'); currentVerticalTabLink?.removeAttribute('aria-current'); - verticalTab.setAttribute('aria-current', 'true'); + verticalTab.setAttribute('aria-current', 'page'); verticalTabs.forEach(v => v.classList.remove('active')); verticalTab.classList.add('active'); From 7672e8aa8c3eeffb5e649f3befed1aebc95791f9 Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Mon, 15 Jun 2026 18:25:38 +0200 Subject: [PATCH 11/30] fix(tag-list): improve a11y --- packages/components/tag/src/tag.spec.ts | 1 + packages/components/tag/src/tag.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/components/tag/src/tag.spec.ts b/packages/components/tag/src/tag.spec.ts index b9df120d9a..e4507c3a42 100644 --- a/packages/components/tag/src/tag.spec.ts +++ b/packages/components/tag/src/tag.spec.ts @@ -97,6 +97,7 @@ describe('sl-tag', () => { it('should have a button', () => { expect(button).to.exist; + expect(button).to.have.attribute('type', 'button'); }); it('should focus the button when the tag is focused', async () => { diff --git a/packages/components/tag/src/tag.ts b/packages/components/tag/src/tag.ts index d586ca17bf..eb79cce507 100644 --- a/packages/components/tag/src/tag.ts +++ b/packages/components/tag/src/tag.ts @@ -147,7 +147,8 @@ export class Tag extends ScopedElementsMixin(LitElement) { )} aria-disabled=${ifDefined(this.disabled ? 'true' : undefined)} aria-label=${msg(str`Remove tag '${this.label}'`, { id: 'sl.tag.remove' })} - part="button"> + part="button" + type="button"> ${this.navigationDescription From 1c3c367f1e1030c4c5e29bc16ac4b34de9adeb4a Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Mon, 15 Jun 2026 18:56:02 +0200 Subject: [PATCH 12/30] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/components/tag/src/tag-list.ts | 8 +++----- website/src/categories/components/tag/accessibility.md | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/components/tag/src/tag-list.ts b/packages/components/tag/src/tag-list.ts index 06d2efcc8b..5d6c438b01 100644 --- a/packages/components/tag/src/tag-list.ts +++ b/packages/components/tag/src/tag-list.ts @@ -357,11 +357,9 @@ export class TagList extends ScopedElementsMixin(LitElement) { ); this.tags.forEach(tag => { - tag.navigationDescription = tag.removable - ? msg('Use arrow keys to move between removable tags.', { - id: 'sl.tagList.navigationInstructions' - }) - : undefined; + tag.navigationDescription = msg('Use arrow keys to move between removable tags.', { + id: 'sl.tagList.navigationInstructions' + }); tag.role = 'listitem'; tag.size = this.size; tag.variant = this.variant; diff --git a/website/src/categories/components/tag/accessibility.md b/website/src/categories/components/tag/accessibility.md index 234c13e28a..6d1e6f38ab 100644 --- a/website/src/categories/components/tag/accessibility.md +++ b/website/src/categories/components/tag/accessibility.md @@ -45,10 +45,10 @@ When a remove button is focused, the tag can be removed by activating the button |`aria-label`|string|String that labels the tag list. Use this to label what the tag list indicates, such as the selected options in a combobox.| |`aria-labelledby`|string|Can be used to connect with a single header/element that describes the tag list.| -Do not mix static and removable tags in the same tag list. Use a static tag list when no tags can be removed, and a removable tag list when users can remove tags. - {.ds-table .ds-table-align-top} +Do not mix static and removable tags in the same tag list. Use a static tag list when no tags can be removed, and a removable tag list when users can remove tags. +
From f088cb595c56a3e38f5f324f5347b3a58fb2c1fb Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Mon, 15 Jun 2026 19:09:40 +0200 Subject: [PATCH 13/30] fix(tag-list): improve a11y --- packages/components/tag/src/tag.spec.ts | 18 ++++++++++++++++++ packages/components/tag/src/tag.ts | 3 +++ 2 files changed, 21 insertions(+) diff --git a/packages/components/tag/src/tag.spec.ts b/packages/components/tag/src/tag.spec.ts index e4507c3a42..139d27fa0f 100644 --- a/packages/components/tag/src/tag.spec.ts +++ b/packages/components/tag/src/tag.spec.ts @@ -164,6 +164,24 @@ describe('sl-tag', () => { expect(onRemove).to.have.been.calledOnce; }); + it('should prevent backspace and delete key events from leaking', () => { + const onKeydown = spy(); + + el.addEventListener('keydown', onKeydown); + + const event = new KeyboardEvent('keydown', { + bubbles: true, + cancelable: true, + composed: true, + key: 'Backspace' + }); + + button.dispatchEvent(event); + + expect(event.defaultPrevented).to.be.true; + expect(onKeydown).not.to.have.been.called; + }); + it('should emit an sl-remove event when a remove button is clicked', async () => { const onRemove = spy(); diff --git a/packages/components/tag/src/tag.ts b/packages/components/tag/src/tag.ts index eb79cce507..0033b47c09 100644 --- a/packages/components/tag/src/tag.ts +++ b/packages/components/tag/src/tag.ts @@ -175,6 +175,9 @@ export class Tag extends ScopedElementsMixin(LitElement) { #onKeydown(event: KeyboardEvent): void { if (event.key === 'Backspace' || event.key === 'Delete') { + event.preventDefault(); + event.stopPropagation(); + this.#onRemove(event); } } From d33a5138c4fb1b7e9d6082b0c8ccfed5533a75de Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Tue, 16 Jun 2026 11:58:09 +0200 Subject: [PATCH 14/30] fix(tag-list): improve a11y --- packages/components/tag/src/tag-list.spec.ts | 15 +++++++++ packages/components/tag/src/tag-list.ts | 32 ++++++++++---------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/packages/components/tag/src/tag-list.spec.ts b/packages/components/tag/src/tag-list.spec.ts index bfd1b8932c..dd86a930c3 100644 --- a/packages/components/tag/src/tag-list.spec.ts +++ b/packages/components/tag/src/tag-list.spec.ts @@ -112,6 +112,21 @@ describe('sl-tag-list', () => { el.querySelector('sl-tag')?.renderRoot.querySelector('#navigation-description') ).to.have.trimmed.text('Use arrow keys to move between removable tags.'); }); + + it('should resync the navigation description when the list updates', async () => { + const tag = el.querySelector('sl-tag')!; + + tag.navigationDescription = 'Stale navigation description'; + await tag.updateComplete; + + el.requestUpdate(); + await el.updateComplete; + await tag.updateComplete; + + expect(tag.renderRoot.querySelector('#navigation-description')).to.have.trimmed.text( + 'Use arrow keys to move between removable tags.' + ); + }); }); describe('stacked', () => { diff --git a/packages/components/tag/src/tag-list.ts b/packages/components/tag/src/tag-list.ts index 5d6c438b01..a3ed2395d6 100644 --- a/packages/components/tag/src/tag-list.ts +++ b/packages/components/tag/src/tag-list.ts @@ -226,9 +226,7 @@ export class TagList extends ScopedElementsMixin(LitElement) { override updated(changes: PropertyValues): void { super.updated(changes); - if (changes.has('size')) { - this.tags?.forEach(tag => (tag.size = this.size)); - } + this.#syncTags(); if (changes.has('stacked')) { if (this.stacked && this.stack) { @@ -240,10 +238,6 @@ export class TagList extends ScopedElementsMixin(LitElement) { } this.#syncStackObservation(); - - if (changes.has('variant')) { - this.tags?.forEach(tag => (tag.variant = this.variant)); - } } override render(): TemplateResult { @@ -356,15 +350,7 @@ export class TagList extends ScopedElementsMixin(LitElement) { (el): el is Tag => el instanceof Tag ); - this.tags.forEach(tag => { - tag.navigationDescription = msg('Use arrow keys to move between removable tags.', { - id: 'sl.tagList.navigationInstructions' - }); - tag.role = 'listitem'; - tag.size = this.size; - tag.variant = this.variant; - tag.setAttribute('role', 'listitem'); - }); + this.#syncTags(); this.#rovingTabindexController.clearElementCache(); @@ -384,6 +370,20 @@ export class TagList extends ScopedElementsMixin(LitElement) { }); } + #syncTags(): void { + const navigationDescription = msg('Use arrow keys to move between removable tags.', { + id: 'sl.tagList.navigationInstructions' + }); + + this.tags.forEach(tag => { + tag.navigationDescription = navigationDescription; + tag.role = 'listitem'; + tag.size = this.size; + tag.variant = this.variant; + tag.setAttribute('role', 'listitem'); + }); + } + #runVisibilityUpdate(): void { if (this.stack) { const measuredStackInlineSize = this.stack.getBoundingClientRect().width; From 3daeb3e3fc6cd90baed81cb157131d0b2319090d Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Tue, 16 Jun 2026 12:20:58 +0200 Subject: [PATCH 15/30] fix(tag-list): improve a11y --- packages/components/tag/src/tag.scss | 9 +++++++- packages/components/tag/src/tag.spec.ts | 28 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/components/tag/src/tag.scss b/packages/components/tag/src/tag.scss index 259d099f45..49ebe9953b 100644 --- a/packages/components/tag/src/tag.scss +++ b/packages/components/tag/src/tag.scss @@ -43,7 +43,7 @@ color: var(--sl-color-foreground-info-bold); } -:host(:state(focus-visible)) { +:host(:not([removable]):state(focus-visible)) { outline-color: var(--sl-color-border-focused); position: relative; z-index: 1; // Make sure the focus ring is above other elements @@ -101,6 +101,13 @@ button { color: var(--sl-color-foreground-disabled); cursor: default; } + + &:focus-visible { + outline: var(--sl-size-borderWidth-focusRing) solid var(--sl-color-border-focused); + outline-offset: calc(var(--sl-size-borderWidth-focusRing) * -1); + position: relative; + z-index: 1; // Make sure the focus ring is above other elements + } } .sr-only { diff --git a/packages/components/tag/src/tag.spec.ts b/packages/components/tag/src/tag.spec.ts index 139d27fa0f..c7bddeb4ee 100644 --- a/packages/components/tag/src/tag.spec.ts +++ b/packages/components/tag/src/tag.spec.ts @@ -107,6 +107,24 @@ describe('sl-tag', () => { expect(button).to.match(':focus'); }); + it('should focus the remove button when tabbing to the tag', async () => { + const wrapper = await fixture(html` +
+ + My label +
+ `), + before = wrapper.querySelector('button')!, + tag = wrapper.querySelector('sl-tag')!, + removeButton = tag.renderRoot.querySelector('button')!; + + before.focus(); + await userEvent.tab(); + + expect(document.activeElement).to.equal(tag); + expect(tag.shadowRoot?.activeElement).to.equal(removeButton); + }); + it('should have an accessible label on the remove button', () => { expect(button).to.have.attribute('aria-label', "Remove tag 'My label'"); }); @@ -137,6 +155,16 @@ describe('sl-tag', () => { expect(onRemove).not.to.have.been.called; }); + it('should not be removed when the label is clicked', async () => { + const onRemove = spy(el, 'remove'), + label = el.renderRoot.querySelector('[part="label"]')!; + + label.click(); + await el.updateComplete; + + expect(onRemove).not.to.have.been.called; + }); + it('should be removed when the button is clicked using the keyboard', async () => { const onRemove = spy(el, 'remove'); From aeb24388a108cae9873c3fe409885ffeef05dcc2 Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Tue, 16 Jun 2026 12:39:19 +0200 Subject: [PATCH 16/30] fix(tag-list): improve a11y --- packages/components/tag/src/tag.scss | 2 +- packages/components/tag/src/tag.spec.ts | 11 +++++++++++ packages/components/tag/src/tag.ts | 18 ++++++++++++------ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/components/tag/src/tag.scss b/packages/components/tag/src/tag.scss index 49ebe9953b..f8d05e455b 100644 --- a/packages/components/tag/src/tag.scss +++ b/packages/components/tag/src/tag.scss @@ -43,7 +43,7 @@ color: var(--sl-color-foreground-info-bold); } -:host(:not([removable]):state(focus-visible)) { +:host(:not([removable]):where(:focus-visible, :state(focus-visible))) { outline-color: var(--sl-color-border-focused); position: relative; z-index: 1; // Make sure the focus ring is above other elements diff --git a/packages/components/tag/src/tag.spec.ts b/packages/components/tag/src/tag.spec.ts index c7bddeb4ee..947be3a023 100644 --- a/packages/components/tag/src/tag.spec.ts +++ b/packages/components/tag/src/tag.spec.ts @@ -67,6 +67,17 @@ describe('sl-tag', () => { expect(el).not.to.match(':focus'); expect(el).not.to.match(':state(focus-visible)'); }); + + it('should show a focus ring when the host itself is focus-visible', async () => { + const tag = await fixture(html` + My label + `); + + tag.focus({ focusVisible: true } as FocusOptions); + expect(document.activeElement).to.equal(tag); + expect(tag).to.match(':focus-visible'); + expect(getComputedStyle(tag).outlineColor).to.equal('rgb(1, 2, 3)'); + }); }); describe('removable', () => { diff --git a/packages/components/tag/src/tag.ts b/packages/components/tag/src/tag.ts index 0033b47c09..b87fa56d2f 100644 --- a/packages/components/tag/src/tag.ts +++ b/packages/components/tag/src/tag.ts @@ -50,12 +50,6 @@ export class Tag extends ScopedElementsMixin(LitElement) { }; } - /** @internal */ - static override shadowRootOptions: ShadowRootInit = { - ...LitElement.shadowRootOptions, - delegatesFocus: true - }; - /** @internal */ static override styles: CSSResultGroup = styles; @@ -122,6 +116,18 @@ export class Tag extends ScopedElementsMixin(LitElement) { super.disconnectedCallback(); } + override focus(options?: FocusOptions): void { + const focusTarget = this.removable + ? this.renderRoot.querySelector('button') + : this.renderRoot.querySelector('[part="label"][tabindex]'); + + if (focusTarget) { + focusTarget.focus(options); + } else { + super.focus(options); + } + } + override render(): TemplateResult { const hasTabindex = !this.disabled && !this.removable && this.tooltip; From 2fc9d0e6f215b03e15455ebc1679a1c36032bb1a Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Tue, 16 Jun 2026 12:52:07 +0200 Subject: [PATCH 17/30] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .changeset/chatty-badgers-dream.md | 3 +-- packages/components/tag/src/tag-list.ts | 4 ++-- website/src/categories/components/tag/usage.md | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.changeset/chatty-badgers-dream.md b/.changeset/chatty-badgers-dream.md index ceacf093ea..f5deff9571 100644 --- a/.changeset/chatty-badgers-dream.md +++ b/.changeset/chatty-badgers-dream.md @@ -6,6 +6,5 @@ Accessibility improvements to `` and ``: - The remove button now has a proper accessible label ("Remove tag 'X'") instead of being `aria-hidden` - The remove button uses `aria-disabled` instead of `disabled`, keeping it keyboard-reachable when the tag is disabled -- Focus is delegated to the remove button via `delegatesFocus`; `:state(focus-visible)` tracks focus for styling -- Overflow tooltip `aria-describedby` is now on the label part instead of the host +- Focus is delegated to the remove button via the component's `focus()` implementation; `:state(focus-visible)` tracks focus for styling - `` correctly sets `role="listitem"` on each tag diff --git a/packages/components/tag/src/tag-list.ts b/packages/components/tag/src/tag-list.ts index a3ed2395d6..ae2d7d7334 100644 --- a/packages/components/tag/src/tag-list.ts +++ b/packages/components/tag/src/tag-list.ts @@ -138,10 +138,10 @@ export class TagList extends ScopedElementsMixin(LitElement) { #rovingTabindexController = new RovingTabindexController(this, { direction: 'horizontal', focusInIndex: (elements: Tag[]) => { - const index = elements.findIndex(el => !el.disabled); + const index = elements.findIndex(el => el !== this.stackTag || !el.disabled); return index === -1 ? 0 : index; - }, + } elements: () => [ ...(this.stacked && this.stackTag && this.stackTag.style.display !== 'none' ? [this.stackTag] diff --git a/website/src/categories/components/tag/usage.md b/website/src/categories/components/tag/usage.md index 6030eddf91..556bb620b4 100644 --- a/website/src/categories/components/tag/usage.md +++ b/website/src/categories/components/tag/usage.md @@ -52,7 +52,7 @@ Use tags to categorize and label course content, enhancing organization, discove Enable users to create and manage their own custom labels, allowing for a personalized organization system. In a student dashboard, for example, users might tag their notes or assignments with custom labels like "Exam Prep," "Homework," "Group Project," or "To Review," facilitating better organization. Use removable tags when users can edit or remove those labels. ### Tag lists -Keep tag lists consistent: use either static tags or removable tags in a single list, but do not mix both types in the same list. A tag can only be disabled when it is also removable. +Keep tag lists consistent: use either static tags or removable tags in a single list, but do not mix both types in the same list. A tag should only be disabled when it is also removable. From cc02e18e2859ad42af3c74665d30c37c44456222 Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Tue, 16 Jun 2026 12:55:36 +0200 Subject: [PATCH 18/30] fix(tag-list): improve a11y --- packages/components/tag/src/tag-list.spec.ts | 27 ++++++++++++++++++++ packages/components/tag/src/tag-list.ts | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/components/tag/src/tag-list.spec.ts b/packages/components/tag/src/tag-list.spec.ts index dd86a930c3..4f04c5716c 100644 --- a/packages/components/tag/src/tag-list.spec.ts +++ b/packages/components/tag/src/tag-list.spec.ts @@ -223,6 +223,33 @@ describe('sl-tag-list', () => { expect(tabindexes).to.deep.equal([0, -1, -1, -1, -1, -1, -1, -1, -1]); }); + it('should not use a disabled stack tag as the initial tab stop', async () => { + el = await fixture(html` + + My label 1 + My label 2 + My label 3 + My label 4 + My label 5 + My label 6 + My label 7 + My label 8 + + `); + + await new Promise(resolve => setTimeout(resolve, 60)); + + const stackTag = el.renderRoot.querySelector('sl-tag')!, + visibleTag = Array.from(el.querySelectorAll('sl-tag')).find( + tag => getComputedStyle(tag).display !== 'none' + ); + + expect(stackTag).to.have.attribute('disabled'); + expect(stackTag.tabIndex).to.equal(-1); + expect(visibleTag).to.exist; + expect(visibleTag?.tabIndex).to.equal(0); + }); + it('should not have a stack when there is enough space', async () => { // Give the `#breakResizeObserverLoop` time to do its thing await new Promise(resolve => setTimeout(resolve, 60)); diff --git a/packages/components/tag/src/tag-list.ts b/packages/components/tag/src/tag-list.ts index ae2d7d7334..1ae8f18f72 100644 --- a/packages/components/tag/src/tag-list.ts +++ b/packages/components/tag/src/tag-list.ts @@ -141,7 +141,7 @@ export class TagList extends ScopedElementsMixin(LitElement) { const index = elements.findIndex(el => el !== this.stackTag || !el.disabled); return index === -1 ? 0 : index; - } + }, elements: () => [ ...(this.stacked && this.stackTag && this.stackTag.style.display !== 'none' ? [this.stackTag] From e99294d8f712bbfce365f312c4d1c96653e2df6f Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Tue, 16 Jun 2026 13:10:15 +0200 Subject: [PATCH 19/30] fix(tag-list): improve a11y --- packages/components/tag/src/tag-list.spec.ts | 22 +++++++++++++++ packages/components/tag/src/tag-list.ts | 28 ++++++++++++++------ 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/packages/components/tag/src/tag-list.spec.ts b/packages/components/tag/src/tag-list.spec.ts index 4f04c5716c..f4072f5165 100644 --- a/packages/components/tag/src/tag-list.spec.ts +++ b/packages/components/tag/src/tag-list.spec.ts @@ -250,6 +250,28 @@ describe('sl-tag-list', () => { expect(visibleTag?.tabIndex).to.equal(0); }); + it('should not add a disabled stack tag to the tab order when all regular tags are hidden', async () => { + el = await fixture(html` + + My very long label 1 + My very long label 2 + My very long label 3 + + `); + + await new Promise(resolve => setTimeout(resolve, 60)); + + const stackTag = el.renderRoot.querySelector('sl-tag')!, + visibleTags = Array.from(el.querySelectorAll('sl-tag')).filter( + tag => getComputedStyle(tag).display !== 'none' + ); + + expect(stackTag).to.have.attribute('disabled'); + expect(stackTag).to.be.displayed; + expect(stackTag.tabIndex).to.equal(-1); + expect(visibleTags).to.have.length(0); + }); + it('should not have a stack when there is enough space', async () => { // Give the `#breakResizeObserverLoop` time to do its thing await new Promise(resolve => setTimeout(resolve, 60)); diff --git a/packages/components/tag/src/tag-list.ts b/packages/components/tag/src/tag-list.ts index 1ae8f18f72..d7a6dc770a 100644 --- a/packages/components/tag/src/tag-list.ts +++ b/packages/components/tag/src/tag-list.ts @@ -138,17 +138,25 @@ export class TagList extends ScopedElementsMixin(LitElement) { #rovingTabindexController = new RovingTabindexController(this, { direction: 'horizontal', focusInIndex: (elements: Tag[]) => { - const index = elements.findIndex(el => el !== this.stackTag || !el.disabled); + const index = elements.findIndex(el => this.#isFocusableElement(el)); return index === -1 ? 0 : index; }, - elements: () => [ - ...(this.stacked && this.stackTag && this.stackTag.style.display !== 'none' - ? [this.stackTag] - : []), - ...(this.tags ?? []).filter(t => t.style.display !== 'none' && !!t.removable) - ], - isFocusableElement: (el: Tag) => el !== this.stackTag || !el.disabled + elements: () => { + const stackTags = + this.stacked && + this.stackTag && + this.stackTag.style.display !== 'none' && + this.#isFocusableElement(this.stackTag) + ? [this.stackTag] + : []; + + return [ + ...stackTags, + ...(this.tags ?? []).filter(t => t.style.display !== 'none' && !!t.removable) + ]; + }, + isFocusableElement: (el: Tag) => this.#isFocusableElement(el) }); /** Disables interaction with the tag list and renders the stacked tag as disabled. */ @@ -296,6 +304,10 @@ export class TagList extends ScopedElementsMixin(LitElement) { return typeof inlineSize === 'number'; } + #isFocusableElement(el: Tag): boolean { + return el !== this.stackTag || !el.disabled; + } + #getBorderBoxInlineSize(entry: ResizeObserverEntry): number | undefined { const borderBoxSize = (entry as { borderBoxSize?: unknown }).borderBoxSize; From ffdd2041782142485e9490473ccad52ca44ced92 Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Tue, 16 Jun 2026 19:56:04 +0200 Subject: [PATCH 20/30] fix(tag-list): improve a11y --- .changeset/shiny-shrimps-remain.md | 5 ++ .../components/combobox/src/combobox.spec.ts | 51 ++++++++++++++++++- packages/components/combobox/src/combobox.ts | 48 +++++++++++++++-- 3 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 .changeset/shiny-shrimps-remain.md diff --git a/.changeset/shiny-shrimps-remain.md b/.changeset/shiny-shrimps-remain.md new file mode 100644 index 0000000000..92a6d64b21 --- /dev/null +++ b/.changeset/shiny-shrimps-remain.md @@ -0,0 +1,5 @@ +--- +'@sl-design-system/combobox': patch +--- + +Improved keyboard focus behavior for removable selected tags in multi-select comboboxes. Focus now stays within the selected tag list when removing tags one by one and only returns to the input after the last tag is removed. The combobox also avoids showing a separate fake tag focus indicator when focus is on a tag remove button diff --git a/packages/components/combobox/src/combobox.spec.ts b/packages/components/combobox/src/combobox.spec.ts index c3c3ae8103..3c52e30f84 100644 --- a/packages/components/combobox/src/combobox.spec.ts +++ b/packages/components/combobox/src/combobox.spec.ts @@ -1113,14 +1113,14 @@ describe('sl-combobox', () => { expect(tags.every(tag => tag.hasAttribute('aria-hidden'))).to.be.false; }); - it('should set aria-hidden on sl-tag elements when not disabled', async () => { + it('should not set aria-hidden on sl-tag elements when not disabled', async () => { el.disabled = false; await el.updateComplete; const tags = Array.from(el.renderRoot.querySelectorAll('sl-tag')); expect(tags).to.have.lengthOf(2); - expect(tags.every(tag => tag.getAttribute('aria-hidden') === 'true')).to.be.true; + expect(tags.every(tag => tag.hasAttribute('aria-hidden'))).to.be.false; }); }); @@ -1259,6 +1259,18 @@ describe('sl-combobox', () => { expect(removable).to.be.true; }); + it('should not show fake tag focus when navigating remove buttons', async () => { + const tags = Array.from(el.renderRoot.querySelectorAll('sl-tag')), + button = tags[0].renderRoot.querySelector('button'); + + button?.dispatchEvent( + new KeyboardEvent('keydown', { bubbles: true, composed: true, key: 'ArrowRight' }) + ); + await el.updateComplete; + + expect(tags.some(tag => tag.classList.contains('focused'))).to.be.false; + }); + it('should stack options when there is limited space', async () => { vi.useFakeTimers(); @@ -1355,6 +1367,41 @@ describe('sl-combobox', () => { // Verify the tag was removed expect(el.value).to.deep.equal([]); }); + + it('should focus the next tag after removing a tag', async () => { + el.value = ['Option 1', 'Option 2', 'Option 3']; + await el.updateComplete; + + const tags = Array.from(el.renderRoot.querySelectorAll('sl-tag')); + + tags[0].renderRoot.querySelector('button')?.focus(); + await userEvent.keyboard('{Enter}'); + await el.updateComplete; + await waitForNextFrame(); + + const remainingTags = Array.from(el.renderRoot.querySelectorAll('sl-tag')); + + expect(el.value).to.deep.equal(['Option 2', 'Option 3']); + expect((el.renderRoot as ShadowRoot).activeElement).to.equal(remainingTags[0]); + expect(remainingTags[0].shadowRoot?.activeElement).to.equal( + remainingTags[0].renderRoot.querySelector('button') + ); + }); + + it('should focus the input after removing the last tag', async () => { + el.value = ['Option 1']; + await el.updateComplete; + + const tag = el.renderRoot.querySelector('sl-tag')!; + + tag.renderRoot.querySelector('button')?.focus(); + await userEvent.keyboard('{Enter}'); + await el.updateComplete; + await waitForNextFrame(); + + expect(el.value).to.deep.equal([]); + expect(document.activeElement).to.equal(input); + }); }); describe('allow custom values', () => { diff --git a/packages/components/combobox/src/combobox.ts b/packages/components/combobox/src/combobox.ts index fabc96e2fc..b83bade50a 100644 --- a/packages/components/combobox/src/combobox.ts +++ b/packages/components/combobox/src/combobox.ts @@ -35,7 +35,7 @@ import { type SlChangeEvent, type SlFocusEvent } from '@sl-design-system/shared/events.js'; -import { Tag, TagList } from '@sl-design-system/tag'; +import { type SlRemoveEvent, Tag, TagList } from '@sl-design-system/tag'; import { TextField } from '@sl-design-system/text-field'; import { type CSSResultGroup, @@ -504,6 +504,7 @@ export class Combobox extends ObserveAttributesMixin( ${this.multiple && this.selectedItems.length ? html` extends ObserveAttributesMixin( item => item, item => html` this.#onRemove(item)} + @sl-remove=${(event: SlRemoveEvent) => this.#onRemove(item, event)} ?disabled=${this.disabled} ?removable=${!this.disabled} aria-hidden=${this.disabled ? nothing : 'true'} @@ -683,6 +684,10 @@ export class Combobox extends ObserveAttributesMixin( } #onKeydown(event: KeyboardEvent): void { + if (!event.composedPath().includes(this.input)) { + return; + } + const isSelectOnlySpace = !!this.selectOnly && event.key === ' '; if ((event.key === 'Enter' || isSelectOnlySpace) && !this.focusedTag) { @@ -807,7 +812,9 @@ export class Combobox extends ObserveAttributesMixin( this.#pointerDown = false; } - #onRemove(item: ComboboxItem): void { + #onRemove(item: ComboboxItem, event?: SlRemoveEvent): void { + const nextFocusedItem = event ? this.#getNextSelectedTagItem(item) : undefined; + this.#removeSelectedOption(item); this.#updateFilteredOptions(); this.#updateCurrent(); @@ -815,6 +822,41 @@ export class Combobox extends ObserveAttributesMixin( if (this.#popoverJustClosed) { this.wrapper?.showPopover(); } + + if (event) { + void this.updateComplete.then(() => { + requestAnimationFrame(() => this.#focusSelectedTag(nextFocusedItem)); + }); + } + } + + #onTagListFocusIn(): void { + this.focusedTag = undefined; + } + + #getNextSelectedTagItem(item: ComboboxItem): ComboboxItem | undefined { + const tags = Array.from(this.renderRoot.querySelectorAll('sl-tag')), + visibleTagItems = tags + .map((tag, index) => ({ item: this.selectedItems[index], tag })) + .filter(({ item, tag }) => item && tag.removable && tag.style.display !== 'none'), + index = visibleTagItems.findIndex(({ item: tagItem }) => tagItem === item); + + return visibleTagItems[index + 1]?.item ?? visibleTagItems[index - 1]?.item; + } + + #focusSelectedTag(item?: ComboboxItem): void { + const tags = Array.from(this.renderRoot.querySelectorAll('sl-tag')), + tag = item ? tags[this.selectedItems.indexOf(item)] : undefined, + focusTarget = + tag && tag.style.display !== 'none' + ? tag + : tags.find(tag => tag.removable && tag.style.display !== 'none'); + + if (focusTarget) { + focusTarget.focus(); + } else { + this.input.focus(); + } } /** Updates the list of options and the listbox link with the text input. */ From 5bd54515a337f27eb27a9917f3217b0ddc95aec8 Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Tue, 16 Jun 2026 20:09:33 +0200 Subject: [PATCH 21/30] fix(tag-list): improve a11y --- packages/components/combobox/src/combobox.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/combobox/src/combobox.ts b/packages/components/combobox/src/combobox.ts index b83bade50a..2654f11251 100644 --- a/packages/components/combobox/src/combobox.ts +++ b/packages/components/combobox/src/combobox.ts @@ -518,7 +518,6 @@ export class Combobox extends ObserveAttributesMixin( @sl-remove=${(event: SlRemoveEvent) => this.#onRemove(item, event)} ?disabled=${this.disabled} ?removable=${!this.disabled} - aria-hidden=${this.disabled ? nothing : 'true'} class=${this.focusedTag === item ? 'focused' : ''}> ${item.label} From 9a322550fdc78c9f86e3510aac903c35e5667be5 Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Tue, 16 Jun 2026 20:48:53 +0200 Subject: [PATCH 22/30] fix(tag-list): improve a11y --- packages/components/tag/src/tag-list.ts | 1 - .../components/vertical-tabs/vertical-tabs.ts | 23 +++++++++---------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/components/tag/src/tag-list.ts b/packages/components/tag/src/tag-list.ts index d7a6dc770a..202e68b633 100644 --- a/packages/components/tag/src/tag-list.ts +++ b/packages/components/tag/src/tag-list.ts @@ -389,7 +389,6 @@ export class TagList extends ScopedElementsMixin(LitElement) { this.tags.forEach(tag => { tag.navigationDescription = navigationDescription; - tag.role = 'listitem'; tag.size = this.size; tag.variant = this.variant; tag.setAttribute('role', 'listitem'); diff --git a/website/src/ts/components/vertical-tabs/vertical-tabs.ts b/website/src/ts/components/vertical-tabs/vertical-tabs.ts index 9360784a28..cffe555a70 100644 --- a/website/src/ts/components/vertical-tabs/vertical-tabs.ts +++ b/website/src/ts/components/vertical-tabs/vertical-tabs.ts @@ -30,8 +30,6 @@ export class VerticalTabs extends LitElement { /** Used to render vertical links content - tagElement is a source of links text, H2 is the default */ @property() tagElement = 'H2'; - nextUniqueId = 0; - observer = new IntersectionObserver( entries => { let updated = false; @@ -141,16 +139,17 @@ export class VerticalTabs extends LitElement {
From e0cba4544bf6cb7ba442afd2f09a29fd428a376e Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Tue, 16 Jun 2026 21:02:06 +0200 Subject: [PATCH 23/30] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/components/tag/src/tag.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/components/tag/src/tag.ts b/packages/components/tag/src/tag.ts index b87fa56d2f..e625c4c5d3 100644 --- a/packages/components/tag/src/tag.ts +++ b/packages/components/tag/src/tag.ts @@ -147,12 +147,14 @@ export class Tag extends ScopedElementsMixin(LitElement) { @blur=${this.#onBlur} @click=${this.#onRemove} @focus=${this.#onFocus} - @keydown=${this.#onKeydown} aria-describedby=${ifDefined( - this.navigationDescription ? 'navigation-description' : undefined + [ + this.tooltip ? 'tooltip' : undefined, + this.navigationDescription ? 'navigation-description' : undefined + ] + .filter(Boolean) + .join(' ') || undefined )} - aria-disabled=${ifDefined(this.disabled ? 'true' : undefined)} - aria-label=${msg(str`Remove tag '${this.label}'`, { id: 'sl.tag.remove' })} part="button" type="button"> From 3b5c1518375c397b157f3539ceaea3c5f7d5967e Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Tue, 16 Jun 2026 21:16:25 +0200 Subject: [PATCH 24/30] fix(tag-list): improve a11y --- packages/components/tag/src/tag.spec.ts | 13 ++++++++++++- packages/components/tag/src/tag.ts | 20 +++++++++++--------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/components/tag/src/tag.spec.ts b/packages/components/tag/src/tag.spec.ts index 947be3a023..d55705ed8e 100644 --- a/packages/components/tag/src/tag.spec.ts +++ b/packages/components/tag/src/tag.spec.ts @@ -246,14 +246,25 @@ describe('sl-tag', () => { el.focus(); await el.updateComplete; - const label = el.renderRoot.querySelector('[part="label"]')!; + const label = el.renderRoot.querySelector('[part="label"]')!, + button = el.renderRoot.querySelector('button')!; + expect(label).to.have.attribute('aria-describedby'); + expect(button).to.have.attribute('aria-describedby', 'tooltip'); const tooltip = el.renderRoot.querySelector('sl-tooltip'); expect(tooltip).to.exist; expect(tooltip).to.have.trimmed.text('My label is very long'); }); + it('should include both the tooltip and navigation description on the remove button', async () => { + el.navigationDescription = 'Use arrow keys to move between removable tags.'; + await el.updateComplete; + + const button = el.renderRoot.querySelector('button')!; + expect(button).to.have.attribute('aria-describedby', 'tooltip navigation-description'); + }); + it('should update the tooltip when the label changes without resizing', async () => { expect(el.renderRoot.querySelector('sl-tooltip')).to.exist; diff --git a/packages/components/tag/src/tag.ts b/packages/components/tag/src/tag.ts index e625c4c5d3..d90fa76192 100644 --- a/packages/components/tag/src/tag.ts +++ b/packages/components/tag/src/tag.ts @@ -129,7 +129,13 @@ export class Tag extends ScopedElementsMixin(LitElement) { } override render(): TemplateResult { - const hasTabindex = !this.disabled && !this.removable && this.tooltip; + const hasTabindex = !this.disabled && !this.removable && this.tooltip, + buttonDescription = [ + this.tooltip ? 'tooltip' : undefined, + this.navigationDescription ? 'navigation-description' : undefined + ] + .filter(Boolean) + .join(' '); return html` ${this.tooltip ? html`${this.label}` : nothing} @@ -147,14 +153,10 @@ export class Tag extends ScopedElementsMixin(LitElement) { @blur=${this.#onBlur} @click=${this.#onRemove} @focus=${this.#onFocus} - aria-describedby=${ifDefined( - [ - this.tooltip ? 'tooltip' : undefined, - this.navigationDescription ? 'navigation-description' : undefined - ] - .filter(Boolean) - .join(' ') || undefined - )} + @keydown=${this.#onKeydown} + aria-describedby=${ifDefined(buttonDescription || undefined)} + aria-disabled=${ifDefined(this.disabled ? 'true' : undefined)} + aria-label=${msg(str`Remove tag '${this.label}'`, { id: 'sl.tag.remove' })} part="button" type="button"> From 9036dc2124c726827fd5a571cd49a81c8ac06f84 Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Tue, 16 Jun 2026 21:41:53 +0200 Subject: [PATCH 25/30] fix(tag-list): improve a11y --- packages/components/tag/src/tag-list.spec.ts | 34 +++++++++++++++++ packages/components/tag/src/tag.spec.ts | 5 ++- packages/components/tag/src/tag.ts | 40 +++++++++++++++++++- 3 files changed, 77 insertions(+), 2 deletions(-) diff --git a/packages/components/tag/src/tag-list.spec.ts b/packages/components/tag/src/tag-list.spec.ts index f4072f5165..66efc9c53f 100644 --- a/packages/components/tag/src/tag-list.spec.ts +++ b/packages/components/tag/src/tag-list.spec.ts @@ -113,6 +113,40 @@ describe('sl-tag-list', () => { ).to.have.trimmed.text('Use arrow keys to move between removable tags.'); }); + it('should use a single tab stop for removable tag buttons', async () => { + el = await fixture(html` +
+ + + My label 1 + My label 2 + My label 3 + + +
+ `).then(wrapper => wrapper.querySelector('sl-tag-list')!); + + await new Promise(resolve => requestAnimationFrame(() => resolve())); + + const wrapper = el.parentElement!, + before = wrapper.querySelector('button')!, + after = wrapper.querySelector('button:last-child')!, + tags = Array.from(el.querySelectorAll('sl-tag')), + buttons = tags.map(tag => tag.renderRoot.querySelector('button')!); + + expect(buttons.map(button => button.tabIndex)).to.deep.equal([0, -1, -1]); + + before.focus(); + await userEvent.tab(); + + expect(document.activeElement).to.equal(tags[0]); + expect(tags[0].shadowRoot?.activeElement).to.equal(buttons[0]); + + await userEvent.tab(); + + expect(document.activeElement).to.equal(after); + }); + it('should resync the navigation description when the list updates', async () => { const tag = el.querySelector('sl-tag')!; diff --git a/packages/components/tag/src/tag.spec.ts b/packages/components/tag/src/tag.spec.ts index d55705ed8e..4e08b806a5 100644 --- a/packages/components/tag/src/tag.spec.ts +++ b/packages/components/tag/src/tag.spec.ts @@ -75,7 +75,10 @@ describe('sl-tag', () => { tag.focus({ focusVisible: true } as FocusOptions); expect(document.activeElement).to.equal(tag); - expect(tag).to.match(':focus-visible'); + expect(tag.shadowRoot?.activeElement).to.equal( + tag.renderRoot.querySelector('[part="label"]') + ); + expect(tag).to.match(':state(focus-visible)'); expect(getComputedStyle(tag).outlineColor).to.equal('rgb(1, 2, 3)'); }); }); diff --git a/packages/components/tag/src/tag.ts b/packages/components/tag/src/tag.ts index d90fa76192..a3adde5fd2 100644 --- a/packages/components/tag/src/tag.ts +++ b/packages/components/tag/src/tag.ts @@ -53,6 +53,12 @@ export class Tag extends ScopedElementsMixin(LitElement) { /** @internal */ static override styles: CSSResultGroup = styles; + /** @internal */ + static override shadowRootOptions: ShadowRootInit = { + ...LitElement.shadowRootOptions, + delegatesFocus: true + }; + /** @internal */ #internals = this.attachInternals(); @@ -102,6 +108,15 @@ export class Tag extends ScopedElementsMixin(LitElement) { */ @property({ reflect: true }) variant?: TagVariant; + override get tabIndex(): number { + return super.tabIndex; + } + + override set tabIndex(tabIndex: number) { + super.tabIndex = tabIndex; + this.#syncButtonTabIndex(); + } + override connectedCallback(): void { super.connectedCallback(); @@ -128,8 +143,13 @@ export class Tag extends ScopedElementsMixin(LitElement) { } } + protected override updated(): void { + this.#syncButtonTabIndex(); + } + override render(): TemplateResult { - const hasTabindex = !this.disabled && !this.removable && this.tooltip, + const hasTabindex = + !this.disabled && !this.removable && (this.tooltip || this.hasAttribute('tabindex')), buttonDescription = [ this.tooltip ? 'tooltip' : undefined, this.navigationDescription ? 'navigation-description' : undefined @@ -205,6 +225,24 @@ export class Tag extends ScopedElementsMixin(LitElement) { this.remove(); } + #syncButtonTabIndex(): void { + const button = this.renderRoot.querySelector('button'); + + if (!button) { + return; + } + + if (this.navigationDescription) { + if (button.matches(':focus') && this.tabIndex === -1) { + return; + } + + button.tabIndex = this.tabIndex; + } else { + button.removeAttribute('tabindex'); + } + } + #onResize(): void { const label = this.renderRoot.querySelector('[part="label"]'); From 6200ddb842f2263f90736375658d41720aa14b9e Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Tue, 16 Jun 2026 22:59:10 +0200 Subject: [PATCH 26/30] fix(tag-list): improve a11y --- packages/components/tag/src/tag.spec.ts | 6 ++++++ packages/components/tag/src/tag.ts | 12 +++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/components/tag/src/tag.spec.ts b/packages/components/tag/src/tag.spec.ts index 4e08b806a5..dd490543c0 100644 --- a/packages/components/tag/src/tag.spec.ts +++ b/packages/components/tag/src/tag.spec.ts @@ -81,6 +81,12 @@ describe('sl-tag', () => { expect(tag).to.match(':state(focus-visible)'); expect(getComputedStyle(tag).outlineColor).to.equal('rgb(1, 2, 3)'); }); + + it('should respect the host tabindex on the label wrapper', async () => { + const tag = await fixture(html`My label`); + + expect(tag.renderRoot.querySelector('[part="label"]')).to.have.attribute('tabindex', '-1'); + }); }); describe('removable', () => { diff --git a/packages/components/tag/src/tag.ts b/packages/components/tag/src/tag.ts index a3adde5fd2..4cdbef6ba0 100644 --- a/packages/components/tag/src/tag.ts +++ b/packages/components/tag/src/tag.ts @@ -148,8 +148,14 @@ export class Tag extends ScopedElementsMixin(LitElement) { } override render(): TemplateResult { - const hasTabindex = - !this.disabled && !this.removable && (this.tooltip || this.hasAttribute('tabindex')), + const labelTabIndex = + !this.disabled && !this.removable + ? this.hasAttribute('tabindex') + ? this.tabIndex.toString() + : this.tooltip + ? '0' + : undefined + : undefined, buttonDescription = [ this.tooltip ? 'tooltip' : undefined, this.navigationDescription ? 'navigation-description' : undefined @@ -164,7 +170,7 @@ export class Tag extends ScopedElementsMixin(LitElement) { @focus=${this.#onFocus} aria-describedby=${ifDefined(this.tooltip ? 'tooltip' : undefined)} part="label" - tabindex=${ifDefined(hasTabindex ? '0' : undefined)}> + tabindex=${ifDefined(labelTabIndex)}>
${this.removable From f913b4fc55c123aded515df8ed9bf2b0dc8029c0 Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Wed, 17 Jun 2026 06:28:49 +0200 Subject: [PATCH 27/30] fix(tag-list): improve a11y --- packages/components/tag/src/tag-list.spec.ts | 6 ++++++ packages/components/tag/src/tag.ts | 4 ---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/components/tag/src/tag-list.spec.ts b/packages/components/tag/src/tag-list.spec.ts index 66efc9c53f..8c0b01b96c 100644 --- a/packages/components/tag/src/tag-list.spec.ts +++ b/packages/components/tag/src/tag-list.spec.ts @@ -142,6 +142,12 @@ describe('sl-tag-list', () => { expect(document.activeElement).to.equal(tags[0]); expect(tags[0].shadowRoot?.activeElement).to.equal(buttons[0]); + await userEvent.tab({ shift: true }); + + expect(document.activeElement).to.equal(before); + + before.focus(); + await userEvent.tab(); await userEvent.tab(); expect(document.activeElement).to.equal(after); diff --git a/packages/components/tag/src/tag.ts b/packages/components/tag/src/tag.ts index 4cdbef6ba0..8e16e496a2 100644 --- a/packages/components/tag/src/tag.ts +++ b/packages/components/tag/src/tag.ts @@ -239,10 +239,6 @@ export class Tag extends ScopedElementsMixin(LitElement) { } if (this.navigationDescription) { - if (button.matches(':focus') && this.tabIndex === -1) { - return; - } - button.tabIndex = this.tabIndex; } else { button.removeAttribute('tabindex'); From 0a913f5e7a16cef1e57aeca74405eb76a3631f27 Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Wed, 17 Jun 2026 06:39:35 +0200 Subject: [PATCH 28/30] fix(tag-list): improve a11y --- packages/components/tag/src/tag.spec.ts | 7 +++++++ packages/components/tag/src/tag.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/components/tag/src/tag.spec.ts b/packages/components/tag/src/tag.spec.ts index dd490543c0..50d89c730e 100644 --- a/packages/components/tag/src/tag.spec.ts +++ b/packages/components/tag/src/tag.spec.ts @@ -145,6 +145,13 @@ describe('sl-tag', () => { expect(tag.shadowRoot?.activeElement).to.equal(removeButton); }); + it('should respect the host tabindex on the remove button', async () => { + el = await fixture(html`My label`); + button = el.renderRoot.querySelector('button')!; + + expect(button).to.have.attribute('tabindex', '-1'); + }); + it('should have an accessible label on the remove button', () => { expect(button).to.have.attribute('aria-label', "Remove tag 'My label'"); }); diff --git a/packages/components/tag/src/tag.ts b/packages/components/tag/src/tag.ts index 8e16e496a2..66cc818cce 100644 --- a/packages/components/tag/src/tag.ts +++ b/packages/components/tag/src/tag.ts @@ -238,7 +238,7 @@ export class Tag extends ScopedElementsMixin(LitElement) { return; } - if (this.navigationDescription) { + if (this.navigationDescription || this.hasAttribute('tabindex')) { button.tabIndex = this.tabIndex; } else { button.removeAttribute('tabindex'); From f19371c4bb732218fc8c4ec749b3a252bc673857 Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Wed, 17 Jun 2026 06:51:58 +0200 Subject: [PATCH 29/30] fix(tag-list): improve a11y --- packages/components/tag/src/tag-list.spec.ts | 16 +++++++++++++ packages/components/tag/src/tag-list.ts | 25 ++++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/components/tag/src/tag-list.spec.ts b/packages/components/tag/src/tag-list.spec.ts index 8c0b01b96c..c454ac02a7 100644 --- a/packages/components/tag/src/tag-list.spec.ts +++ b/packages/components/tag/src/tag-list.spec.ts @@ -64,6 +64,22 @@ describe('sl-tag-list', () => { expect(el).not.to.have.attribute('stacked'); expect(el.stacked).not.to.be.true; }); + + it('should ignore arrow key navigation when there are no removable tags', async () => { + const tag = el.querySelector('sl-tag')!, + label = tag.renderRoot.querySelector('[part="label"]')!; + + tag.tabIndex = 0; + await tag.updateComplete; + + label.focus(); + + expect(() => + label.dispatchEvent( + new KeyboardEvent('keydown', { bubbles: true, composed: true, key: 'ArrowRight' }) + ) + ).not.to.throw; + }); }); describe('removable', () => { diff --git a/packages/components/tag/src/tag-list.ts b/packages/components/tag/src/tag-list.ts index 202e68b633..35e5c96b1b 100644 --- a/packages/components/tag/src/tag-list.ts +++ b/packages/components/tag/src/tag-list.ts @@ -74,6 +74,9 @@ export class TagList extends ScopedElementsMixin(LitElement) { /** Animation frame used to run an additional initial stabilization pass. */ #initialVisibilityPassFrame?: number; + /** Whether the roving tabindex controller is currently listening for keyboard navigation. */ + #rovingTabindexManaged = true; + /** Number of completed passes before the initial visibility is considered stable. */ #initialVisibilityPasses = 0; @@ -235,6 +238,7 @@ export class TagList extends ScopedElementsMixin(LitElement) { super.updated(changes); this.#syncTags(); + this.#syncRovingTabindexController(); if (changes.has('stacked')) { if (this.stacked && this.stack) { @@ -364,7 +368,7 @@ export class TagList extends ScopedElementsMixin(LitElement) { this.#syncTags(); - this.#rovingTabindexController.clearElementCache(); + this.#clearRovingTabindexCache(); // Resolve the first layout immediately, without timers. if (!this.#hasResolvedInitialVisibility) { @@ -496,7 +500,7 @@ export class TagList extends ScopedElementsMixin(LitElement) { } }); - this.#rovingTabindexController.clearElementCache(); + this.#clearRovingTabindexCache(); // Calculate the stack size based on the visibility of the tags this.stackSize = this.tags.reduce( @@ -514,6 +518,23 @@ export class TagList extends ScopedElementsMixin(LitElement) { } // Now that we updated the visibility of the tags, we need to clear the element cache + this.#clearRovingTabindexCache(); + } + + #clearRovingTabindexCache(): void { this.#rovingTabindexController.clearElementCache(); + this.#syncRovingTabindexController(); + } + + #syncRovingTabindexController(): void { + const hasManagedElements = this.#rovingTabindexController.elements.length > 0; + + if (hasManagedElements && !this.#rovingTabindexManaged) { + this.#rovingTabindexController.manage(); + this.#rovingTabindexManaged = true; + } else if (!hasManagedElements && this.#rovingTabindexManaged) { + this.#rovingTabindexController.unmanage(); + this.#rovingTabindexManaged = false; + } } } From e2e2a0314d8d67bc982e8733ffd9861b43000a32 Mon Sep 17 00:00:00 2001 From: michal-sanoma Date: Wed, 17 Jun 2026 14:42:25 +0200 Subject: [PATCH 30/30] fix(tag-list): improve a11y --- .../components/tag/src/tag-list.stories.ts | 7 ++++++ packages/components/tag/src/tag.scss | 23 ++++++++++++------- packages/components/tag/src/tag.stories.ts | 13 ++++++----- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/components/tag/src/tag-list.stories.ts b/packages/components/tag/src/tag-list.stories.ts index 75ab7f9c65..8a4c80e61c 100644 --- a/packages/components/tag/src/tag-list.stories.ts +++ b/packages/components/tag/src/tag-list.stories.ts @@ -69,6 +69,13 @@ export const Removable: Story = { } }; +export const InfoRemovable: Story = { + args: { + removable: true, + variant: 'info' + } +}; + export const RemovableDisabled: Story = { args: { disabled: true, diff --git a/packages/components/tag/src/tag.scss b/packages/components/tag/src/tag.scss index f8d05e455b..1e928fa200 100644 --- a/packages/components/tag/src/tag.scss +++ b/packages/components/tag/src/tag.scss @@ -1,8 +1,8 @@ :host { --_bg-color: var(--sl-color-background-neutral-subtlest); - --_bg-mix-color: var(--sl-color-background-neutral-interactive-plain); - --_bg-opacity: var(--sl-opacity-interactive-plain-idle); --_br-color: var(--sl-color-border-neutral-plain); + --_button-bg-color: var(--sl-color-background-neutral-subtlest); + --_button-bg-interactive-color: var(--sl-color-background-neutral-interactive-plain); align-items: center; border: var(--sl-size-borderWidth-subtle) solid var(--_br-color); @@ -37,12 +37,17 @@ :host([variant='info']) { --_bg-color: var(--sl-color-background-info-subtlest); - --_bg-mix-color: var(--sl-color-background-info-interactive-plain); --_br-color: var(--sl-color-border-info-subtle); + --_button-bg-color: var(--sl-color-background-info-subtlest); + --_button-bg-interactive-color: var(--sl-color-background-info-interactive-plain); color: var(--sl-color-foreground-info-bold); } +:host([variant='info']) button:not([aria-disabled='true']) { + color: var(--sl-color-foreground-info-bold); +} + :host(:not([removable]):where(:focus-visible, :state(focus-visible))) { outline-color: var(--sl-color-border-focused); position: relative; @@ -53,8 +58,10 @@ background: var(--_bg-color); display: block; flex: 1; + font: var(--sl-text-new-body-md); overflow: hidden; padding: calc(var(--sl-size-025) - var(--sl-size-borderWidth-default)) var(--sl-size-100); + text-align: center; text-overflow: ellipsis; white-space: nowrap; } @@ -63,7 +70,7 @@ button { align-items: center; align-self: stretch; aspect-ratio: 1; - background: var(--_bg-color); + background: var(--_button-bg-color); border: 0; box-sizing: border-box; color: inherit; @@ -83,16 +90,16 @@ button { &:not([aria-disabled='true']):hover { background: color-mix( in srgb, - var(--_bg-color), - var(--_bg-mix-color) calc(100% * var(--sl-opacity-interactive-plain-hover)) + var(--_button-bg-color), + var(--_button-bg-interactive-color) calc(100% * var(--sl-opacity-interactive-plain-hover)) ); } &:not([aria-disabled='true']):active { background: color-mix( in srgb, - var(--_bg-color), - var(--_bg-mix-color) calc(100% * var(--sl-opacity-interactive-plain-active)) + var(--_button-bg-color), + var(--_button-bg-interactive-color) calc(100% * var(--sl-opacity-interactive-plain-active)) ); } diff --git a/packages/components/tag/src/tag.stories.ts b/packages/components/tag/src/tag.stories.ts index bf57da501a..5b00072361 100644 --- a/packages/components/tag/src/tag.stories.ts +++ b/packages/components/tag/src/tag.stories.ts @@ -52,12 +52,6 @@ export default { export const Basic: Story = {}; -export const Disabled: Story = { - args: { - disabled: true - } -}; - export const Info: Story = { args: { variant: 'info' @@ -84,6 +78,13 @@ export const Removable: Story = { } }; +export const InfoRemovable: Story = { + args: { + removable: true, + variant: 'info' + } +}; + export const RemovableDisabled: Story = { args: { disabled: true,