Skip to content

Improve tag-list accessibility#3214

Open
jpzwarte wants to merge 6 commits into
mainfrom
fix/2868-tag-list-a11y
Open

Improve tag-list accessibility#3214
jpzwarte wants to merge 6 commits into
mainfrom
fix/2868-tag-list-a11y

Conversation

@jpzwarte
Copy link
Copy Markdown
Member

@jpzwarte jpzwarte commented Apr 14, 2026

What changed

This PR improves the accessibility of the <sl-tag> component's remove button by reworking how focus, keyboard interaction, and disabled state are handled.

Closes #2868

<sl-tag> component

  • delegatesFocus: true: Added to shadow root options so that focusing the host element automatically delegates focus to the inner elements. This removes the need to manually manage tabindex on the host.
  • Custom state for focus-visible: Replaced the CSS :host(:focus-visible) selector with :host(:state(focus-visible)) backed by ElementInternals. Focus/blur handlers on both the label wrapper and the button toggle the focus-visible state, giving more precise control over when the focus ring is shown.
  • Label slot wrapped in <div part="label">: The slot is now wrapped in a <div> that acts as the label part. It receives tabindex="0" only when a tooltip is shown and the tag is not disabled/removable. Focus/blur events bubble up from this element to drive the custom focus-visible state.
  • Button is always rendered when removable: Previously the button was hidden when disabled. Now it is always rendered and aria-disabled="true" is set instead of ?disabled. This ensures the button remains discoverable to assistive technologies even when disabled.
  • Accessible button label: Replaced aria-hidden="true" on the button with a localized aria-label ("Remove tag '<label>'") so screen readers announce the action correctly.
  • Keyboard handling moved to button: @keydown is now handled on the button itself (Backspace/Delete to remove) instead of relying on EventsController on the host.
  • Tooltip reworked: The lazy Tooltip instance approach is replaced with a reactive @state() tooltip boolean. When overflow is detected via ResizeObserver, the tooltip is rendered declaratively in the template and referenced via aria-describedby.
  • Removed manual tabindex and aria-description management: The updated() lifecycle override was removed. tabindex and aria-description are no longer set imperatively on the host element.
  • removable reflects to attribute: The property now uses reflect: true so CSS can target :host([removable]) reliably.
  • Added @csspart label and @csspart button JSDoc documentation.
  • Removed pointer-events: none from the disabled CSS rule so the remove button is still clickable (but guarded by aria-disabled logic).
  • Added outline: 0 to the button to suppress the native focus ring (the host's custom focus-visible state handles it).
  • Updated SCSS selectors from slot to [part='label'] to target the new <div> wrapper.

<sl-tag-list> component

  • Sets tag.role = 'listitem' directly (in addition to setAttribute) for consistency.
  • Added @customElement sl-tag-list JSDoc tag.

Locales

  • Removed sl.tag.removalInstructions (no longer used).
  • Added sl.tag.remove with a template string for the button's aria-label.

Stories

  • Added an OverflowRemovable story to test overflow behavior on removable tags.
  • Added a RemovableDisabled story to show the disabled + removable state.
  • Replaced styleMap with ifDefined for the inline style on the tag story.

Copilot AI review requested due to automatic review settings April 14, 2026 10:25
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 14, 2026

🦋 Changeset detected

Latest commit: b6ae79f

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

💥 An error occurred when fetching the changed packages and changesets in this PR
Some errors occurred when validating the changesets config:
The package "@sl-design-system/grid" depends on the ignored package "@sl-design-system/example-data", but "@sl-design-system/grid" is not being ignored. Please add "@sl-design-system/grid" to the `ignore` option.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to improve accessibility for the sl-tag component (particularly when tags are removable) by shifting removal interaction to an explicit, labeled remove button and updating related localization, styles, stories, and tests.

Changes:

  • Replaced the previous “press Backspace/Delete” ARIA instruction with an explicit remove button aria-label (localized) and updated NL locale resources accordingly.
  • Updated sl-tag focus handling and styling to support a host-level focus outline via custom states and delegatesFocus.
  • Adjusted Storybook stories and component tests to reflect the new removable/disabled behaviors.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/locales/src/nl.xlf Removes old removal-instructions string; adds new sl.tag.remove translation.
packages/locales/src/nl.ts Removes old key and adds a new remove-label entry (currently mismatched vs XLF/component id).
packages/components/tag/src/tag.ts Implements accessible remove button labeling and new focus handling; removes old ARIA description logic.
packages/components/tag/src/tag.stories.ts Simplifies max-width styling and adds a removable+disabled story.
packages/components/tag/src/tag.spec.ts Updates/extends tests for reflectable removable, focus delegation, and remove button labeling/disabled behavior.
packages/components/tag/src/tag.scss Switches focus styling to :state(focus-visible) and tweaks button outline/disabled interaction behavior.

Comment thread packages/locales/src/nl.ts Outdated
Comment thread packages/components/tag/src/tag.scss
Comment thread packages/components/tag/src/tag.ts Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 14, 2026

🕸 Website preview

You can view a preview here (commit b6ae79f745014f407ae511c308a54b396c395b88).

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 14, 2026

🕸 Storybook preview

You can view a preview here (commit b6ae79f745014f407ae511c308a54b396c395b88).

Copilot AI review requested due to automatic review settings April 21, 2026 12:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

packages/components/tag/src/tag-list.ts:346

  • tag.role = 'listitem' and tag.setAttribute('role', 'listitem') are redundant because the IDL role property reflects to the role attribute. Keeping only one avoids duplication and makes it clearer where the role is set.
      tag.role = 'listitem';
      tag.size = this.size;
      tag.variant = this.variant;
      tag.setAttribute('role', 'listitem');
    });

