@ds-mo/ui 2.2.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build-stamp +1 -1
- package/dist/components/ds-app-shell.js +1 -1
- package/dist/components/ds-app-shell.js.map +1 -1
- package/dist/components/ds-bar-nav.js +1 -1
- package/dist/components/ds-bar-nav.js.map +1 -1
- package/dist/components/ds-button-unfilled-icon.js +1 -1
- package/dist/components/ds-checkbox.js +1 -1
- package/dist/components/ds-field.js +1 -1
- package/dist/components/ds-field.js.map +1 -1
- package/dist/components/ds-panel-nav.js +1 -1
- package/dist/components/ds-panel-nav.js.map +1 -1
- package/dist/components/ds-panel-tools.js +1 -1
- package/dist/components/ds-panel-tools.js.map +1 -1
- package/dist/components/ds-slider.js +1 -1
- package/dist/components/ds-tab-group-nav.js +1 -1
- package/dist/components/ds-toggle.js +1 -1
- package/dist/components/{p-Bl-GTn9s.js → p-BTndnbR_.js} +2 -2
- package/dist/components/p-BTndnbR_.js.map +1 -0
- package/dist/components/p-D4zKc1RW.js +2 -0
- package/dist/components/p-D4zKc1RW.js.map +1 -0
- package/dist/types/components/BarNav/BarNav.d.ts +4 -0
- package/dist/types/components/ButtonUnfilledIcon/ButtonUnfilledIcon.d.ts +5 -0
- package/dist/types/components/PanelNav/PanelNav.d.ts +7 -2
- package/dist/types/components/PanelTools/PanelTools.d.ts +4 -0
- package/dist/types/components/TabGroupNav/TabGroupNav.d.ts +22 -3
- package/dist/types/components.d.ts +39 -0
- package/package.json +1 -1
- package/src/angular/proxies.ts +11 -5
- package/src/react/ds-tab-group-nav.ts +8 -2
- package/src/wc/components/AppShell/AppShell.tsx +5 -7
- package/src/wc/components/BarNav/BarNav.tsx +33 -2
- package/src/wc/components/ButtonUnfilledIcon/ButtonUnfilledIcon.tsx +7 -0
- package/src/wc/components/PanelNav/PanelNav.tsx +116 -20
- package/src/wc/components/PanelTools/PanelTools.tsx +64 -14
- package/src/wc/components/TabGroupNav/TabGroupNav.tsx +164 -22
- package/src/wc/components.d.ts +39 -0
- package/dist/components/p-Bl-GTn9s.js.map +0 -1
- package/dist/components/p-CZpCmCiq.js +0 -2
- package/dist/components/p-CZpCmCiq.js.map +0 -1
package/src/angular/proxies.ts
CHANGED
|
@@ -234,7 +234,7 @@ export declare interface DsButtonGroup extends Components.DsButtonGroup {
|
|
|
234
234
|
|
|
235
235
|
|
|
236
236
|
@ProxyCmp({
|
|
237
|
-
inputs: ['activeFill', 'ariaLabel', 'background', 'controls', 'dot', 'expanded', 'hasBorder', 'haspopup', 'icon', 'inactive', 'isActive', 'pressed', 'type'],
|
|
237
|
+
inputs: ['activeFill', 'ariaLabel', 'background', 'controls', 'dot', 'expanded', 'focusTabIndex', 'hasBorder', 'haspopup', 'icon', 'inactive', 'isActive', 'pressed', 'type'],
|
|
238
238
|
methods: ['setFocus']
|
|
239
239
|
})
|
|
240
240
|
@Component({
|
|
@@ -242,7 +242,7 @@ export declare interface DsButtonGroup extends Components.DsButtonGroup {
|
|
|
242
242
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
243
243
|
template: '<ng-content></ng-content>',
|
|
244
244
|
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
|
245
|
-
inputs: ['activeFill', 'ariaLabel', 'background', 'controls', 'dot', 'expanded', 'hasBorder', 'haspopup', 'icon', 'inactive', 'isActive', 'pressed', 'type'],
|
|
245
|
+
inputs: ['activeFill', 'ariaLabel', 'background', 'controls', 'dot', 'expanded', 'focusTabIndex', 'hasBorder', 'haspopup', 'icon', 'inactive', 'isActive', 'pressed', 'type'],
|
|
246
246
|
outputs: ['dsClick', 'dsChange'],
|
|
247
247
|
standalone: false
|
|
248
248
|
})
|
|
@@ -919,20 +919,22 @@ export declare interface DsTabGroup extends Components.DsTabGroup {
|
|
|
919
919
|
|
|
920
920
|
|
|
921
921
|
@ProxyCmp({
|
|
922
|
-
inputs: ['ariaLabel', 'ariaLabelledby', 'background', 'orientation', 'tabs', 'value']
|
|
922
|
+
inputs: ['ariaLabel', 'ariaLabelledby', 'background', 'orientation', 'rovingEnabled', 'selectionFollowsFocus', 'tabs', 'value'],
|
|
923
|
+
methods: ['focusTab', 'focusLastTab', 'focusFirstTab']
|
|
923
924
|
})
|
|
924
925
|
@Component({
|
|
925
926
|
selector: 'ds-tab-group-nav',
|
|
926
927
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
927
928
|
template: '<ng-content></ng-content>',
|
|
928
929
|
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
|
929
|
-
inputs: ['ariaLabel', 'ariaLabelledby', 'background', 'orientation', 'tabs', 'value'],
|
|
930
|
-
outputs: ['dsChange'],
|
|
930
|
+
inputs: ['ariaLabel', 'ariaLabelledby', 'background', 'orientation', 'rovingEnabled', 'selectionFollowsFocus', 'tabs', 'value'],
|
|
931
|
+
outputs: ['dsChange', 'dsRovingExit'],
|
|
931
932
|
standalone: false
|
|
932
933
|
})
|
|
933
934
|
export class DsTabGroupNav {
|
|
934
935
|
protected el: HTMLDsTabGroupNavElement;
|
|
935
936
|
@Output() dsChange = new EventEmitter<CustomEvent<string>>();
|
|
937
|
+
@Output() dsRovingExit = new EventEmitter<CustomEvent<'start' | 'end'>>();
|
|
936
938
|
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
|
937
939
|
c.detach();
|
|
938
940
|
this.el = r.nativeElement;
|
|
@@ -943,6 +945,10 @@ export class DsTabGroupNav {
|
|
|
943
945
|
export declare interface DsTabGroupNav extends Components.DsTabGroupNav {
|
|
944
946
|
|
|
945
947
|
dsChange: EventEmitter<CustomEvent<string>>;
|
|
948
|
+
/**
|
|
949
|
+
* Fired when arrow navigation reaches the first/last tab in manual selection mode.
|
|
950
|
+
*/
|
|
951
|
+
dsRovingExit: EventEmitter<CustomEvent<'start' | 'end'>>;
|
|
946
952
|
}
|
|
947
953
|
|
|
948
954
|
|
|
@@ -15,13 +15,19 @@ import { type DsTabGroupNavCustomEvent } from "@ds-mo/ui";
|
|
|
15
15
|
import type { Components } from "@ds-mo/ui/dist/components";
|
|
16
16
|
import { DsTabGroupNav as DsTabGroupNavElement, defineCustomElement as defineDsTabGroupNav } from "@ds-mo/ui/dist/components/ds-tab-group-nav.js";
|
|
17
17
|
|
|
18
|
-
export type DsTabGroupNavEvents = {
|
|
18
|
+
export type DsTabGroupNavEvents = {
|
|
19
|
+
onDsChange: EventName<DsTabGroupNavCustomEvent<string>>,
|
|
20
|
+
onDsRovingExit: EventName<DsTabGroupNavCustomEvent<'start' | 'end'>>
|
|
21
|
+
};
|
|
19
22
|
|
|
20
23
|
export const DsTabGroupNav: StencilReactComponent<DsTabGroupNavElement, DsTabGroupNavEvents, Components.DsTabGroupNav> = /*@__PURE__*/ createComponent<DsTabGroupNavElement, DsTabGroupNavEvents, Components.DsTabGroupNav>({
|
|
21
24
|
tagName: 'ds-tab-group-nav',
|
|
22
25
|
elementClass: DsTabGroupNavElement,
|
|
23
26
|
// @ts-ignore - ignore potential React type mismatches between the Stencil Output Target and your project.
|
|
24
27
|
react: React,
|
|
25
|
-
events: {
|
|
28
|
+
events: {
|
|
29
|
+
onDsChange: 'dsChange',
|
|
30
|
+
onDsRovingExit: 'dsRovingExit'
|
|
31
|
+
} as DsTabGroupNavEvents,
|
|
26
32
|
defineCustomElement: defineDsTabGroupNav
|
|
27
33
|
});
|
|
@@ -322,17 +322,15 @@ export class AppShell {
|
|
|
322
322
|
<slot name="panel" />
|
|
323
323
|
</div>
|
|
324
324
|
<div class="app-shell__main">
|
|
325
|
-
<div class="app-
|
|
326
|
-
<
|
|
327
|
-
<slot name="bar" />
|
|
328
|
-
</div>
|
|
329
|
-
<div class="app-shell__content">
|
|
330
|
-
<slot />
|
|
331
|
-
</div>
|
|
325
|
+
<div class="app-shell__bar">
|
|
326
|
+
<slot name="bar" />
|
|
332
327
|
</div>
|
|
333
328
|
<div class="app-shell__tools">
|
|
334
329
|
<slot name="tools" />
|
|
335
330
|
</div>
|
|
331
|
+
<div class="app-shell__content">
|
|
332
|
+
<slot />
|
|
333
|
+
</div>
|
|
336
334
|
</div>
|
|
337
335
|
</div>
|
|
338
336
|
</Host>
|
|
@@ -86,6 +86,7 @@ export class BarNav {
|
|
|
86
86
|
@State() private visibleTabs: BarNavTab[] = [];
|
|
87
87
|
@State() private overflowTabs: BarNavTab[] = [];
|
|
88
88
|
@State() private menuInitialFocusVisible = false;
|
|
89
|
+
@State() private overflowRovingFocused = false;
|
|
89
90
|
|
|
90
91
|
private static readonly HOST_PROP_SYNC_BUDGET = 8;
|
|
91
92
|
private static readonly INTRINSIC_WIDTH_RETRY_MAX = 3;
|
|
@@ -95,7 +96,7 @@ export class BarNav {
|
|
|
95
96
|
private headerEl: HTMLElement | null = null;
|
|
96
97
|
private triggerEl: (HTMLElement & { setFocus?: () => Promise<void> }) | null = null;
|
|
97
98
|
private menuEl: HTMLDsMenuElement | null = null;
|
|
98
|
-
private visibleTabGroupEl:
|
|
99
|
+
private visibleTabGroupEl: HTMLDsTabGroupNavElement | null = null;
|
|
99
100
|
private probeTabGroupEl: QueryableHost | null = null;
|
|
100
101
|
private resizeObserver: ResizeObserver | null = null;
|
|
101
102
|
private intrinsicWidthRetryCount = 0;
|
|
@@ -571,11 +572,32 @@ export class BarNav {
|
|
|
571
572
|
this.menuOpen = true;
|
|
572
573
|
}
|
|
573
574
|
|
|
575
|
+
private handleTabRovingExit(e: CustomEvent<'start' | 'end'>) {
|
|
576
|
+
if (e.detail !== 'end' || !this.hasOverflowTabs) return;
|
|
577
|
+
this.overflowRovingFocused = true;
|
|
578
|
+
void this.triggerEl?.setFocus?.();
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private handleOverflowFocus = () => {
|
|
582
|
+
this.overflowRovingFocused = true;
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
private handleOverflowBlur = () => {
|
|
586
|
+
this.overflowRovingFocused = false;
|
|
587
|
+
};
|
|
588
|
+
|
|
574
589
|
private handleTriggerKeyDown(e: KeyboardEvent) {
|
|
575
590
|
if (!this.hasOverflowTabs) return;
|
|
576
591
|
|
|
577
592
|
if (!this.menuOpen) {
|
|
578
|
-
if (e.key === '
|
|
593
|
+
if (e.key === 'ArrowLeft') {
|
|
594
|
+
e.preventDefault();
|
|
595
|
+
this.overflowRovingFocused = false;
|
|
596
|
+
void this.visibleTabGroupEl?.focusLastTab();
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
579
601
|
e.preventDefault();
|
|
580
602
|
this.toggleTabMenu({ focusVisible: true });
|
|
581
603
|
}
|
|
@@ -645,7 +667,13 @@ export class BarNav {
|
|
|
645
667
|
}}
|
|
646
668
|
tabs={renderedTabs}
|
|
647
669
|
value={this.effectiveValue}
|
|
670
|
+
selectionFollowsFocus={false}
|
|
671
|
+
rovingEnabled={!this.overflowRovingFocused}
|
|
648
672
|
onDsChange={(e: Event) => this.handleTabChange(e)}
|
|
673
|
+
onDsRovingExit={(e: CustomEvent<'start' | 'end'>) => this.handleTabRovingExit(e)}
|
|
674
|
+
onFocusin={() => {
|
|
675
|
+
this.overflowRovingFocused = false;
|
|
676
|
+
}}
|
|
649
677
|
/>
|
|
650
678
|
</div>
|
|
651
679
|
)}
|
|
@@ -666,6 +694,7 @@ export class BarNav {
|
|
|
666
694
|
icon="Ellipses"
|
|
667
695
|
isActive={this.menuOpen}
|
|
668
696
|
activeFill={false}
|
|
697
|
+
focusTabIndex={this.overflowRovingFocused ? 0 : -1}
|
|
669
698
|
ref={(el?: HTMLDsButtonUnfilledIconElement) => {
|
|
670
699
|
this.triggerEl = (el as (HTMLElement & { setFocus?: () => Promise<void> })) ?? null;
|
|
671
700
|
}}
|
|
@@ -673,6 +702,8 @@ export class BarNav {
|
|
|
673
702
|
expanded={this.menuOpen}
|
|
674
703
|
aria-label="More tabs"
|
|
675
704
|
onDsClick={() => this.toggleTabMenu({ focusVisible: false })}
|
|
705
|
+
onFocusin={this.handleOverflowFocus}
|
|
706
|
+
onFocusout={this.handleOverflowBlur}
|
|
676
707
|
onKeyDown={(e: KeyboardEvent) => this.handleTriggerKeyDown(e)}
|
|
677
708
|
/>
|
|
678
709
|
)}
|
|
@@ -46,6 +46,12 @@ export class ButtonUnfilledIcon {
|
|
|
46
46
|
@Prop() haspopup: string | undefined;
|
|
47
47
|
@Prop() pressed: boolean | undefined;
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Native `tabindex` for roving keyboard groups in shell chrome.
|
|
51
|
+
* Omit for the default button tab stop (`0`).
|
|
52
|
+
*/
|
|
53
|
+
@Prop({ attribute: 'tab-index' }) focusTabIndex?: number;
|
|
54
|
+
|
|
49
55
|
@Event() dsClick!: EventEmitter<MouseEvent>;
|
|
50
56
|
@Event() dsChange!: EventEmitter<boolean>;
|
|
51
57
|
|
|
@@ -88,6 +94,7 @@ export class ButtonUnfilledIcon {
|
|
|
88
94
|
type={this.type}
|
|
89
95
|
class={cls}
|
|
90
96
|
disabled={this.inactive}
|
|
97
|
+
tabIndex={this.focusTabIndex ?? 0}
|
|
91
98
|
aria-label={this.ariaLabel}
|
|
92
99
|
aria-controls={this.controls}
|
|
93
100
|
aria-expanded={this.expanded === undefined ? undefined : String(this.expanded)}
|
|
@@ -143,14 +143,14 @@ export class PanelNav {
|
|
|
143
143
|
@Watch('groups')
|
|
144
144
|
onGroupsChange(val: string | PanelNavGroup[]) {
|
|
145
145
|
this.parsedGroups = parsePanelNavGroups(val);
|
|
146
|
-
this.
|
|
146
|
+
this.rovingIndex = 0;
|
|
147
147
|
this.syncActiveFromUrl();
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
@Watch('activeId')
|
|
151
151
|
@Watch('urlDerivedActiveId')
|
|
152
152
|
onActiveIdChange() {
|
|
153
|
-
|
|
153
|
+
/* Active route updates aria-current only — roving tab stop stays user-controlled. */
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
@Watch('currentUrl')
|
|
@@ -288,26 +288,112 @@ export class PanelNav {
|
|
|
288
288
|
return this.parsedGroups.flatMap(g => g.items);
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
private
|
|
292
|
-
|
|
293
|
-
this.rovingIndex = idx >= 0 ? idx : 0;
|
|
291
|
+
private getRovingTotal(): number {
|
|
292
|
+
return 1 + this.getAllItems().length + 2;
|
|
294
293
|
}
|
|
295
294
|
|
|
296
|
-
private
|
|
297
|
-
|
|
295
|
+
private getFooterRovingIndex(): number {
|
|
296
|
+
return 1 + this.getAllItems().length;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private getUserRovingIndex(): number {
|
|
300
|
+
return this.getFooterRovingIndex() + 1;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private getRovingElement(index: number): HTMLElement | null {
|
|
304
|
+
const itemCount = this.getAllItems().length;
|
|
305
|
+
if (index === 0) {
|
|
306
|
+
return this.el.querySelector('.panel-nav__header-btn');
|
|
307
|
+
}
|
|
308
|
+
if (index >= 1 && index <= itemCount) {
|
|
309
|
+
const items = this.el.querySelectorAll<HTMLElement>('.panel-nav__body .panel-nav__item');
|
|
310
|
+
return items[index - 1] ?? null;
|
|
311
|
+
}
|
|
312
|
+
if (index === this.getFooterRovingIndex()) {
|
|
313
|
+
return this.el.querySelector('.panel-nav__footer-btn .button-icon');
|
|
314
|
+
}
|
|
315
|
+
if (index === this.getUserRovingIndex()) {
|
|
316
|
+
return this.el.querySelector('.panel-nav__footer-user');
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private focusRovingAt(index: number) {
|
|
322
|
+
this.rovingIndex = index;
|
|
323
|
+
this.getRovingElement(index)?.focus({ preventScroll: true });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private activateRovingIndex(index: number) {
|
|
327
|
+
const itemCount = this.getAllItems().length;
|
|
328
|
+
const items = this.getAllItems();
|
|
329
|
+
if (index === 0) {
|
|
330
|
+
this.handleToggle();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (index >= 1 && index <= itemCount) {
|
|
334
|
+
this.handleItemClick(items[index - 1].id);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (index === this.getFooterRovingIndex()) {
|
|
338
|
+
this.handleFooterAction();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (index === this.getUserRovingIndex()) {
|
|
342
|
+
const anchor = this.el.querySelector(`#${PANEL_NAV_USER_MENU_ANCHOR_ID}`) as HTMLElement | null;
|
|
343
|
+
if (anchor) this.dsNavUserAction.emit({ anchor });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private handleRovingKeyDown(e: KeyboardEvent, index: number) {
|
|
348
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
349
|
+
e.preventDefault();
|
|
350
|
+
this.activateRovingIndex(index);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const total = this.getRovingTotal();
|
|
355
|
+
const footerIdx = this.getFooterRovingIndex();
|
|
356
|
+
const userIdx = this.getUserRovingIndex();
|
|
298
357
|
let next: number;
|
|
358
|
+
|
|
299
359
|
switch (e.key) {
|
|
300
|
-
case 'ArrowDown':
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
360
|
+
case 'ArrowDown':
|
|
361
|
+
e.preventDefault();
|
|
362
|
+
if (index === footerIdx) next = userIdx;
|
|
363
|
+
else if (index === userIdx) next = 0;
|
|
364
|
+
else next = (index + 1) % total;
|
|
365
|
+
break;
|
|
366
|
+
case 'ArrowUp':
|
|
367
|
+
e.preventDefault();
|
|
368
|
+
if (index === 0) next = userIdx;
|
|
369
|
+
else if (index === userIdx) next = footerIdx;
|
|
370
|
+
else next = (index - 1 + total) % total;
|
|
371
|
+
break;
|
|
372
|
+
case 'ArrowRight':
|
|
373
|
+
if (index === footerIdx) {
|
|
374
|
+
e.preventDefault();
|
|
375
|
+
next = userIdx;
|
|
376
|
+
} else return;
|
|
377
|
+
break;
|
|
378
|
+
case 'ArrowLeft':
|
|
379
|
+
if (index === userIdx) {
|
|
380
|
+
e.preventDefault();
|
|
381
|
+
next = footerIdx;
|
|
382
|
+
} else return;
|
|
383
|
+
break;
|
|
384
|
+
case 'Home':
|
|
385
|
+
e.preventDefault();
|
|
386
|
+
next = 0;
|
|
387
|
+
break;
|
|
388
|
+
case 'End':
|
|
389
|
+
e.preventDefault();
|
|
390
|
+
next = userIdx;
|
|
391
|
+
break;
|
|
392
|
+
default:
|
|
393
|
+
return;
|
|
305
394
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
this.el.querySelectorAll<HTMLElement>('.panel-nav__body .panel-nav__item')
|
|
309
|
-
);
|
|
310
|
-
items[next]?.focus();
|
|
395
|
+
|
|
396
|
+
this.focusRovingAt(next);
|
|
311
397
|
}
|
|
312
398
|
|
|
313
399
|
private checkScroll() {
|
|
@@ -411,6 +497,7 @@ export class PanelNav {
|
|
|
411
497
|
|
|
412
498
|
private renderNavItem(item: PanelNavItem, idx: number, collapsed: boolean) {
|
|
413
499
|
const isActive = item.id === this.effectiveActiveId;
|
|
500
|
+
const rovingPosition = idx + 1;
|
|
414
501
|
|
|
415
502
|
const itemContent = [
|
|
416
503
|
<span class="panel-nav__item-icon">
|
|
@@ -446,10 +533,10 @@ export class PanelNav {
|
|
|
446
533
|
},
|
|
447
534
|
'aria-current': isActive ? ('page' as const) : undefined,
|
|
448
535
|
title: collapsed ? item.label : undefined,
|
|
449
|
-
tabIndex:
|
|
536
|
+
tabIndex: rovingPosition === this.rovingIndex ? 0 : -1,
|
|
450
537
|
onClick: () => this.handleItemClick(item.id),
|
|
451
|
-
onKeyDown: (e: KeyboardEvent) => this.
|
|
452
|
-
onFocus: () => { this.rovingIndex =
|
|
538
|
+
onKeyDown: (e: KeyboardEvent) => this.handleRovingKeyDown(e, rovingPosition),
|
|
539
|
+
onFocus: () => { this.rovingIndex = rovingPosition; },
|
|
453
540
|
};
|
|
454
541
|
|
|
455
542
|
const useAnchor = this.routerMode === 'anchor' && item.href;
|
|
@@ -487,7 +574,10 @@ export class PanelNav {
|
|
|
487
574
|
<button
|
|
488
575
|
type="button"
|
|
489
576
|
class="panel-nav__header-btn ds-focus-ring-inset"
|
|
577
|
+
tabIndex={this.rovingIndex === 0 ? 0 : -1}
|
|
490
578
|
onClick={() => this.handleToggle()}
|
|
579
|
+
onKeyDown={(e: KeyboardEvent) => this.handleRovingKeyDown(e, 0)}
|
|
580
|
+
onFocus={() => { this.rovingIndex = 0; }}
|
|
491
581
|
aria-label={collapsed ? 'Expand navigation' : 'Collapse navigation'}
|
|
492
582
|
aria-expanded={collapsed ? 'false' : 'true'}
|
|
493
583
|
>
|
|
@@ -543,9 +633,12 @@ export class PanelNav {
|
|
|
543
633
|
class="panel-nav__footer-btn"
|
|
544
634
|
icon={isDashboardChrome ? 'Gear' : 'Dashboard'}
|
|
545
635
|
activeFill={false}
|
|
636
|
+
focusTabIndex={this.rovingIndex === this.getFooterRovingIndex() ? 0 : -1}
|
|
546
637
|
title={isDashboardChrome ? 'Settings' : 'Dashboard'}
|
|
547
638
|
aria-label={isDashboardChrome ? 'Open settings' : 'Go to dashboard'}
|
|
548
639
|
onDsClick={() => this.handleFooterAction()}
|
|
640
|
+
onKeyDown={(e: KeyboardEvent) => this.handleRovingKeyDown(e, this.getFooterRovingIndex())}
|
|
641
|
+
onFocusin={() => { this.rovingIndex = this.getFooterRovingIndex(); }}
|
|
549
642
|
/>
|
|
550
643
|
|
|
551
644
|
{/* Right user — label fades like nav items; right icon cross-fades chevron ↔ circle+initial */}
|
|
@@ -553,8 +646,11 @@ export class PanelNav {
|
|
|
553
646
|
type="button"
|
|
554
647
|
id={PANEL_NAV_USER_MENU_ANCHOR_ID}
|
|
555
648
|
class="panel-nav__item panel-nav__footer-user ds-focus-ring-inset"
|
|
649
|
+
tabIndex={this.rovingIndex === this.getUserRovingIndex() ? 0 : -1}
|
|
556
650
|
aria-label={collapsed ? `User: ${userName}` : `User menu for ${userName}`}
|
|
557
651
|
onClick={(e) => this.handleUserAction(e)}
|
|
652
|
+
onKeyDown={(e: KeyboardEvent) => this.handleRovingKeyDown(e, this.getUserRovingIndex())}
|
|
653
|
+
onFocus={() => { this.rovingIndex = this.getUserRovingIndex(); }}
|
|
558
654
|
>
|
|
559
655
|
<span class="panel-nav__item-label panel-nav__footer-user-label">
|
|
560
656
|
<span class="panel-nav__item-label-text">
|
|
@@ -51,12 +51,21 @@ export class PanelTools {
|
|
|
51
51
|
/** Suppresses width transition until the host has painted its initial open state. */
|
|
52
52
|
@State() private readyForMotion = false;
|
|
53
53
|
|
|
54
|
+
@State() private rovingIndex = 0;
|
|
55
|
+
|
|
54
56
|
private motionEnableGeneration = 0;
|
|
55
57
|
|
|
56
58
|
private get railItems(): PanelToolsItem[] {
|
|
57
59
|
return parsePanelToolsItems(this.items, this.itemsJson);
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
private get orderedRailItems(): PanelToolsItem[] {
|
|
63
|
+
const railItems = this.railItems;
|
|
64
|
+
const headerItem = railItems.find(item => item.id === PANEL_TOOLS_PRIMARY_TOOL_ID);
|
|
65
|
+
const bodyItems = railItems.filter(item => item.id !== PANEL_TOOLS_PRIMARY_TOOL_ID);
|
|
66
|
+
return headerItem ? [headerItem, ...bodyItems] : bodyItems;
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
disconnectedCallback() {
|
|
61
70
|
this.el.removeEventListener('transitionend', this.handleTransitionEnd);
|
|
62
71
|
this.motionEnableGeneration += 1;
|
|
@@ -102,6 +111,7 @@ export class PanelTools {
|
|
|
102
111
|
@Watch('items')
|
|
103
112
|
@Watch('itemsJson')
|
|
104
113
|
itemsChanged() {
|
|
114
|
+
this.rovingIndex = 0;
|
|
105
115
|
this.deferMotionEnable();
|
|
106
116
|
}
|
|
107
117
|
|
|
@@ -142,7 +152,40 @@ export class PanelTools {
|
|
|
142
152
|
this.dsToolChange.emit({ id, selected });
|
|
143
153
|
};
|
|
144
154
|
|
|
145
|
-
private
|
|
155
|
+
private focusRailAt(index: number) {
|
|
156
|
+
const items = this.orderedRailItems;
|
|
157
|
+
if (!items.length) return;
|
|
158
|
+
const bounded = ((index % items.length) + items.length) % items.length;
|
|
159
|
+
this.rovingIndex = bounded;
|
|
160
|
+
const actions = Array.from(
|
|
161
|
+
this.el.querySelectorAll<HTMLElement>('.panel-tools__rail-action .button-icon'),
|
|
162
|
+
);
|
|
163
|
+
actions[bounded]?.focus({ preventScroll: true });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private handleRailKeyDown = (e: KeyboardEvent, index: number) => {
|
|
167
|
+
const items = this.orderedRailItems;
|
|
168
|
+
if (!items.length) return;
|
|
169
|
+
|
|
170
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
this.handleToolChange(items[index].id);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (e.key === 'ArrowDown') {
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
this.focusRailAt(index + 1);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (e.key === 'ArrowUp') {
|
|
183
|
+
e.preventDefault();
|
|
184
|
+
this.focusRailAt(index - 1);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
private renderRailAction(item: PanelToolsItem, index: number) {
|
|
146
189
|
return (
|
|
147
190
|
<ds-button-unfilled-icon
|
|
148
191
|
key={item.id}
|
|
@@ -152,18 +195,21 @@ export class PanelTools {
|
|
|
152
195
|
activeFill={false}
|
|
153
196
|
dot={item.dot ?? false}
|
|
154
197
|
inactive={item.inactive}
|
|
198
|
+
focusTabIndex={index === this.rovingIndex ? 0 : -1}
|
|
155
199
|
aria-label={item.ariaLabel ?? PANEL_TOOLS_LABELS[item.id]}
|
|
156
200
|
pressed={this.isRailSelected(item.id)}
|
|
157
|
-
|
|
201
|
+
onFocusin={() => { this.rovingIndex = index; }}
|
|
202
|
+
onKeyDown={(e: KeyboardEvent) => this.handleRailKeyDown(e, index)}
|
|
203
|
+
onDsClick={() => this.handleToolChange(item.id)}
|
|
158
204
|
/>
|
|
159
205
|
);
|
|
160
206
|
}
|
|
161
207
|
|
|
162
208
|
render() {
|
|
163
209
|
const headerLabel = this.headerLabel();
|
|
164
|
-
const
|
|
165
|
-
const headerItem =
|
|
166
|
-
const bodyItems =
|
|
210
|
+
const orderedRailItems = this.orderedRailItems;
|
|
211
|
+
const headerItem = orderedRailItems.find(item => item.id === PANEL_TOOLS_PRIMARY_TOOL_ID);
|
|
212
|
+
const bodyItems = orderedRailItems.filter(item => item.id !== PANEL_TOOLS_PRIMARY_TOOL_ID);
|
|
167
213
|
const showDrawerChrome = this.isDrawerPresent();
|
|
168
214
|
const drawerResting = panelToolsDrawerResting(this.open, this.motion);
|
|
169
215
|
|
|
@@ -181,6 +227,19 @@ export class PanelTools {
|
|
|
181
227
|
aria-label="Tools"
|
|
182
228
|
>
|
|
183
229
|
<div class="panel-tools__layout">
|
|
230
|
+
<nav class="panel-tools__rail" aria-label="Tool shortcuts">
|
|
231
|
+
{headerItem ? (
|
|
232
|
+
<div class="panel-tools__rail-header">
|
|
233
|
+
{this.renderRailAction(headerItem, 0)}
|
|
234
|
+
</div>
|
|
235
|
+
) : null}
|
|
236
|
+
<div class="panel-tools__rail-body">
|
|
237
|
+
{bodyItems.map((item, bodyIdx) =>
|
|
238
|
+
this.renderRailAction(item, headerItem ? bodyIdx + 1 : bodyIdx),
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
</nav>
|
|
242
|
+
|
|
184
243
|
<div
|
|
185
244
|
class={{
|
|
186
245
|
'panel-tools__drawer': true,
|
|
@@ -247,15 +306,6 @@ export class PanelTools {
|
|
|
247
306
|
</div>
|
|
248
307
|
</div>
|
|
249
308
|
</div>
|
|
250
|
-
|
|
251
|
-
<nav class="panel-tools__rail" aria-label="Tool shortcuts">
|
|
252
|
-
{headerItem ? (
|
|
253
|
-
<div class="panel-tools__rail-header">{this.renderRailAction(headerItem)}</div>
|
|
254
|
-
) : null}
|
|
255
|
-
<div class="panel-tools__rail-body">
|
|
256
|
-
{bodyItems.map(item => this.renderRailAction(item))}
|
|
257
|
-
</div>
|
|
258
|
-
</nav>
|
|
259
309
|
</div>
|
|
260
310
|
</Host>
|
|
261
311
|
);
|