Skip to content

feat: add label step option for the dialog's accessible name - #3482

Open
chuckcarpenter wants to merge 1 commit into
mainfrom
fix/2390-step-aria-label
Open

feat: add label step option for the dialog's accessible name#3482
chuckcarpenter wants to merge 1 commit into
mainfrom
fix/2390-step-aria-label

Conversation

@chuckcarpenter

@chuckcarpenter chuckcarpenter commented Aug 13, 2026

Copy link
Copy Markdown
Member

A step with no title gets no naming attribute at all on its <dialog>, so
the dialog has no accessible name and screen readers announce it without one.
Steps whose content is text-only had no way to fix that short of adding a
visible title and changing the UI.

Add an optional label step option, emitted as aria-label on the dialog
only when the step has no title. It accepts a string or a function
returning one, matching title and the existing label options on buttons
and the cancel icon. The function is invoked with the step as this, and
only when its result will actually be used, so a label that throws (an i18n
catalog that has not loaded yet, say) cannot break a titled step.

When both title and label are given, title wins: aria-labelledby
outranks aria-label in the accessible name computation, so emitting both
would leave label silently dead, and it keeps the accessible name matching
the visible heading (WCAG 2.5.3). The title path is untouched and its
output is byte-identical.

The resolved value is gated on its trimmed length before the attribute is
set, so label: '', label: () => '' and whitespace-only values omit
aria-label rather than writing a name that assistive technology treats as
empty. The untrimmed string is what gets written. Non-string values are
ignored rather than coerced, so a stray plain-JS label cannot produce an
[object Object] accessible name.

Note on the original report: #2390 pastes a <div role="dialog">, which is
pre-v15 markup. v15 renders a native <dialog> and sets no role attribute
anywhere, so the specific axe rule cited in that report no longer selects this
element. The underlying complaint is still real and is what this fixes — an
unnamed dialog is an unnamed dialog regardless of which linter notices.

Every existing step is unaffected: applyAttrs skips null, so the DOM for a
step without label is byte-identical. The one behavior change is for a
plain-JS consumer who was already passing an inert label key; on a title-less
step that key now becomes an accessible name.

Fixes #2390

Verification

  • 236 unit tests (69 new in shepherd-element.spec.js), Cypress a11y green, lint / prettier / types:check clean.
  • Mutation-tested, including that the label function is invoked only when its result is used, and that the trimmed-length gate rejects '', () => '', and whitespace-only values.

Worth flagging for review: the axe rule quoted in #2390 (aria-dialog-name) targets [role="dialog"], and v15 sets no role attribute anywhere — git grep role -- shepherd.js/src returns nothing. That markup came from the pre-v15 Svelte <div>. The underlying problem is still real (a native <dialog> with no accessible name), but the specific rule cited in the report no longer selects this element, so this is feat: rather than an a11y bug fix.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added an optional label for Shepherd steps without visible titles, improving dialog accessibility.
    • Labels may be provided as text or a function and preserve surrounding whitespace.
    • Visible titles take precedence over labels; empty or whitespace-only labels are omitted.
  • Documentation
    • Documented label usage, precedence, and accessibility behavior.
  • Tests
    • Added coverage for string and function labels, title handling, and invalid or empty values.

A step with no `title` gets no naming attribute at all on its `<dialog>`, so
the dialog has no accessible name and screen readers announce it without one.
Steps whose content is text-only had no way to fix that short of adding a
visible `title` and changing the UI.

Add an optional `label` step option, emitted as `aria-label` on the dialog
only when the step has no `title`. It accepts a string or a function
returning one, matching `title` and the existing `label` options on buttons
and the cancel icon. The function is invoked with the step as `this`, and
only when its result will actually be used, so a `label` that throws (an i18n
catalog that has not loaded yet, say) cannot break a titled step.

When both `title` and `label` are given, `title` wins: `aria-labelledby`
outranks `aria-label` in the accessible name computation, so emitting both
would leave `label` silently dead, and it keeps the accessible name matching
the visible heading (WCAG 2.5.3). The `title` path is untouched and its
output is byte-identical.

The resolved value is gated on its trimmed length before the attribute is
set, so `label: ''`, `label: () => ''` and whitespace-only values omit
`aria-label` rather than writing a name that assistive technology treats as
empty. The untrimmed string is what gets written. Non-string values are
ignored rather than coerced, so a stray plain-JS `label` cannot produce an
`[object Object]` accessible name.

Note on the original report: #2390 pastes a `<div role="dialog">`, which is
pre-v15 markup. v15 renders a native `<dialog>` and sets no `role` attribute
anywhere, so the specific axe rule cited in that report no longer selects this
element. The underlying complaint is still real and is what this fixes — an
unnamed dialog is an unnamed dialog regardless of which linter notices.

