feat(focusgroup): DLT-3285 add v-dt-focusgroup directive for declarative roving tabindex#1187
Conversation
|
Please add either the |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds a new v-dt-focusgroup directive (implementation, constants, export), Storybook docs/stories/examples, unit tests, ESLint rules/docs enforcing role/label, package re-exports and Storybook registration, documentation/data updates for a "Vue Utilities" nav entry, and extends active-link detection to include /functions-and-utilities/ in docs nav logic. Changes
Sequence DiagramsequenceDiagram
participant User as User
participant Dir as v-dt-focusgroup<br/>(Directive)
participant DOM as DOM<br/>Elements
participant App as Vue App
User->>Dir: Key press or focus
activate Dir
Dir->>Dir: parseConfig() / resolveSelector() / resolveSkipDisabled()
Dir->>DOM: querySelectorAll -> filter hidden / skipped / disabled
DOM-->>Dir: items[]
Dir->>Dir: compute next target (axis, loop, RTL)
Dir->>DOM: update roving tabindex attributes
Dir->>DOM: focus(target)
DOM->>App: dispatch dt-focusgroup-move(detail: item,index,prev)
deactivate Dir
App-->>User: consumer handles selection/activation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsTimed out fetching pipeline failures after 30000ms 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
packages/dialtone-vue/directives/focusgroup_directive/focusgroup.js (1)
199-214: RTL detection edge case (optional).Line 202's
|| el.closest('[dir="rtl"]')could yield incorrect results if the container has explicitdir="ltr"but sits inside an RTL ancestor.getComputedStyle(el).directionalone is authoritative since it reflects inheritance correctly. The closest check appears redundant.Low-impact since nested opposing
dirattributes are rare.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/dialtone-vue/directives/focusgroup_directive/focusgroup.js` around lines 199 - 214, The RTL detection is using getComputedStyle(el).direction combined with el.closest('[dir="rtl"]'), which can be wrong when the element has an explicit dir="ltr" but is inside an RTL ancestor; change the isRTL assignment in attach to rely solely on getComputedStyle(el).direction === 'rtl' (remove the el.closest('[dir="rtl"]') fallback) and update the state object (state.isRTL) initialization to use that single authoritative value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/eslint-plugin-dialtone/lib/rules/focusgroup-requires-label.js`:
- Around line 35-38: The predicate that currently only accepts literal
accessible-name attributes (it checks !attr.directive) should be expanded to
also accept Vue bound attributes; update the condition in
focusgroup-requires-label.js so it returns true for either non-directive
attributes matching attr.key.name === 'aria-label' or 'aria-labelledby' OR for
directive/bound attributes where the attribute identifier lives on
attr.key.name.name (check attr.directive && attr.key && attr.key.name &&
(attr.key.name === 'aria-label' || attr.key.name === 'aria-labelledby') or
attr.key.name.name depending on AST shape), and add RuleTester valid cases
covering templates with :aria-label and :aria-labelledby to prevent false
positives.
- Around line 27-29: The call to
sourceCode.parserServices.defineTemplateBodyVisitor in the rule assumes
parserServices exists; add a guard that checks for sourceCode.parserServices and
sourceCode.parserServices.defineTemplateBodyVisitor before calling it (e.g., if
missing, return an empty visitor object). Update the visitor creation in the
top-level of focusgroup-requires-label (the code using sourceCode and
defineTemplateBodyVisitor and the VAttribute visitor) to early-return {} when
parserServices/defineTemplateBodyVisitor is falsy so the rule no longer throws
when vue-eslint-parser is not configured.
In `@packages/eslint-plugin-dialtone/lib/rules/focusgroup-requires-role.js`:
- Around line 34-36: The predicate computing hasRole in
focusgroup-requires-role.js currently ignores directive attributes (attr =>
!attr.directive && attr.key.name === 'role'), which misses bound roles like
:role/v-bind:role; update the predicate used for hasRole to treat either a plain
attribute with key.name === 'role' OR a directive whose argument identifies the
role (e.g., attr.directive && attr.key.argument && attr.key.argument.name ===
'role'), so bound role attributes are counted as present; after updating the
hasRole logic, add a RuleTester valid case for a template using :role="someRole"
(or v-bind:role) to prevent regressions.
---
Nitpick comments:
In `@packages/dialtone-vue/directives/focusgroup_directive/focusgroup.js`:
- Around line 199-214: The RTL detection is using getComputedStyle(el).direction
combined with el.closest('[dir="rtl"]'), which can be wrong when the element has
an explicit dir="ltr" but is inside an RTL ancestor; change the isRTL assignment
in attach to rely solely on getComputedStyle(el).direction === 'rtl' (remove the
el.closest('[dir="rtl"]') fallback) and update the state object (state.isRTL)
initialization to use that single authoritative value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Central YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro
Run ID: e16b61d6-df89-43c6-9772-177e5882a8a9
📒 Files selected for processing (25)
apps/dialtone-documentation/docs/.vuepress/theme/components/Navbar.vueapps/dialtone-documentation/docs/.vuepress/theme/components/Page.vueapps/dialtone-documentation/docs/.vuepress/theme/composables/useSidebarItems.jsapps/dialtone-documentation/docs/.vuepress/theme/layouts/Layout.vueapps/dialtone-documentation/docs/.vuepress/views/UiKitsOverview.vueapps/dialtone-documentation/docs/_data/site-nav.jsonapps/dialtone-documentation/docs/_data/vue-utilities.jsonapps/dialtone-documentation/docs/functions-and-utilities/index.mdapps/dialtone-documentation/docs/scratch.mdpackages/dialtone-vue/.storybook/preview.jsxpackages/dialtone-vue/directives/focusgroup_directive/focusgroup.jspackages/dialtone-vue/directives/focusgroup_directive/focusgroup.mdxpackages/dialtone-vue/directives/focusgroup_directive/focusgroup.stories.jspackages/dialtone-vue/directives/focusgroup_directive/focusgroup.test.jspackages/dialtone-vue/directives/focusgroup_directive/focusgroup_constants.jspackages/dialtone-vue/directives/focusgroup_directive/focusgroup_directive_events.story.vuepackages/dialtone-vue/directives/focusgroup_directive/focusgroup_directive_recipes.story.vuepackages/dialtone-vue/directives/focusgroup_directive/index.jspackages/dialtone-vue/directives/mode_directive/mode_directive_default.story.vuepackages/dialtone-vue/directives/scrollbar_directive/scrollbar_directive_default.story.vuepackages/dialtone-vue/index.jspackages/eslint-plugin-dialtone/lib/rules/focusgroup-requires-label.jspackages/eslint-plugin-dialtone/lib/rules/focusgroup-requires-role.jspackages/eslint-plugin-dialtone/tests/lib/rules/focusgroup-requires-label.jspackages/eslint-plugin-dialtone/tests/lib/rules/focusgroup-requires-role.js
…isabledSkipped story
…tainer and add unknown token warning
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/dialtone-vue/directives/focusgroup_directive/focusgroup.js`:
- Line 204: The RTL detection currently uses el.closest('[dir="rtl"]') which can
be overridden by nearer dir attributes; replace that logic so isRTL is
determined solely from getComputedStyle(el).direction === 'rtl' (remove the
el.closest check) inside the focusgroup directive where isRTL is computed, and
update any related references to rely on that computed direction. Also add a
unit/integration test that renders a parent with dir="rtl" and a nested
focusgroup element with dir="ltr" and assert horizontal navigation behaves as
LTR (i.e., left/right are not inverted) to prevent regressions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Central YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro
Run ID: dcdd7fe1-6861-42d8-a2b0-d441ab6a9271
📒 Files selected for processing (4)
packages/dialtone-vue/directives/focusgroup_directive/focusgroup.jspackages/dialtone-vue/directives/focusgroup_directive/focusgroup.mdxpackages/dialtone-vue/directives/focusgroup_directive/focusgroup.stories.jspackages/dialtone-vue/directives/focusgroup_directive/focusgroup_constants.js
✅ Files skipped from review due to trivial changes (1)
- packages/dialtone-vue/directives/focusgroup_directive/focusgroup.mdx
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/dialtone-vue/directives/focusgroup_directive/focusgroup_constants.js
…nabled item on blur
There was a problem hiding this comment.
♻️ Duplicate comments (1)
packages/dialtone-vue/directives/focusgroup_directive/focusgroup.js (1)
209-209:⚠️ Potential issue | 🟠 MajorUse computed direction only for RTL detection
Line 209 incorrectly combines computed direction with
closest('[dir="rtl"]'). In nesteddiroverrides (e.g., RTL parent, LTR focusgroup), this inverts horizontal navigation incorrectly. Use only computed style direction.Suggested fix
- const isRTL = getComputedStyle(el).direction === 'rtl' || el.closest('[dir="rtl"]') !== null; + const isRTL = getComputedStyle(el).direction === 'rtl';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/dialtone-vue/directives/focusgroup_directive/focusgroup.js` at line 209, The RTL detection incorrectly combines computed style with a DOM ancestor check; update the isRTL assignment in focusgroup.js to rely solely on the computed style for the element (use getComputedStyle(el).direction === 'rtl') and remove the el.closest('[dir="rtl"]') part so nested dir overrides (e.g., an LTR focusgroup inside an RTL parent) don't invert navigation; locate and change the isRTL declaration where it’s currently set to const isRTL = getComputedStyle(el).direction === 'rtl' || el.closest('[dir="rtl"]') !== null.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@packages/dialtone-vue/directives/focusgroup_directive/focusgroup.js`:
- Line 209: The RTL detection incorrectly combines computed style with a DOM
ancestor check; update the isRTL assignment in focusgroup.js to rely solely on
the computed style for the element (use getComputedStyle(el).direction ===
'rtl') and remove the el.closest('[dir="rtl"]') part so nested dir overrides
(e.g., an LTR focusgroup inside an RTL parent) don't invert navigation; locate
and change the isRTL declaration where it’s currently set to const isRTL =
getComputedStyle(el).direction === 'rtl' || el.closest('[dir="rtl"]') !== null.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Central YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro
Run ID: 44fc47ce-5b41-4811-9d8c-3b6a52cb7604
📒 Files selected for processing (9)
packages/dialtone-vue/directives/focusgroup_directive/focusgroup.jspackages/dialtone-vue/directives/focusgroup_directive/focusgroup.test.jspackages/dialtone-vue/directives/focusgroup_directive/focusgroup_directive_events.story.vuepackages/eslint-plugin-dialtone/docs/rules/focusgroup-requires-label.mdpackages/eslint-plugin-dialtone/docs/rules/focusgroup-requires-role.mdpackages/eslint-plugin-dialtone/lib/rules/focusgroup-requires-label.jspackages/eslint-plugin-dialtone/lib/rules/focusgroup-requires-role.jspackages/eslint-plugin-dialtone/tests/lib/rules/focusgroup-requires-label.jspackages/eslint-plugin-dialtone/tests/lib/rules/focusgroup-requires-role.js
✅ Files skipped from review due to trivial changes (4)
- packages/eslint-plugin-dialtone/docs/rules/focusgroup-requires-role.md
- packages/eslint-plugin-dialtone/docs/rules/focusgroup-requires-label.md
- packages/eslint-plugin-dialtone/tests/lib/rules/focusgroup-requires-label.js
- packages/eslint-plugin-dialtone/tests/lib/rules/focusgroup-requires-role.js
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/eslint-plugin-dialtone/lib/rules/focusgroup-requires-role.js
- packages/eslint-plugin-dialtone/lib/rules/focusgroup-requires-label.js
- packages/dialtone-vue/directives/focusgroup_directive/focusgroup.test.js
…r to check dir attribute value
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bda3cb6fdc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "Codex (@codex) review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".
Brad Paugh (braddialpad)
left a comment
There was a problem hiding this comment.
This PR looks pretty good IMO. I have one question, what is the behaviour of multi nested v-dt-focusgroup is this something that should be acknowledged, managed or handled? I believe at the moment if you have multiple nested groups and you press left arrow for example, this event will trigger in all of them. Expected behaviour?
| function parseObjectConfig (config, value) { | ||
| for (const key of CONFIG_KEYS) { | ||
| if (value[key] !== undefined) config[key] = value[key]; | ||
| } | ||
| } | ||
|
|
||
| function parseStringConfig (config, value) { | ||
| const tokens = value.split(/\s+/); | ||
| for (const token of tokens) { | ||
| const mapping = TOKEN_MAP[token]; | ||
| if (mapping) { | ||
| config[mapping.key] = mapping.value; | ||
| } else if (token && process.env.NODE_ENV !== 'production') { | ||
| // eslint-disable-next-line no-console | ||
| console.warn( | ||
| `[DtFocusgroupDirective] Unknown token "${token}". ` + | ||
| `Valid tokens: ${Object.keys(TOKEN_MAP).join(', ')}.`, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export function parseConfig (value) { | ||
| const config = { ...FOCUSGROUP_DEFAULTS }; | ||
|
|
||
| if (!value || value === true) return config; | ||
|
|
||
| if (typeof value === 'object') { | ||
| parseObjectConfig(config, value); | ||
| } else if (typeof value === 'string') { | ||
| parseStringConfig(config, value); | ||
| } | ||
|
|
||
| return config; | ||
| } | ||
|
|
||
| /** | ||
| * Shallow comparison of two parsed config objects. | ||
| * Used by the updated hook to avoid teardown/reattach when config is semantically identical. | ||
| */ | ||
| export function configsEqual (a, b) { | ||
| return CONFIG_KEYS.every(key => a[key] === b[key]); | ||
| } | ||
|
|
||
| /** | ||
| * Resolve the final item selector for a focusgroup container. | ||
| * | ||
| * Priority: explicit config.selector > role-aware default > fallback (all focusable). | ||
| * | ||
| * @param {HTMLElement} el - The focusgroup container element | ||
| * @param {{ selector: string|null }} config - Parsed focusgroup config | ||
| * @returns {string} CSS selector string | ||
| */ | ||
| export function resolveSelector (el, config) { | ||
| if (config.selector) return config.selector; | ||
|
|
||
| const role = el.getAttribute('role'); | ||
| if (role && ROLE_DEFAULTS_MAP[role]) { | ||
| return ROLE_DEFAULTS_MAP[role].selector; | ||
| } | ||
|
|
||
| return FOCUSABLE_SELECTOR; | ||
| } | ||
|
|
||
| /** | ||
| * Resolve whether disabled items should be skipped during navigation. | ||
| * | ||
| * Priority: explicit config.skipDisabled > role-aware default > true. | ||
| * | ||
| * @param {HTMLElement} el - The focusgroup container element | ||
| * @param {{ skipDisabled: boolean|null }} config - Parsed focusgroup config | ||
| * @returns {boolean} | ||
| */ | ||
| export function resolveSkipDisabled (el, config) { | ||
| if (config.skipDisabled !== null) return config.skipDisabled; | ||
|
|
||
| const role = el.getAttribute('role'); | ||
| if (role && ROLE_DEFAULTS_MAP[role]) { | ||
| return ROLE_DEFAULTS_MAP[role].skipDisabled; | ||
| } | ||
|
|
||
| return true; | ||
| } |
There was a problem hiding this comment.
These are functions, not constants. Should probably just be moved to a different file to avoid confusion.
There was a problem hiding this comment.
extracted into focusgroup_utils.js
Nesting is not recommended, though technicaly possible. Each Distinct axes is effectively the workaround if nested is truly needed. The treeview example I did demonstrates this: directive owns vertical (Up/Down) by default, and I created custom script to manage left/right for DtCollapsible's expand/collapse with 2D grid navigation is the proper answer for most "I want to nest" cases. When someone thinks they need nested focusgroups, it's probably a grid where Up/Down and Left/Right mean different things (rows vs columns). 2D Grid could be a future token extension (e.g., I've added language in the doc to discourage nested use. |
|
I threw in a blog post to announce this directive: https://dialtone.dialpad.com/deploy-previews/pr-1187/dialtone/whats-new/ |
|
✔️ Deploy previews ready! |
Brad Paugh (braddialpad)
left a comment
There was a problem hiding this comment.
Thanks for this, it's really cool. Approving with one additional question.
Does it also make sense to use this component as a "focus trap" component where we want to trap the focus within a certain area, but don't necessarily want to use arrow keyboard navigation? ex/ what we do in popover/modal
Nah. This directive makes the container a single What does sound very reasonable: a Dialtone And to that end, we should consider combing through our products to discover what is done over and over, what should we make an equivalent available... functions, utils, composables, etc... |
# [3.220.0-next.1](dialtone-vue/v3.219.0...dialtone-vue/v3.220.0-next.1) (2026-04-15) ### Bug Fixes * **Editor:** NO-JIRA remove extra blank line in editor.mdx ([18c47ef](18c47ef)) * **Split Button, Filter Pill:** DLT-3260 DLT-3261 correct misalignment at size 100 ([#1176](#1176)) ([fe2b777](fe2b777)) * **Tabs:** DLT-3251 improve accessibility ([#1171](#1171)) ([01b101e](01b101e)) ### Code Refactoring * **Combobox Multi Select:** NO-JIRA use logical css properties ([#1104](#1104)) ([a675daf](a675daf)) * **Components,recipes:** DLT-3043 DLT-3046 logical start/end naming with deprecated backward-compatible fallbacks ([#1079](#1079)) ([1d26a4e](1d26a4e)) * **Components:** DLT-3100 remove rootClass references ([#1184](#1184)) ([6531783](6531783)) * **Components:** dlt-3282 renamed show prop to open ([#1190](#1190)) ([5509d0f](5509d0f)) * **Components:** dlt-3283 refactor prop/slots to be consistent ([#1196](#1196)) ([2d22c76](2d22c76)) * **Modal:** DLT-3262 migrate DtModal to native dialog element ([#1179](#1179)) ([589edb5](589edb5)) * **Text:** DLT-3302 tone prop uses css variables instead of css utilities ([#1197](#1197)) ([75df16f](75df16f)) * **Tokens,css,link,text,breadcrumbs,input Group,split Button:** tokens, CSS foundation, deprecations, tooling DLT-3011 DLT-2961 DLT-3068 DLT-3069 DLT-3070 DLT-3071 DLT-3072 ([#1092](#1092)) ([7783090](7783090)) * **Tokens:** DLT-3013 convert color system from HSL to OKLCH ([#1060](#1060)) ([42dc418](42dc418)) ### Documentation * **Dialtone Vue:** DLT-3226 update documentation and consuming code to numeric size scale ([#1158](#1158)) ([149c763](149c763)) * **Playground:** DLT-3119 refine playground UI and integrate across doc site ([#1127](#1127)) ([ba6b599](ba6b599)) ### Features * **Avatar:** DLT-2942 updated avatar component ([#1047](#1047)) ([e50563d](e50563d)) * **Avatar:** DLT-3161 change from clickable to "interactive" ([#1182](#1182)) ([2d00fe0](2d00fe0)) * **Banner, Notice, Toast, Breadcrumbs, Checkbox, Radio, Input, Select Menu, Empty State:** visual refresh, dt-text integration, form api parity DLT-2944 DLT-2945 DLT-2949 DLT-2959 DLT-2960 DLT-2957 ([#1091](#1091)) ([16e908a](16e908a)) * **Button,tabs,split Button:** visual refresh and slot updates DLT-2946 DLT-2947 DLT-2965 ([#1090](#1090)) ([ffdd56f](ffdd56f)) * **Components:** DLT-3157 align critical/positive prop values and CSS class names ([#1177](#1177)) ([edd98b2](edd98b2)) * **Components:** DLT-3159 enforce positive boolean props ([#1198](#1198)) ([ac73bdc](ac73bdc)) * **Dialtone Css, Dialtone Tokens, Dialtone Vue:** DLT-3197 DLT-3198 DLT-3199 DLT-3200 DLT-3201 DLT-3202 DLT-3203 DLT-3204 DLT-3205 DLT-3206 DLT-3207 DLT-3208 DLT-3209 DLT-3210 DLT-3211 token-stop utility classes, migration tooling, and documentation ([#1150](#1150)) ([6237b84](6237b84)) * **Dialtone Vue:** DLT-3225 migrate component size props to numeric ordinal scale ([#1157](#1157)) ([725bd41](725bd41)) * **Filter Pill:** DLT-2941 DtFilterPill improvements ([#1110](#1110)) ([23c5689](23c5689)) * **Focusgroup:** DLT-3285 add v-dt-focusgroup directive for declarative roving tabindex ([#1187](#1187)) ([e1ea074](e1ea074)) * **Focustrap:** DLT-3297 add v-dt-focustrap directive for declarative focus trapping ([#1195](#1195)) ([6ef9a21](6ef9a21)) * **Link,button:** DLT-3012 add underline prop ([#1059](#1059)) ([1cb91ea](1cb91ea)) * **Mode Directive, Mode Island:** DLT-3067 DLT-3078 DLT-3077 add `v-dt-mode` directive ([#1109](#1109)) ([f97efca](f97efca)) * **Segmented Control:** DLT-413 new component ([#1144](#1144)) ([ad4c6c3](ad4c6c3)) * **Split Button:** DLT-3257 add show-divider prop ([#1173](#1173)) ([5bef411](5bef411)) * **Tabs:** DLT-3114 add vertical orientation variant ([#1120](#1120)) ([3290a23](3290a23)) * **Tabs:** DLT-3162 add spread prop for horizontal distribution ([#1135](#1135)) ([6e7cd04](6e7cd04)) * **Text:** export text component from dialtone-vue ([757fdb7](757fdb7)) * **Tokens:** DLT-2937 introduce spacing and layout token systems ([#1045](#1045)) ([de23e12](de23e12)) * **Tokens:** update color ramps and consumer color migration tools DLT-3004 DLT-3005 DLT-3006 DLT-3007 DLT-3008 ([#1054](#1054)) ([28cb1de](28cb1de))
# [1.12.0-next.3](eslint-plugin-dialtone/v1.12.0-next.2...eslint-plugin-dialtone/v1.12.0-next.3) (2026-04-15) ### Features * **Focusgroup:** DLT-3285 add v-dt-focusgroup directive for declarative roving tabindex ([#1187](#1187)) ([e1ea074](e1ea074))
# [9.180.0-next.1](dialtone/v9.179.0...dialtone/v9.180.0-next.1) (2026-04-15) ### Bug Fixes * **Doc:** NO-JIRA correct font size utility json ([#1015](#1015)) ([76cf0fa](76cf0fa)) * **Editor:** NO-JIRA remove extra blank line in editor.mdx ([18c47ef](18c47ef)) * possible merge issue in sidebarItem.vue ([5e2e91b](5e2e91b)) * **Split Button, Filter Pill:** DLT-3260 DLT-3261 correct misalignment at size 100 ([#1176](#1176)) ([fe2b777](fe2b777)) * **Stylelint:** disable hue-degree-notation rule and update OKLCH color syntax ([81f886e](81f886e)) * **Stylelint:** disable hue-degree-notation rule and update OKLCH color syntax ([8721f43](8721f43)) * **Tabs:** DLT-3251 improve accessibility ([#1171](#1171)) ([01b101e](01b101e)) * **Tokens:** DLT-3053 preserve root font size variable as reference ([#1093](#1093)) ([112ab4b](112ab4b)) * **Tokens:** handle multi-layer box shadows correctly in postcss plugin ([feea7f1](feea7f1)) * **Tokens:** NO-JIRA output line-height as unitless instead of percentages ([#1040](#1040)) ([33d8505](33d8505)) * **Tokens:** wrap css math expressions with calc() in layered build ([#966](#966)) ([1635f61](1635f61)) * update @dialpad/i18n dependencies to latest versions ([7a4b777](7a4b777)) ### Code Refactoring * **Combobox Multi Select:** NO-JIRA use logical css properties ([#1104](#1104)) ([a675daf](a675daf)) * **Components,recipes:** DLT-3043 DLT-3046 logical start/end naming with deprecated backward-compatible fallbacks ([#1079](#1079)) ([1d26a4e](1d26a4e)) * **Components:** DLT-3100 remove rootClass references ([#1184](#1184)) ([6531783](6531783)) * **Components:** dlt-3282 renamed show prop to open ([#1190](#1190)) ([5509d0f](5509d0f)) * **Components:** dlt-3283 refactor prop/slots to be consistent ([#1196](#1196)) ([2d22c76](2d22c76)) * **Css:** DLT-1876 css logical properties ([#872](#872)) ([1acd6c9](1acd6c9)) * **Modal:** DLT-3262 migrate DtModal to native dialog element ([#1179](#1179)) ([589edb5](589edb5)) * **Notice,banner,toast:** DLT-3296 decouple CSS and fix specificity violations ([#1194](#1194)) ([9579ac0](9579ac0)) * **Style:** DLT-3014 wrap all styles in css cascade layers ([#1061](#1061)) ([f8b3fed](f8b3fed)) * **Text:** DLT-2883 rename headline sizes from xxl => 2xl, etc ([#1029](#1029)) ([0ac86f3](0ac86f3)) * **Text:** DLT-3302 tone prop uses css variables instead of css utilities ([#1197](#1197)) ([75df16f](75df16f)) * **Tokens,css,link,text,breadcrumbs,input Group,split Button:** tokens, CSS foundation, deprecations, tooling DLT-3011 DLT-2961 DLT-3068 DLT-3069 DLT-3070 DLT-3071 DLT-3072 ([#1092](#1092)) ([7783090](7783090)) * **Tokens:** DLT-2884 deprecate space tokens in favor of size tokens ([#1013](#1013)) ([63f6d2f](63f6d2f)) * **Tokens:** DLT-3013 convert color system from HSL to OKLCH ([#1060](#1060)) ([42dc418](42dc418)) ### Documentation * **Components:** DLT-3178 DLT-3182 unify code examples into single code-example component ([#1147](#1147)) ([d0b6e14](d0b6e14)) * **Css Utilities:** DLT-3295 DLT-3280 mark physical utilities as deprecated ([#1193](#1193)) ([73f1479](73f1479)) * **Dialtone Vue:** DLT-3226 update documentation and consuming code to numeric size scale ([#1158](#1158)) ([149c763](149c763)) * DLT-1510 DLT-1511 DLT-1512 site redesign and ia ([#968](#968)) ([6ea7eb9](6ea7eb9)) * DLT-2908 ui kits landing page ([#1049](#1049)) ([d805b9d](d805b9d)) * DLT-3243 fenced code blocks for examples and snippets ([#1166](#1166)) ([cb169df](cb169df)) * DLT-3250 display component status badges in sidebar and page headers ([#1170](#1170)) ([7d5f5d5](7d5f5d5)) * DLT-3252 add GFM alert syntax for notices ([#1172](#1172)) ([1697911](1697911)) * DLT-3258 DLT-3259 improve sidebar search with logical keywords and fuzzy matching ([#1174](#1174)) ([2bf4639](2bf4639)) * NO-JIRA fix focus left-nav items on keypress down up ([#1106](#1106)) ([882c9b9](882c9b9)) * **Playground:** DLT-3119 refine playground UI and integrate across doc site ([#1127](#1127)) ([ba6b599](ba6b599)) * **Playground:** DLT-3242 segmented control for compact enum props ([#1165](#1165)) ([fa440d8](fa440d8)) * **Utilities:** DLT-3183 convert to vue demo fenced block ([#1167](#1167)) ([522197f](522197f)) ### Features * **Avatar:** DLT-2942 updated avatar component ([#1047](#1047)) ([e50563d](e50563d)) * **Avatar:** DLT-3161 change from clickable to "interactive" ([#1182](#1182)) ([2d00fe0](2d00fe0)) * **Banner, Notice, Toast, Breadcrumbs, Checkbox, Radio, Input, Select Menu, Empty State:** visual refresh, dt-text integration, form api parity DLT-2944 DLT-2945 DLT-2949 DLT-2959 DLT-2960 DLT-2957 ([#1091](#1091)) ([16e908a](16e908a)) * **Button,tabs,split Button:** visual refresh and slot updates DLT-2946 DLT-2947 DLT-2965 ([#1090](#1090)) ([ffdd56f](ffdd56f)) * **Components:** DLT-3157 align critical/positive prop values and CSS class names ([#1177](#1177)) ([edd98b2](edd98b2)) * **Components:** DLT-3159 enforce positive boolean props ([#1198](#1198)) ([ac73bdc](ac73bdc)) * **Css Utility:** DLT-2715 add default color for border css utilities ([#874](#874)) ([71310ab](71310ab)) * **Dialtone Css, Dialtone Tokens, Dialtone Vue:** DLT-3197 DLT-3198 DLT-3199 DLT-3200 DLT-3201 DLT-3202 DLT-3203 DLT-3204 DLT-3205 DLT-3206 DLT-3207 DLT-3208 DLT-3209 DLT-3210 DLT-3211 token-stop utility classes, migration tooling, and documentation ([#1150](#1150)) ([6237b84](6237b84)) * **Dialtone Vue:** DLT-3225 migrate component size props to numeric ordinal scale ([#1157](#1157)) ([725bd41](725bd41)) * **Eslint Plugin Dialtone:** DLT-3047 physical-to-logical naming migration tooling ([#1163](#1163)) ([d94dc4b](d94dc4b)) * **Eslint Plugin Dialtone:** DLT-3227 DLT-3228 add deprecated-tshirt-sizes ESLint rule and migration codemod ([#1159](#1159)) ([21a011f](21a011f)) * **Filter Pill:** DLT-2704 create component ([#473](#473)) ([6267445](6267445)) * **Filter Pill:** DLT-2941 DtFilterPill improvements ([#1110](#1110)) ([23c5689](23c5689)) * **Focusgroup:** DLT-3285 add v-dt-focusgroup directive for declarative roving tabindex ([#1187](#1187)) ([e1ea074](e1ea074)) * **Focustrap:** DLT-3297 add v-dt-focustrap directive for declarative focus trapping ([#1195](#1195)) ([6ef9a21](6ef9a21)) * **Link,button:** DLT-3012 add underline prop ([#1059](#1059)) ([1cb91ea](1cb91ea)) * **Mode Directive, Mode Island:** DLT-3067 DLT-3078 DLT-3077 add `v-dt-mode` directive ([#1109](#1109)) ([f97efca](f97efca)) * **Segmented Control:** DLT-413 new component ([#1144](#1144)) ([ad4c6c3](ad4c6c3)) * **Split Button:** DLT-3257 add show-divider prop ([#1173](#1173)) ([5bef411](5bef411)) * **Style:** DLT-2976 update underline thickness and offset ([#1058](#1058)) ([1e56947](1e56947)) * **Tabs:** DLT-3114 add vertical orientation variant ([#1120](#1120)) ([3290a23](3290a23)) * **Tabs:** DLT-3162 add spread prop for horizontal distribution ([#1135](#1135)) ([6e7cd04](6e7cd04)) * **Text:** export text component from dialtone-vue ([757fdb7](757fdb7)) * **Text:** new vue component DLT-2864 DLT-2863 DLT-2862 ([#995](#995)) ([196ef8a](196ef8a)) * **Tokens:** DLT-1977 add high contrast design tokens and theme ([#938](#938)) ([a1c0457](a1c0457)) * **Tokens:** DLT-2767 exploratory shell themes ([#909](#909)) ([3afcf63](3afcf63)) * **Tokens:** DLT-2781 create color-assistive themes ([#921](#921)) ([a6cc397](a6cc397)) * **Tokens:** DLT-2802 DLT-2563 layered theming system and mode island component ([#945](#945)) ([5c72c6c](5c72c6c)) * **Tokens:** DLT-2937 introduce spacing and layout token systems ([#1045](#1045)) ([de23e12](de23e12)) * **Tokens:** update color ramps and consumer color migration tools DLT-3004 DLT-3005 DLT-3006 DLT-3007 DLT-3008 ([#1054](#1054)) ([28cb1de](28cb1de)) * **Typography, Tokens:** DLT-2856 DLT-2857 next typography system ([#994](#994)) ([d744e97](d744e97))
🛠️ Type Of Change
📖 Jira Ticket
DLT-3285
DLT-3042
📖 Description
New Vue directive
v-dt-focusgroupthat declaratively adds roving tabindex to any composite widget container. Arrow-key cycling, Home/End, looping, focus memory, disabled-item skipping, RTL support — all without writing keyboard event handlers.Token syntax
Object syntax for advanced
Role-aware defaults infer the item selector and disabled behavior from the container's
roleattribute (tablist→[role="tab"],listbox→[role="option"], etc.).Also includes:
focusgroup-requires-role,focusgroup-requires-label) as accessibility guardrails/functions-and-utilities/) listing all consumer-facing directives, functions, and utilities — driven by_data/vue-utilities.json<dt-icon name="storybook-color">Does NOT:
keyboard_list_navigationmixin (different pattern — activedescendant)📦 Cross-Package Impact
dialtone-vueeslint-plugin-dialtonedialtone-documentation💡 Context
Roving tabindex is reimplemented from scratch in 6+ Dialtone components with inconsistent feature support. Consumers building custom widgets have no primitive for this. A loose version was built in Beacon and proved the need. This directive gives every team a confident, one-line way to add keyboard cycling to toolbars, tablists, menus, contact lists, and data tables.
Benefits all users — not just accessibility or power users. Arrow-key navigation is standardized, faster, and more predictable than Tab for navigating grouped controls.
Aligned with the Open UI Scoped Focusgroup proposal so the mental model transfers when the native HTML attribute ships.
📝 Checklist
For all PRs:
For all Vue changes:
🔮 Next Steps
useFocusGroup) for DtTabGroup/DtSegmentedControl internal adoption🔗 Sources
📷 Screenshots / GIFs
vue-utilities.mp4
focus-group-stories.mp4
Adds v-dt-focusgroup Vue directive for declarative roving tabindex (arrow/Home/End, looping, memory, RTL, disabled-item skipping, role-aware defaults), two ESLint accessibility rules with tests, Storybook stories/MDX docs and recipes, Vue Utilities docs/site-nav updates, and package exports/Storybook registration.