@ds-mo/ui 2.2.1 → 2.4.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/index.js +1 -1
- package/dist/components/index.js.map +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/AppShell/AppShell.d.ts +3 -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 +8 -2
- package/dist/types/components/PanelTools/PanelTools.d.ts +8 -0
- package/dist/types/components/PanelTools/panel-tools-utils.d.ts +7 -1
- package/dist/types/components/TabGroupNav/TabGroupNav.d.ts +22 -3
- package/dist/types/components.d.ts +62 -0
- package/dist/types/nav/shell-shortcuts.d.ts +10 -0
- package/package.json +2 -2
- package/src/angular/proxies.ts +17 -9
- package/src/react/ds-tab-group-nav.ts +8 -2
- package/src/wc/components/AppShell/AppShell.tsx +43 -8
- 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 -22
- package/src/wc/components/PanelTools/PanelTools.tsx +91 -23
- package/src/wc/components/PanelTools/panel-tools-utils.ts +13 -1
- package/src/wc/components/TabGroupNav/TabGroupNav.tsx +164 -22
- package/src/wc/components.d.ts +62 -0
- package/src/wc/nav/shell-shortcuts.ts +63 -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
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { Component, Prop, Element, Watch, h, Host } from '@stencil/core';
|
|
1
|
+
import { Component, Prop, Element, Watch, Listen, h, Host } from '@stencil/core';
|
|
2
2
|
import type { NavChromeStyle } from '../../nav/nav-chrome';
|
|
3
|
+
import {
|
|
4
|
+
isEditableShortcutTarget,
|
|
5
|
+
resolveShellShortcut,
|
|
6
|
+
} from '../../nav/shell-shortcuts';
|
|
7
|
+
import type { PanelToolsToolId } from '../PanelTools/panel-tools-types';
|
|
3
8
|
import {
|
|
4
9
|
CHROME_TRANSITION_END,
|
|
5
10
|
CHROME_TRANSITION_START,
|
|
@@ -48,6 +53,9 @@ export class AppShell {
|
|
|
48
53
|
*/
|
|
49
54
|
@Prop() gradientSrc: string = '';
|
|
50
55
|
|
|
56
|
+
/** When `true` (default), registers global shell keyboard shortcuts. Tool chords (⌘/Ctrl+K/A/S/M/N) toggle their drawer open and closed; ⌘/Ctrl+[ toggles panel nav; ⌘/Ctrl+] closes any open tool drawer. */
|
|
57
|
+
@Prop({ attribute: 'shortcuts-enabled' }) shortcutsEnabled: boolean = true;
|
|
58
|
+
|
|
51
59
|
@Element() el!: HTMLElement;
|
|
52
60
|
|
|
53
61
|
private resizeObserver: ResizeObserver | null = null;
|
|
@@ -242,6 +250,35 @@ export class AppShell {
|
|
|
242
250
|
return this.gradient || this.grid;
|
|
243
251
|
}
|
|
244
252
|
|
|
253
|
+
@Listen('keydown', { target: 'window', capture: true })
|
|
254
|
+
handleWindowKeyDown(e: KeyboardEvent) {
|
|
255
|
+
if (!this.shortcutsEnabled) return;
|
|
256
|
+
if (isEditableShortcutTarget(e.target)) return;
|
|
257
|
+
|
|
258
|
+
const action = resolveShellShortcut(e);
|
|
259
|
+
if (!action) return;
|
|
260
|
+
|
|
261
|
+
e.preventDefault();
|
|
262
|
+
|
|
263
|
+
const panel = this.el.querySelector('ds-panel-nav') as HTMLDsPanelNavElement | null;
|
|
264
|
+
const tools = this.el.querySelector('ds-panel-tools') as HTMLDsPanelToolsElement | null;
|
|
265
|
+
|
|
266
|
+
if (action === 'toggle-panel-nav') {
|
|
267
|
+
void panel?.toggleCollapsed();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (action === 'close-panel-tools') {
|
|
272
|
+
void tools?.closeDrawer();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (action.startsWith('open-tool:') && tools) {
|
|
277
|
+
const toolId = action.slice('open-tool:'.length) as PanelToolsToolId;
|
|
278
|
+
void tools.activateTool(toolId);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
245
282
|
private syncChrome() {
|
|
246
283
|
const panelNav = this.el.querySelector('ds-panel-nav') as HTMLElement | null;
|
|
247
284
|
const bar = this.el.querySelector('ds-bar-nav') as HTMLElement | null;
|
|
@@ -322,17 +359,15 @@ export class AppShell {
|
|
|
322
359
|
<slot name="panel" />
|
|
323
360
|
</div>
|
|
324
361
|
<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>
|
|
362
|
+
<div class="app-shell__bar">
|
|
363
|
+
<slot name="bar" />
|
|
332
364
|
</div>
|
|
333
365
|
<div class="app-shell__tools">
|
|
334
366
|
<slot name="tools" />
|
|
335
367
|
</div>
|
|
368
|
+
<div class="app-shell__content">
|
|
369
|
+
<slot />
|
|
370
|
+
</div>
|
|
336
371
|
</div>
|
|
337
372
|
</div>
|
|
338
373
|
</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)}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Component, Prop, Event, EventEmitter, Watch, State, Element, h, Host } from '@stencil/core';
|
|
1
|
+
import { Component, Prop, Event, EventEmitter, Watch, State, Element, Method, h, Host } from '@stencil/core';
|
|
2
2
|
import type { ChromeTransitionDetail } from '../../nav/chrome-transition';
|
|
3
3
|
import type { NavChromeStyle } from '../../nav/nav-chrome';
|
|
4
4
|
import {
|
|
@@ -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,104 @@ 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 getFooterRovingIndex(): number {
|
|
292
|
+
return 1 + this.getAllItems().length;
|
|
294
293
|
}
|
|
295
294
|
|
|
296
|
-
private
|
|
297
|
-
|
|
298
|
-
|
|
295
|
+
private getUserRovingIndex(): number {
|
|
296
|
+
return this.getFooterRovingIndex() + 1;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private getRovingElement(index: number): HTMLElement | null {
|
|
300
|
+
const itemCount = this.getAllItems().length;
|
|
301
|
+
if (index === 0) {
|
|
302
|
+
return this.el.querySelector('.panel-nav__header-btn');
|
|
303
|
+
}
|
|
304
|
+
if (index >= 1 && index <= itemCount) {
|
|
305
|
+
const items = this.el.querySelectorAll<HTMLElement>('.panel-nav__body .panel-nav__item');
|
|
306
|
+
return items[index - 1] ?? null;
|
|
307
|
+
}
|
|
308
|
+
if (index === this.getFooterRovingIndex()) {
|
|
309
|
+
return this.el.querySelector('.panel-nav__footer-btn .button-icon');
|
|
310
|
+
}
|
|
311
|
+
if (index === this.getUserRovingIndex()) {
|
|
312
|
+
return this.el.querySelector('.panel-nav__footer-user');
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private focusRovingAt(index: number) {
|
|
318
|
+
this.rovingIndex = index;
|
|
319
|
+
this.getRovingElement(index)?.focus({ preventScroll: true });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private activateRovingIndex(index: number) {
|
|
323
|
+
const itemCount = this.getAllItems().length;
|
|
324
|
+
const items = this.getAllItems();
|
|
325
|
+
if (index === 0) {
|
|
326
|
+
this.handleToggle();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (index >= 1 && index <= itemCount) {
|
|
330
|
+
this.handleItemClick(items[index - 1].id);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (index === this.getFooterRovingIndex()) {
|
|
334
|
+
this.handleFooterAction();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (index === this.getUserRovingIndex()) {
|
|
338
|
+
const anchor = this.el.querySelector(`#${PANEL_NAV_USER_MENU_ANCHOR_ID}`) as HTMLElement | null;
|
|
339
|
+
if (anchor) this.dsNavUserAction.emit({ anchor });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private handleRovingKeyDown(e: KeyboardEvent, index: number) {
|
|
344
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
345
|
+
e.preventDefault();
|
|
346
|
+
this.activateRovingIndex(index);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const footerIdx = this.getFooterRovingIndex();
|
|
351
|
+
const userIdx = this.getUserRovingIndex();
|
|
352
|
+
let next: number | undefined;
|
|
353
|
+
|
|
299
354
|
switch (e.key) {
|
|
300
|
-
case 'ArrowDown':
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
355
|
+
case 'ArrowDown':
|
|
356
|
+
if (index === userIdx) return;
|
|
357
|
+
if (index === footerIdx) next = userIdx;
|
|
358
|
+
else next = index + 1;
|
|
359
|
+
break;
|
|
360
|
+
case 'ArrowUp':
|
|
361
|
+
if (index === 0) return;
|
|
362
|
+
if (index === userIdx) next = footerIdx;
|
|
363
|
+
else if (index === footerIdx) next = footerIdx - 1;
|
|
364
|
+
else next = index - 1;
|
|
365
|
+
break;
|
|
366
|
+
case 'ArrowRight':
|
|
367
|
+
if (index === footerIdx) next = userIdx;
|
|
368
|
+
else return;
|
|
369
|
+
break;
|
|
370
|
+
case 'ArrowLeft':
|
|
371
|
+
if (index === userIdx) next = footerIdx;
|
|
372
|
+
else return;
|
|
373
|
+
break;
|
|
374
|
+
case 'Home':
|
|
375
|
+
e.preventDefault();
|
|
376
|
+
next = 0;
|
|
377
|
+
break;
|
|
378
|
+
case 'End':
|
|
379
|
+
e.preventDefault();
|
|
380
|
+
next = userIdx;
|
|
381
|
+
break;
|
|
382
|
+
default:
|
|
383
|
+
return;
|
|
305
384
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
);
|
|
310
|
-
items[next]?.focus();
|
|
385
|
+
|
|
386
|
+
if (next === undefined || next === index) return;
|
|
387
|
+
e.preventDefault();
|
|
388
|
+
this.focusRovingAt(next);
|
|
311
389
|
}
|
|
312
390
|
|
|
313
391
|
private checkScroll() {
|
|
@@ -331,6 +409,12 @@ export class PanelNav {
|
|
|
331
409
|
this.applyToggle(!this.collapsed);
|
|
332
410
|
}
|
|
333
411
|
|
|
412
|
+
/** Toggle expanded/collapsed panel nav — used by shell keyboard shortcuts. */
|
|
413
|
+
@Method()
|
|
414
|
+
async toggleCollapsed() {
|
|
415
|
+
this.handleToggle();
|
|
416
|
+
}
|
|
417
|
+
|
|
334
418
|
private handleFooterAction() {
|
|
335
419
|
this.dsNavFooterAction.emit();
|
|
336
420
|
}
|
|
@@ -411,6 +495,7 @@ export class PanelNav {
|
|
|
411
495
|
|
|
412
496
|
private renderNavItem(item: PanelNavItem, idx: number, collapsed: boolean) {
|
|
413
497
|
const isActive = item.id === this.effectiveActiveId;
|
|
498
|
+
const rovingPosition = idx + 1;
|
|
414
499
|
|
|
415
500
|
const itemContent = [
|
|
416
501
|
<span class="panel-nav__item-icon">
|
|
@@ -446,10 +531,10 @@ export class PanelNav {
|
|
|
446
531
|
},
|
|
447
532
|
'aria-current': isActive ? ('page' as const) : undefined,
|
|
448
533
|
title: collapsed ? item.label : undefined,
|
|
449
|
-
tabIndex:
|
|
534
|
+
tabIndex: rovingPosition === this.rovingIndex ? 0 : -1,
|
|
450
535
|
onClick: () => this.handleItemClick(item.id),
|
|
451
|
-
onKeyDown: (e: KeyboardEvent) => this.
|
|
452
|
-
onFocus: () => { this.rovingIndex =
|
|
536
|
+
onKeyDown: (e: KeyboardEvent) => this.handleRovingKeyDown(e, rovingPosition),
|
|
537
|
+
onFocus: () => { this.rovingIndex = rovingPosition; },
|
|
453
538
|
};
|
|
454
539
|
|
|
455
540
|
const useAnchor = this.routerMode === 'anchor' && item.href;
|
|
@@ -487,7 +572,10 @@ export class PanelNav {
|
|
|
487
572
|
<button
|
|
488
573
|
type="button"
|
|
489
574
|
class="panel-nav__header-btn ds-focus-ring-inset"
|
|
575
|
+
tabIndex={this.rovingIndex === 0 ? 0 : -1}
|
|
490
576
|
onClick={() => this.handleToggle()}
|
|
577
|
+
onKeyDown={(e: KeyboardEvent) => this.handleRovingKeyDown(e, 0)}
|
|
578
|
+
onFocus={() => { this.rovingIndex = 0; }}
|
|
491
579
|
aria-label={collapsed ? 'Expand navigation' : 'Collapse navigation'}
|
|
492
580
|
aria-expanded={collapsed ? 'false' : 'true'}
|
|
493
581
|
>
|
|
@@ -543,9 +631,12 @@ export class PanelNav {
|
|
|
543
631
|
class="panel-nav__footer-btn"
|
|
544
632
|
icon={isDashboardChrome ? 'Gear' : 'Dashboard'}
|
|
545
633
|
activeFill={false}
|
|
634
|
+
focusTabIndex={this.rovingIndex === this.getFooterRovingIndex() ? 0 : -1}
|
|
546
635
|
title={isDashboardChrome ? 'Settings' : 'Dashboard'}
|
|
547
636
|
aria-label={isDashboardChrome ? 'Open settings' : 'Go to dashboard'}
|
|
548
637
|
onDsClick={() => this.handleFooterAction()}
|
|
638
|
+
onKeyDown={(e: KeyboardEvent) => this.handleRovingKeyDown(e, this.getFooterRovingIndex())}
|
|
639
|
+
onFocusin={() => { this.rovingIndex = this.getFooterRovingIndex(); }}
|
|
549
640
|
/>
|
|
550
641
|
|
|
551
642
|
{/* Right user — label fades like nav items; right icon cross-fades chevron ↔ circle+initial */}
|
|
@@ -553,8 +644,11 @@ export class PanelNav {
|
|
|
553
644
|
type="button"
|
|
554
645
|
id={PANEL_NAV_USER_MENU_ANCHOR_ID}
|
|
555
646
|
class="panel-nav__item panel-nav__footer-user ds-focus-ring-inset"
|
|
647
|
+
tabIndex={this.rovingIndex === this.getUserRovingIndex() ? 0 : -1}
|
|
556
648
|
aria-label={collapsed ? `User: ${userName}` : `User menu for ${userName}`}
|
|
557
649
|
onClick={(e) => this.handleUserAction(e)}
|
|
650
|
+
onKeyDown={(e: KeyboardEvent) => this.handleRovingKeyDown(e, this.getUserRovingIndex())}
|
|
651
|
+
onFocus={() => { this.rovingIndex = this.getUserRovingIndex(); }}
|
|
558
652
|
>
|
|
559
653
|
<span class="panel-nav__item-label panel-nav__footer-user-label">
|
|
560
654
|
<span class="panel-nav__item-label-text">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Component, Prop, Event, EventEmitter, Element, State, Watch, h, Host } from '@stencil/core';
|
|
1
|
+
import { Component, Prop, Event, EventEmitter, Element, State, Watch, Method, h, Host } from '@stencil/core';
|
|
2
2
|
import type { ChromeTransitionDetail } from '../../nav/chrome-transition';
|
|
3
3
|
import {
|
|
4
4
|
PANEL_TOOLS_LABELS,
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type PanelToolsItem,
|
|
7
7
|
type PanelToolsToolId,
|
|
8
8
|
} from './panel-tools-types';
|
|
9
|
-
import { parsePanelToolsItems, panelToolsDrawerResting } from './panel-tools-utils';
|
|
9
|
+
import { parsePanelToolsItems, panelToolsDrawerResting, resolvePanelToolActivation } from './panel-tools-utils';
|
|
10
10
|
|
|
11
11
|
@Component({
|
|
12
12
|
tag: 'ds-panel-tools',
|
|
@@ -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
|
|
|
@@ -132,17 +142,68 @@ export class PanelTools {
|
|
|
132
142
|
}
|
|
133
143
|
|
|
134
144
|
private handleToolChange = (id: PanelToolsToolId) => {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
const next = resolvePanelToolActivation(this.open, this.activeTool, id);
|
|
146
|
+
this.open = next.open;
|
|
147
|
+
this.activeTool = next.activeTool;
|
|
148
|
+
this.dsToolChange.emit({ id, selected: next.selected });
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/** Toggle any rail tool open/closed — shell shortcuts ⌘/Ctrl+K/A/S/M/N call this. */
|
|
152
|
+
@Method()
|
|
153
|
+
async activateTool(id: PanelToolsToolId) {
|
|
154
|
+
const item = this.railItems.find(entry => entry.id === id);
|
|
155
|
+
if (!item || item.inactive) return;
|
|
156
|
+
this.handleToolChange(id);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Close the tools drawer when open — used by shell keyboard shortcuts. */
|
|
160
|
+
@Method()
|
|
161
|
+
async closeDrawer() {
|
|
162
|
+
if (!this.open) return;
|
|
163
|
+
const id = this.activeTool;
|
|
164
|
+
this.open = false;
|
|
165
|
+
if (id) {
|
|
166
|
+
this.dsToolChange.emit({ id, selected: false });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private focusRailAt(index: number) {
|
|
171
|
+
const items = this.orderedRailItems;
|
|
172
|
+
if (!items.length) return;
|
|
173
|
+
const bounded = Math.max(0, Math.min(index, items.length - 1));
|
|
174
|
+
if (bounded === this.rovingIndex) return;
|
|
175
|
+
this.rovingIndex = bounded;
|
|
176
|
+
const actions = Array.from(
|
|
177
|
+
this.el.querySelectorAll<HTMLElement>('.panel-tools__rail-action .button-icon'),
|
|
178
|
+
);
|
|
179
|
+
actions[bounded]?.focus({ preventScroll: true });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private handleRailKeyDown = (e: KeyboardEvent, index: number) => {
|
|
183
|
+
const items = this.orderedRailItems;
|
|
184
|
+
if (!items.length) return;
|
|
185
|
+
|
|
186
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
this.handleToolChange(items[index].id);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (e.key === 'ArrowDown') {
|
|
193
|
+
if (index >= items.length - 1) return;
|
|
194
|
+
e.preventDefault();
|
|
195
|
+
this.focusRailAt(index + 1);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (e.key === 'ArrowUp') {
|
|
200
|
+
if (index <= 0) return;
|
|
201
|
+
e.preventDefault();
|
|
202
|
+
this.focusRailAt(index - 1);
|
|
141
203
|
}
|
|
142
|
-
this.dsToolChange.emit({ id, selected });
|
|
143
204
|
};
|
|
144
205
|
|
|
145
|
-
private renderRailAction(item: PanelToolsItem) {
|
|
206
|
+
private renderRailAction(item: PanelToolsItem, index: number) {
|
|
146
207
|
return (
|
|
147
208
|
<ds-button-unfilled-icon
|
|
148
209
|
key={item.id}
|
|
@@ -152,18 +213,21 @@ export class PanelTools {
|
|
|
152
213
|
activeFill={false}
|
|
153
214
|
dot={item.dot ?? false}
|
|
154
215
|
inactive={item.inactive}
|
|
216
|
+
focusTabIndex={index === this.rovingIndex ? 0 : -1}
|
|
155
217
|
aria-label={item.ariaLabel ?? PANEL_TOOLS_LABELS[item.id]}
|
|
156
218
|
pressed={this.isRailSelected(item.id)}
|
|
157
|
-
|
|
219
|
+
onFocusin={() => { this.rovingIndex = index; }}
|
|
220
|
+
onKeyDown={(e: KeyboardEvent) => this.handleRailKeyDown(e, index)}
|
|
221
|
+
onDsClick={() => this.handleToolChange(item.id)}
|
|
158
222
|
/>
|
|
159
223
|
);
|
|
160
224
|
}
|
|
161
225
|
|
|
162
226
|
render() {
|
|
163
227
|
const headerLabel = this.headerLabel();
|
|
164
|
-
const
|
|
165
|
-
const headerItem =
|
|
166
|
-
const bodyItems =
|
|
228
|
+
const orderedRailItems = this.orderedRailItems;
|
|
229
|
+
const headerItem = orderedRailItems.find(item => item.id === PANEL_TOOLS_PRIMARY_TOOL_ID);
|
|
230
|
+
const bodyItems = orderedRailItems.filter(item => item.id !== PANEL_TOOLS_PRIMARY_TOOL_ID);
|
|
167
231
|
const showDrawerChrome = this.isDrawerPresent();
|
|
168
232
|
const drawerResting = panelToolsDrawerResting(this.open, this.motion);
|
|
169
233
|
|
|
@@ -181,6 +245,19 @@ export class PanelTools {
|
|
|
181
245
|
aria-label="Tools"
|
|
182
246
|
>
|
|
183
247
|
<div class="panel-tools__layout">
|
|
248
|
+
<nav class="panel-tools__rail" aria-label="Tool shortcuts">
|
|
249
|
+
{headerItem ? (
|
|
250
|
+
<div class="panel-tools__rail-header">
|
|
251
|
+
{this.renderRailAction(headerItem, 0)}
|
|
252
|
+
</div>
|
|
253
|
+
) : null}
|
|
254
|
+
<div class="panel-tools__rail-body">
|
|
255
|
+
{bodyItems.map((item, bodyIdx) =>
|
|
256
|
+
this.renderRailAction(item, headerItem ? bodyIdx + 1 : bodyIdx),
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
</nav>
|
|
260
|
+
|
|
184
261
|
<div
|
|
185
262
|
class={{
|
|
186
263
|
'panel-tools__drawer': true,
|
|
@@ -247,15 +324,6 @@ export class PanelTools {
|
|
|
247
324
|
</div>
|
|
248
325
|
</div>
|
|
249
326
|
</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
327
|
</div>
|
|
260
328
|
</Host>
|
|
261
329
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseJsonArrayProp } from '../BarNav/bar-nav-utils';
|
|
2
|
-
import type { PanelToolsItem } from './panel-tools-types';
|
|
2
|
+
import type { PanelToolsItem, PanelToolsToolId } from './panel-tools-types';
|
|
3
3
|
|
|
4
4
|
export function parsePanelToolsItems(
|
|
5
5
|
items: PanelToolsItem[],
|
|
@@ -36,3 +36,15 @@ export type PanelToolsMotion = 'opening' | 'closing' | 'idle';
|
|
|
36
36
|
export function panelToolsDrawerResting(open: boolean, motion: PanelToolsMotion): boolean {
|
|
37
37
|
return !open && motion === 'idle';
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
/** Toggle or switch rail tool selection — repeat activation closes the active tool. */
|
|
41
|
+
export function resolvePanelToolActivation(
|
|
42
|
+
open: boolean,
|
|
43
|
+
activeTool: PanelToolsToolId | '',
|
|
44
|
+
id: PanelToolsToolId,
|
|
45
|
+
): { open: boolean; activeTool: PanelToolsToolId; selected: boolean } {
|
|
46
|
+
if (open && activeTool === id) {
|
|
47
|
+
return { open: false, activeTool: id, selected: false };
|
|
48
|
+
}
|
|
49
|
+
return { open: true, activeTool: id, selected: true };
|
|
50
|
+
}
|