Every existing step is unaffected: `applyAttrs` skips null, so the DOM for a
step without `label` is byte-identical. The one behavior change is for a
plain-JS consumer who was already passing an inert `label` key; on a title-less
step that key now becomes an accessible name.

Fixes #2390

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
shepherd-docs Ready Ready Preview Aug 13, 2026 1:58pm
shepherd-landing Ready Ready Preview Aug 13, 2026 1:58pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2a0198c1-9aea-415f-92fd-b0353f570930

📥 Commits

Reviewing files that changed from the base of the PR and between 1d9c664 and 9f8dbf7.

📒 Files selected for processing (6)
  • docs-src/src/content/docs/guides/usage.md
  • shepherd.js/src/components/shepherd-element.ts
  • shepherd.js/src/step.ts
  • shepherd.js/test/cypress/integration/a11y.cy.js
  • shepherd.js/test/unit/components/shepherd-element.spec.js
  • shepherd.js/test/unit/step.spec.js

📝 Walkthrough

Walkthrough

The Step API now supports accessible labels for dialogs without visible titles. Labels may be strings or functions, are overridden by titles, and are omitted when empty or whitespace-only. Rendering, unit tests, Cypress tests, and usage documentation cover the behavior.

Changes

Accessible Step Labels

Layer / File(s) Summary
Label contract and dialog rendering
shepherd.js/src/step.ts, shepherd.js/src/components/shepherd-element.ts, docs-src/src/content/docs/guides/usage.md
Adds StepOptions.label, resolves string and function values, applies valid values as aria-label, and documents title precedence and empty-value handling.
Label behavior validation
shepherd.js/test/unit/components/shepherd-element.spec.js, shepherd.js/test/unit/step.spec.js, shepherd.js/test/cypress/integration/a11y.cy.js
Tests direct and inherited labels, function context and errors, invalid values, whitespace preservation, title precedence, and rendered dialog attributes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Mergeability Score: ⚪ Minimal · up to 9f8db

This localized change adds an optional accessible name for title-less dialogs while preserving existing title behavior; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant StepOptions
  participant ShepherdElement
  participant DialogDOM
  StepOptions->>ShepherdElement: provide label or label function
  ShepherdElement->>ShepherdElement: resolve label when title is absent
  ShepherdElement->>DialogDOM: set valid aria-label
Loading

Suggested labels: enhancement

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the new label step option for dialog accessible names.
Linked Issues check ✅ Passed The changes implement issue #2390 by naming title-less dialogs with aria-label while preserving title precedence and existing behavior.
Out of Scope Changes check ✅ Passed The code, documentation, types, and tests directly support the linked accessibility objective without unrelated changes.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/2390-step-aria-label

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

shepherd.js/test/cypress/integration/a11y.cy.js

(node:2) ESLintIgnoreWarning: The ".eslintignore" file is no longer supported. Switch to using the "ignores" property in "eslint.config.js": https://eslint.org/docs/latest/use/configure/migration-guide#ignore-files
(Use node --trace-warnings ... to show where the warning was created)

Oops! Something went wrong! :(

ESLint: 10.8.1

A config object is using the "root" key, which is not supported in flat config system.

Flat configs always act as if they are the root config file, so this key can be safely removed.

shepherd.js/test/unit/components/shepherd-element.spec.js

(node:2) ESLintIgnoreWarning: The ".eslintignore" file is no longer supported. Switch to using the "ignores" property in "eslint.config.js": https://eslint.org/docs/latest/use/configure/migration-guide#ignore-files
(Use node --trace-warnings ... to show where the warning was created)

Oops! Something went wrong! :(

ESLint: 10.8.1

A config object is using the "root" key, which is not supported in flat config system.

Flat configs always act as if they are the root config file, so this key can be safely removed.

shepherd.js/test/unit/step.spec.js

(node:2) ESLintIgnoreWarning: The ".eslintignore" file is no longer supported. Switch to using the "ignores" property in "eslint.config.js": https://eslint.org/docs/latest/use/configure/migration-guide#ignore-files
(Use node --trace-warnings ... to show where the warning was created)

Oops! Something went wrong! :(

ESLint: 10.8.1

A config object is using the "root" key, which is not supported in flat config system.

Flat configs always act as if they are the root config file, so this key can be safely removed.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qltysh

qltysh Bot commented Aug 13, 2026

Copy link
Copy Markdown

Qlty


Coverage Impact

This PR will not change total coverage.

Modified Files with Diff Coverage (1)

RatingFile% DiffUncovered Line #s
Coverage rating: A Coverage rating: A
shepherd.js/src/components/shepherd-element.ts100.0%
Total100.0%
🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Aria-labels on .shepherd-elements of steps without titles

1 participant