Comment thread packages/components/tag/src/tag.spec.ts Outdated
Comment thread packages/components/tag/src/tag.ts
jpzwarte and others added 2 commits April 21, 2026 14:46
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 21, 2026 12:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Comment thread packages/components/tag/src/tag.ts
Comment thread packages/components/tag/src/tag.ts
@jpzwarte jpzwarte marked this pull request as ready for review April 21, 2026 13:12
Copilot AI review requested due to automatic review settings April 21, 2026 13:12
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

packages/components/tag/src/tag.ts:184

  • Tooltip visibility is only recalculated from the ResizeObserver callback. If the slotted label text changes (slotchange) without affecting the host element's size, ResizeObserver may not fire and this.tooltip can become stale. Consider recalculating overflow (or scheduling #onResize()) after updating this.label in #onSlotChange.
  #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('');
  }

Comment thread packages/components/tag/src/tag.ts
Comment thread packages/components/tag/src/tag.ts
Copy link
Copy Markdown
Collaborator

@michal-sanoma michal-sanoma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Small a11y/UX concern: because #onFocus() always adds focus-visible, the host behaves like :focus instead of native :focus-visible. Would it make sense to track input source and apply the ring only for keyboard interaction?

Copy link
Copy Markdown
Contributor

@a11ymiko a11ymiko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Stacked variants keyboard user cannot set focus on tag with tooltip. sl-tag has tabindex=0 but keyboard focus cannot be place on this tag.

Copy link
Copy Markdown
Contributor

@a11ymiko a11ymiko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Stacked Removable variant user cannot place focus on any tag. This happens on Safari, Chrome and Firefox on macOS and Edge and Chrome on Win11. On Firefox on Win11 user can place focus on some removable tag, but it's not the first one in the list.

Screen.Recording.2026-04-22.at.08.48.35.mov

Copy link
Copy Markdown
Contributor

@a11ymiko a11ymiko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor issue but on Chrome on macOS tag with Overflow has it's content announced twice (once from tag text and once from tooltip text). Not adding 'aria-describedby' to div in Overflow sl-tag will fix that issue in Chrome without making an issue for other OSes and browsers.

Screen.Recording.2026-04-22.at.09.38.10.mov

Copy link
Copy Markdown
Contributor

@a11ymiko a11ymiko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Stacked tag lists the first tag (with number of stacked items) doesn't have role=listitem. It should have role=listitem because it's inside sl-tag-list role="list".

<source>No later than <x id="0" equiv-text="${format(this.max, this.locale, this.#helperTextFormatOptions)}"/></source>
<target>Uiterlijk <x id="0" equiv-text="${format(this.max, this.locale, this.#helperTextFormatOptions)}"/></target>
</trans-unit>
<trans-unit id="sl.tag.remove">
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we not add it to spanish and polish as well?

<trans-unit id="sl.tag.removalInstructions">
<source>Press the delete or backspace key to remove this item</source>
<target>Druk op de delete- of backspacetoets om dit item te verwijderen</target>
</trans-unit>
Copy link
Copy Markdown
Collaborator

@Diaan Diaan May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we keep this in as deprecated? If people update the locales but don't update the tag list component it will break the translation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Tag] Tags with interactions should have button roles and native button interactions

5 participants