-

Test header

- -
-
+
+
+
+
+
+
+
+
Options
+
+
+
+ From ccf0eb2351fafddb7cca602391b19033e4cc540c Mon Sep 17 00:00:00 2001 From: pharret31 Date: Tue, 5 May 2026 07:49:34 +0200 Subject: [PATCH 03/11] feat(toolbar): AC-1.3 mouse vs keyboard sync - Track mousedown on toolbar container via _lastFocusFromMouse flag - In _handleFocusIn: if mouse click on TextBox, set _insideActiveItem=true (immediate edit mode without pressing Enter) - Always reset _insideActiveItem when any toolbar item is focused via keyboard - Clicking other items (BG, buttons, SelectBox) resets _insideActiveItem=false Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../js/__internal/ui/toolbar/toolbar.ts | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/toolbar/toolbar.ts b/packages/devextreme/js/__internal/ui/toolbar/toolbar.ts index 82bdf319cea6..18b8bb606159 100644 --- a/packages/devextreme/js/__internal/ui/toolbar/toolbar.ts +++ b/packages/devextreme/js/__internal/ui/toolbar/toolbar.ts @@ -32,8 +32,14 @@ class Toolbar extends ToolbarBase { // Arrow keys pass through to the widget; Esc returns to toolbar navigation. _insideActiveItem = false; + // True when the most recent focus change inside the toolbar was triggered by a mouse click. + // Used to distinguish mouse-initiated focus from keyboard navigation. + _lastFocusFromMouse = false; + _keyboardNavHandler: EventListener | undefined; + _mouseDownHandler: EventListener | undefined; + _keyboardNavContainer: Element | undefined; _getDefaultOptions(): Properties { @@ -272,6 +278,10 @@ class Toolbar extends ToolbarBase { this._handleKeyboardNavigation(e as KeyboardEvent); }; container.addEventListener('keydown', this._keyboardNavHandler, true); + this._mouseDownHandler = (): void => { + this._lastFocusFromMouse = true; + }; + container.addEventListener('mousedown', this._mouseDownHandler, true); this._keyboardNavContainer = container; eventsEngine.on(container, addNamespace('focusin', KBN_NAMESPACE), (e): void => { this._handleFocusIn(e); }); } @@ -281,6 +291,10 @@ class Toolbar extends ToolbarBase { if (this._keyboardNavHandler) { this._keyboardNavContainer.removeEventListener('keydown', this._keyboardNavHandler, true); } + if (this._mouseDownHandler) { + this._keyboardNavContainer.removeEventListener('mousedown', this._mouseDownHandler, true); + this._mouseDownHandler = undefined; + } eventsEngine.off(this._keyboardNavContainer, addNamespace('focusin', KBN_NAMESPACE)); this._keyboardNavContainer = undefined; this._keyboardNavHandler = undefined; @@ -489,19 +503,30 @@ class Toolbar extends ToolbarBase { const targetEl = e.target as Element; const focusableItems = this._getFocusableItems(); - // Reset "inside widget" mode when focus comes back to a toolbar-level focus target. const index = focusableItems.findIndex(($ft) => { const el = $ft.get(0) as Element | undefined; return el === targetEl || (!!el && el.contains(targetEl)); }); - if (index === -1) return; + if (index === -1) { + this._lastFocusFromMouse = false; + return; + } + + const focusTargetEl = focusableItems[index]?.get(0) as Element | undefined; + const isOnFocusTarget = focusTargetEl === targetEl; + + const fromMouse = this._lastFocusFromMouse; + this._lastFocusFromMouse = false; - // If focus returns to the top-level focus target (not a child button inside ButtonGroup), - // exit inside-widget mode. - const isOnFocusTarget = focusableItems[index]?.get(0) === targetEl; if (isOnFocusTarget) { - this._insideActiveItem = false; + // For a mouse click on a TextBox, immediately activate edit mode so the user can + // type without pressing Enter first. For all other items, always reset to toolbar mode. + const isFocusTargetInput = focusTargetEl?.tagName === 'INPUT'; + const isFocusTargetSelectBox = isFocusTargetInput && !!focusTargetEl?.closest?.('.dx-selectbox'); + const isFocusTargetTextBox = isFocusTargetInput && !isFocusTargetSelectBox; + + this._insideActiveItem = fromMouse && isFocusTargetTextBox; } this._activeItemIndex = index; From 67cf113d1878dbb67ede42ce1ac65dbbf9b058a6 Mon Sep 17 00:00:00 2001 From: pharret31 Date: Tue, 5 May 2026 09:38:29 +0200 Subject: [PATCH 04/11] feat(toolbar): AC-1.4 template item keyboard navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - resolveItemFocusTarget: detect template items by focusable content inside .dx-item-content and return the container as the focus target - TEMPLATE_FOCUSABLE_SELECTOR: exported constant for input/button/select/textarea - _syncRovingTabIndex: suppress inner focusables (tabindex=-1) in toolbar mode; restore natural tabindex when template is active + in edit mode - _handleKeyboardNavigation: detect isTemplateContainer; Enter enters the template (delegates to first inner focusable), Space prevents scroll - _enterTemplateItem: new method – sets _insideActiveItem=true, syncs tabindex, focuses first inner focusable element _insideActiveItem=true for template containers (handles mouse click on inner elements), resets to false for all other items - playground: add template item with + ')); + }, + }, + toolbarSeparator, { location: 'before', locateInMenu: 'auto', From 4848ce5a6198a6e9ca436e144e4923dc0e9c1a9a Mon Sep 17 00:00:00 2001 From: pharret31 Date: Tue, 5 May 2026 10:20:05 +0200 Subject: [PATCH 05/11] feat(toolbar): AC-1.5 disabled skip + dynamic item removal AC-1.5.1 (disabled items skip): - Already working via _getFocusableItems() isDisabled check. - Playground: added disabled dxButton between Undo/Redo to verify. AC-1.5.2 (dynamic item removal / focus preservation): Two root-cause fixes: 1. DOM order in _getFocusableItems(): Switch from items-array order (_getToolbarItems) to visual DOM order (.dx-toolbar-item querySelectorAll). Fixes navigation order mismatch when location:'after' items (e.g. Attach button) are defined before location:'before' items in the items array but appear after them in the toolbar visually. 2. Active-item reference tracking in _initMarkup(): Before re-render: snapshot the active item's data object reference. After re-render: find the same data object in the new DOM and update _activeItemIndex to its new position. - If item moved (insertion before it): index is corrected. - If item removed (deletion): falls back to max(0, savedIndex-1) so focus goes to the previous item, not the next one. - Overflow button (no item data): always relocated to last index. Extracted _findFocusItemIndexByData() helper to keep nesting depth within lint limits. Specification: - Split old 1.5 into 1.5.1 (disabled) + 1.5.2 (dynamic removal). - Overflow menu extracted into new section 1.6. - Removed 'Nice to Have' tier. Playground: - Disabled dxButton for AC-1.5.1. - '+ Add item' / '- Remove added item' buttons for AC-1.5.2. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .copilot/toolbar/specification.md | 105 ++++++++++++++++++ .../js/__internal/ui/toolbar/toolbar.ts | 73 ++++++++++-- packages/devextreme/playground/jquery.html | 36 ++++++ 3 files changed, 202 insertions(+), 12 deletions(-) create mode 100644 .copilot/toolbar/specification.md diff --git a/.copilot/toolbar/specification.md b/.copilot/toolbar/specification.md new file mode 100644 index 000000000000..e7bb9d286afb --- /dev/null +++ b/.copilot/toolbar/specification.md @@ -0,0 +1,105 @@ +# Toolbar — Keyboard Navigation (KBN) Enhancements: Functional Requirements + +> **Spec status:** Draft +> **Reference:** [W3C APG Toolbar Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/) + +--- + +## Priority Tiers + +| Label | Meaning | +|-------|---------| +| ✅ Must Have | Required for the initial release | +| 💡 Nice to Have | Desirable but can be deferred | +| 🚫 Out of Scope | Explicitly excluded from this iteration | + +--- + +## ✅ Must Have + +### 1. Keyboard Interaction Logic + +> Keyboard interactions must follow the **W3C APG Toolbar pattern**. +> Navigation within the toolbar must **not** use the `Tab` key. + +--- + +#### 1.1 Core Navigation Keys + +| Key | Behavior | +|-----|----------| +| `Tab` | Moves focus **into** the toolbar (restoring the previously focused item, or the first item). Pressing `Tab` again **exits** the toolbar entirely. | +| `→` Right Arrow | Moves focus to the **next** enabled item. | +| `←` Left Arrow | Moves focus to the **previous** enabled item. | +| `Home` | Moves focus to the **first** enabled item. | +| `End` | Moves focus to the **last** enabled item. | + +**Boundary behavior:** Focus stops at the edges — pressing `←` on the first item or `→` on the last item does **not** wrap around. + +--- + +#### 1.2 Nested Widget Keyboard Behavior + +Behavior when the focused toolbar item is itself an interactive widget: + +| Widget Category | Examples | Enter Trigger | Navigation Inside | Exit | +|-----------------|----------|---------------|-------------------|------| +| **Simple** | Button, CheckBox | Not required | None | Not required | +| **Group** | ButtonGroup, RadioGroup | Automatic — `↑`/`↓` work immediately | `↑` / `↓` | `←` / `→` move to the next toolbar item | +| **Popup Widget** | MenuButton, DropDown, SelectBox | `Enter` / `Space` / `↓` | Follows the popup-specific pattern | `Esc` | +| **Text Input** | TextBox, Autocomplete | `Enter` or auto-focus | Standard input behavior | `Esc` — return to toolbar; `Tab` — exit toolbar | + +--- + +#### 1.3 Mouse vs. Keyboard Synchronization + +The **Roving Tabindex** anchor must follow the user's last interaction, regardless of input method: + +- `tabindex="0"` always sits on the item that last received focus. +- When a user **clicks** a toolbar item with a mouse, that item becomes the new roving tabindex anchor. + +--- + +#### 1.4 Interaction Within Templates + +For toolbar items that use a **custom template** (e.g., a Search box): + +| Key | Behavior | +|-----|----------| +| `←` / `→` Arrow | Moves focus to the **template container** (the toolbar item). | +| `Enter` / `Space` | **Enters** the template — focus moves to the first focusable element inside. | +| `Esc` | **Exits** the template — focus returns to the toolbar item container; arrow-key navigation resumes. | +| `Tab` (inside template) | Follows normal DOM tab order within the template. When the **last** focusable element inside is reached, `Tab` exits the toolbar entirely (next global tab stop). | + +**Focus delegation rule when entering via arrow keys:** + +- If the template root is **itself focusable** (e.g., a custom `