@ds-mo/ui 2.1.1 → 2.2.1
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/README.md +1 -1
- package/dist/.build-stamp +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-breadcrumb.js +1 -1
- package/dist/components/ds-button-group.js +1 -1
- package/dist/components/ds-button-group.js.map +1 -1
- package/dist/components/ds-button-unfilled-icon.d.ts +11 -0
- package/dist/components/ds-button-unfilled-icon.js +2 -0
- package/dist/components/ds-button.js +1 -1
- package/dist/components/ds-checkbox.js +1 -1
- package/dist/components/ds-field.js +1 -1
- package/dist/components/ds-menu.js +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-select.js +1 -1
- package/dist/components/ds-slider.js +1 -1
- package/dist/components/ds-tab-group-nav.js +1 -1
- package/dist/components/ds-tab-group.js +1 -1
- package/dist/components/ds-tab-group.js.map +1 -1
- package/dist/components/ds-toggle.js +1 -1
- package/dist/components/p-Bl-GTn9s.js +2 -0
- package/dist/components/p-Bl-GTn9s.js.map +1 -0
- package/dist/components/{p-CsO880L-.js → p-CD3yg7ue.js} +2 -2
- package/dist/components/p-CZpCmCiq.js +2 -0
- package/dist/components/p-CZpCmCiq.js.map +1 -0
- package/dist/components/p-YzIqPOqL.js +2 -0
- package/dist/components/p-YzIqPOqL.js.map +1 -0
- package/dist/types/components/BarNav/BarNav.d.ts +8 -7
- package/dist/types/components/BarNav/bar-nav-tabs-menu-utils.d.ts +3 -0
- package/dist/types/components/BarNav/bar-nav-types.d.ts +0 -11
- package/dist/types/components/BarNav/index.d.ts +1 -1
- package/dist/types/components/ButtonUnfilledIcon/ButtonUnfilledIcon.d.ts +33 -0
- package/dist/types/components/ButtonUnfilledIcon/index.d.ts +2 -0
- package/dist/types/components/Menu/Menu.d.ts +3 -0
- package/dist/types/components.d.ts +154 -98
- package/package.json +1 -1
- package/src/angular/index.ts +1 -1
- package/src/angular/proxies.ts +34 -32
- package/src/react/components.ts +1 -1
- package/src/react/ds-button-unfilled-icon.ts +33 -0
- package/src/wc/components/BarNav/BarNav.tsx +126 -133
- package/src/wc/components/BarNav/bar-nav-tabs-menu-utils.ts +55 -0
- package/src/wc/components/BarNav/bar-nav-types.ts +0 -12
- package/src/wc/components/BarNav/index.ts +1 -1
- package/src/wc/components/ButtonUnfilledIcon/ButtonUnfilledIcon.tsx +114 -0
- package/src/wc/components/ButtonUnfilledIcon/index.ts +1 -0
- package/src/wc/components/Menu/Menu.tsx +18 -7
- package/src/wc/components/PanelNav/PanelNav.tsx +12 -9
- package/src/wc/components/PanelTools/PanelTools.tsx +4 -2
- package/src/wc/components/TabGroup/TabGroup.tsx +1 -0
- package/src/wc/components/TabGroupNav/TabGroupNav.tsx +1 -0
- package/src/wc/components.d.ts +154 -98
- package/src/wc/utils/focus-ring.css +33 -0
- package/dist/components/ds-bar-nav-action.d.ts +0 -11
- package/dist/components/ds-bar-nav-action.js +0 -2
- package/dist/components/p-CCZFMcLe.js +0 -2
- package/dist/components/p-CCZFMcLe.js.map +0 -1
- package/dist/components/p-CuuuDjJJ.js +0 -2
- package/dist/components/p-CuuuDjJJ.js.map +0 -1
- package/dist/components/p-DlsexsFg.js +0 -2
- package/dist/components/p-DlsexsFg.js.map +0 -1
- package/dist/types/components/BarNavAction/BarNavAction.d.ts +0 -21
- package/dist/types/components/BarNavAction/index.d.ts +0 -2
- package/src/react/ds-bar-nav-action.ts +0 -27
- package/src/wc/components/BarNavAction/BarNavAction.tsx +0 -78
- package/src/wc/components/BarNavAction/index.ts +0 -1
- /package/dist/components/{ds-bar-nav-action.js.map → ds-button-unfilled-icon.js.map} +0 -0
- /package/dist/components/{p-CsO880L-.js.map → p-CD3yg7ue.js.map} +0 -0
package/package.json
CHANGED
package/src/angular/index.ts
CHANGED
package/src/angular/proxies.ts
CHANGED
|
@@ -139,36 +139,6 @@ export declare interface DsBarNav extends Components.DsBarNav {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
|
|
142
|
-
@ProxyCmp({
|
|
143
|
-
inputs: ['ariaLabel', 'background', 'dot', 'icon', 'inactive', 'selected']
|
|
144
|
-
})
|
|
145
|
-
@Component({
|
|
146
|
-
selector: 'ds-bar-nav-action',
|
|
147
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
148
|
-
template: '<ng-content></ng-content>',
|
|
149
|
-
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
|
150
|
-
inputs: ['ariaLabel', 'background', 'dot', 'icon', 'inactive', 'selected'],
|
|
151
|
-
outputs: ['dsChange'],
|
|
152
|
-
standalone: false
|
|
153
|
-
})
|
|
154
|
-
export class DsBarNavAction {
|
|
155
|
-
protected el: HTMLDsBarNavActionElement;
|
|
156
|
-
@Output() dsChange = new EventEmitter<CustomEvent<boolean>>();
|
|
157
|
-
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
|
158
|
-
c.detach();
|
|
159
|
-
this.el = r.nativeElement;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
export declare interface DsBarNavAction extends Components.DsBarNavAction {
|
|
165
|
-
/**
|
|
166
|
-
* Emits the new selected value (!selected) on click.
|
|
167
|
-
*/
|
|
168
|
-
dsChange: EventEmitter<CustomEvent<boolean>>;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
142
|
@ProxyCmp({
|
|
173
143
|
inputs: ['items', 'separator']
|
|
174
144
|
})
|
|
@@ -263,6 +233,38 @@ export declare interface DsButtonGroup extends Components.DsButtonGroup {
|
|
|
263
233
|
}
|
|
264
234
|
|
|
265
235
|
|
|
236
|
+
@ProxyCmp({
|
|
237
|
+
inputs: ['activeFill', 'ariaLabel', 'background', 'controls', 'dot', 'expanded', 'hasBorder', 'haspopup', 'icon', 'inactive', 'isActive', 'pressed', 'type'],
|
|
238
|
+
methods: ['setFocus']
|
|
239
|
+
})
|
|
240
|
+
@Component({
|
|
241
|
+
selector: 'ds-button-unfilled-icon',
|
|
242
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
243
|
+
template: '<ng-content></ng-content>',
|
|
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'],
|
|
246
|
+
outputs: ['dsClick', 'dsChange'],
|
|
247
|
+
standalone: false
|
|
248
|
+
})
|
|
249
|
+
export class DsButtonUnfilledIcon {
|
|
250
|
+
protected el: HTMLDsButtonUnfilledIconElement;
|
|
251
|
+
@Output() dsClick = new EventEmitter<CustomEvent<MouseEvent>>();
|
|
252
|
+
@Output() dsChange = new EventEmitter<CustomEvent<boolean>>();
|
|
253
|
+
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
|
254
|
+
c.detach();
|
|
255
|
+
this.el = r.nativeElement;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
export declare interface DsButtonUnfilledIcon extends Components.DsButtonUnfilledIcon {
|
|
261
|
+
|
|
262
|
+
dsClick: EventEmitter<CustomEvent<MouseEvent>>;
|
|
263
|
+
|
|
264
|
+
dsChange: EventEmitter<CustomEvent<boolean>>;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
|
|
266
268
|
@ProxyCmp({
|
|
267
269
|
inputs: ['elevation', 'radius']
|
|
268
270
|
})
|
|
@@ -548,14 +550,14 @@ export declare interface DsLoader extends Components.DsLoader {}
|
|
|
548
550
|
|
|
549
551
|
|
|
550
552
|
@ProxyCmp({
|
|
551
|
-
inputs: ['align', 'alignOffset', 'anchor', 'anchorId', 'items', 'menuWidth', 'minWidth', 'open', 'sections', 'side', 'sideOffset']
|
|
553
|
+
inputs: ['align', 'alignOffset', 'anchor', 'anchorId', 'initialFocusVisible', 'items', 'menuWidth', 'minWidth', 'open', 'sections', 'side', 'sideOffset']
|
|
552
554
|
})
|
|
553
555
|
@Component({
|
|
554
556
|
selector: 'ds-menu',
|
|
555
557
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
556
558
|
template: '<ng-content></ng-content>',
|
|
557
559
|
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
|
558
|
-
inputs: ['align', 'alignOffset', 'anchor', 'anchorId', 'items', 'menuWidth', 'minWidth', 'open', 'sections', 'side', 'sideOffset'],
|
|
560
|
+
inputs: ['align', 'alignOffset', 'anchor', 'anchorId', 'initialFocusVisible', 'items', 'menuWidth', 'minWidth', 'open', 'sections', 'side', 'sideOffset'],
|
|
559
561
|
outputs: ['dsClose', 'dsSelect'],
|
|
560
562
|
standalone: false
|
|
561
563
|
})
|
package/src/react/components.ts
CHANGED
|
@@ -8,10 +8,10 @@ export { DsAccordion } from "./ds-accordion.js";
|
|
|
8
8
|
export { DsAppShell } from "./ds-app-shell.js";
|
|
9
9
|
export { DsBadge } from "./ds-badge.js";
|
|
10
10
|
export { DsBanner } from "./ds-banner.js";
|
|
11
|
-
export { DsBarNavAction } from "./ds-bar-nav-action.js";
|
|
12
11
|
export { DsBarNav } from "./ds-bar-nav.js";
|
|
13
12
|
export { DsBreadcrumb } from "./ds-breadcrumb.js";
|
|
14
13
|
export { DsButtonGroup } from "./ds-button-group.js";
|
|
14
|
+
export { DsButtonUnfilledIcon } from "./ds-button-unfilled-icon.js";
|
|
15
15
|
export { DsButton } from "./ds-button.js";
|
|
16
16
|
export { DsCard } from "./ds-card.js";
|
|
17
17
|
export { DsCheckbox } from "./ds-checkbox.js";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This file was automatically generated by the Stencil React Output Target.
|
|
5
|
+
* Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/* eslint-disable */
|
|
9
|
+
|
|
10
|
+
import type { EventName, StencilReactComponent } from '@stencil/react-output-target/runtime';
|
|
11
|
+
import { createComponent } from '@stencil/react-output-target/runtime';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
|
|
14
|
+
import { type DsButtonUnfilledIconCustomEvent } from "@ds-mo/ui";
|
|
15
|
+
import type { Components } from "@ds-mo/ui/dist/components";
|
|
16
|
+
import { DsButtonUnfilledIcon as DsButtonUnfilledIconElement, defineCustomElement as defineDsButtonUnfilledIcon } from "@ds-mo/ui/dist/components/ds-button-unfilled-icon.js";
|
|
17
|
+
|
|
18
|
+
export type DsButtonUnfilledIconEvents = {
|
|
19
|
+
onDsClick: EventName<DsButtonUnfilledIconCustomEvent<MouseEvent>>,
|
|
20
|
+
onDsChange: EventName<DsButtonUnfilledIconCustomEvent<boolean>>
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const DsButtonUnfilledIcon: StencilReactComponent<DsButtonUnfilledIconElement, DsButtonUnfilledIconEvents, Components.DsButtonUnfilledIcon> = /*@__PURE__*/ createComponent<DsButtonUnfilledIconElement, DsButtonUnfilledIconEvents, Components.DsButtonUnfilledIcon>({
|
|
24
|
+
tagName: 'ds-button-unfilled-icon',
|
|
25
|
+
elementClass: DsButtonUnfilledIconElement,
|
|
26
|
+
// @ts-ignore - ignore potential React type mismatches between the Stencil Output Target and your project.
|
|
27
|
+
react: React,
|
|
28
|
+
events: {
|
|
29
|
+
onDsClick: 'dsClick',
|
|
30
|
+
onDsChange: 'dsChange'
|
|
31
|
+
} as DsButtonUnfilledIconEvents,
|
|
32
|
+
defineCustomElement: defineDsButtonUnfilledIcon
|
|
33
|
+
});
|
|
@@ -24,10 +24,10 @@ import {
|
|
|
24
24
|
type QueryableHost,
|
|
25
25
|
} from './bar-nav-dom-utils';
|
|
26
26
|
import {
|
|
27
|
-
getActiveTab,
|
|
28
|
-
getActiveTabLabel,
|
|
29
27
|
tabsOverflowContainer,
|
|
30
|
-
|
|
28
|
+
tabsToOverflowMenuSections,
|
|
29
|
+
trimTrailingDividers,
|
|
30
|
+
visibleTabCountForWidth,
|
|
31
31
|
} from './bar-nav-tabs-menu-utils';
|
|
32
32
|
import {
|
|
33
33
|
CHROME_TRANSITION_END,
|
|
@@ -83,7 +83,9 @@ export class BarNav {
|
|
|
83
83
|
@State() private menuOpen = false;
|
|
84
84
|
/** When false, defer showing expanded tabs or the collapsed trigger until intrinsic width is measured (avoids a one-frame flash of the full tab row on narrow viewports). */
|
|
85
85
|
@State() private tabLayoutCommitted = false;
|
|
86
|
-
@State() private
|
|
86
|
+
@State() private visibleTabs: BarNavTab[] = [];
|
|
87
|
+
@State() private overflowTabs: BarNavTab[] = [];
|
|
88
|
+
@State() private menuInitialFocusVisible = false;
|
|
87
89
|
|
|
88
90
|
private static readonly HOST_PROP_SYNC_BUDGET = 8;
|
|
89
91
|
private static readonly INTRINSIC_WIDTH_RETRY_MAX = 3;
|
|
@@ -91,9 +93,7 @@ export class BarNav {
|
|
|
91
93
|
private static readonly OVERFLOW_HYSTERESIS_PX = 8;
|
|
92
94
|
|
|
93
95
|
private headerEl: HTMLElement | null = null;
|
|
94
|
-
private triggerEl:
|
|
95
|
-
private triggerLabelTextEl: HTMLElement | null = null;
|
|
96
|
-
private triggerLabelObserver: ResizeObserver | null = null;
|
|
96
|
+
private triggerEl: (HTMLElement & { setFocus?: () => Promise<void> }) | null = null;
|
|
97
97
|
private menuEl: HTMLDsMenuElement | null = null;
|
|
98
98
|
private visibleTabGroupEl: QueryableHost | null = null;
|
|
99
99
|
private probeTabGroupEl: QueryableHost | null = null;
|
|
@@ -104,7 +104,6 @@ export class BarNav {
|
|
|
104
104
|
private readonly panelToolsTransition = new ChromeTransitionDepth();
|
|
105
105
|
private readonly overflowCoalescer = createRafCoalescer(() => {
|
|
106
106
|
this.updateTabsCollapsed();
|
|
107
|
-
this.updateTriggerLabelTruncation();
|
|
108
107
|
});
|
|
109
108
|
private chromeTransitionShell: HTMLElement | null = null;
|
|
110
109
|
/** Matches `basePath` once tabs + URL are reconciled for the active section. */
|
|
@@ -119,12 +118,8 @@ export class BarNav {
|
|
|
119
118
|
return this.value;
|
|
120
119
|
}
|
|
121
120
|
|
|
122
|
-
private get
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
private get activeTabHasDot(): boolean {
|
|
127
|
-
return !!getActiveTab(this.resolvedTabs, this.effectiveValue)?.dot;
|
|
121
|
+
private get hasOverflowTabs(): boolean {
|
|
122
|
+
return this.tabLayoutCommitted && this.tabsCollapsed && this.overflowTabs.length > 0;
|
|
128
123
|
}
|
|
129
124
|
|
|
130
125
|
private digestTabsConfig(tabs: BarNavTab[]): string {
|
|
@@ -135,8 +130,11 @@ export class BarNav {
|
|
|
135
130
|
this.tabLayoutCommitted = false;
|
|
136
131
|
this.tabsCollapsed = false;
|
|
137
132
|
this.menuOpen = false;
|
|
133
|
+
this.menuInitialFocusVisible = false;
|
|
138
134
|
this.intrinsicWidthRetryCount = 0;
|
|
139
135
|
this.tabLayoutPendingFrames = 0;
|
|
136
|
+
this.visibleTabs = [];
|
|
137
|
+
this.overflowTabs = [];
|
|
140
138
|
}
|
|
141
139
|
|
|
142
140
|
@Watch('tabs')
|
|
@@ -165,15 +163,13 @@ export class BarNav {
|
|
|
165
163
|
onTabsCollapsedChange(collapsed: boolean, prevCollapsed: boolean | undefined) {
|
|
166
164
|
if (!collapsed) {
|
|
167
165
|
this.menuOpen = false;
|
|
166
|
+
this.menuInitialFocusVisible = false;
|
|
168
167
|
this.menuEl = null;
|
|
169
|
-
this.triggerLabelTruncated = false;
|
|
170
|
-
this.syncTriggerLabelObserver();
|
|
171
168
|
}
|
|
172
169
|
this.scheduleOverflowCheck();
|
|
173
170
|
if (collapsed) {
|
|
174
171
|
requestAnimationFrame(() => {
|
|
175
172
|
this.scheduleOverflowCheck();
|
|
176
|
-
this.syncTriggerLabelObserver();
|
|
177
173
|
});
|
|
178
174
|
}
|
|
179
175
|
if (collapsed || prevCollapsed === undefined || prevCollapsed === collapsed) return;
|
|
@@ -200,16 +196,12 @@ export class BarNav {
|
|
|
200
196
|
}
|
|
201
197
|
this.scheduleOverflowCheck();
|
|
202
198
|
this.syncMenuAnchor();
|
|
203
|
-
if (this.tabsCollapsed) {
|
|
204
|
-
this.updateTriggerLabelTruncation();
|
|
205
|
-
}
|
|
206
199
|
}
|
|
207
200
|
|
|
208
201
|
disconnectedCallback() {
|
|
209
202
|
this.resizeObserver?.disconnect();
|
|
210
203
|
this.resizeObserver = null;
|
|
211
204
|
this.overflowCoalescer.cancel();
|
|
212
|
-
this.syncTriggerLabelObserver(true);
|
|
213
205
|
this.unbindChromeTransitionListeners();
|
|
214
206
|
}
|
|
215
207
|
|
|
@@ -303,40 +295,6 @@ export class BarNav {
|
|
|
303
295
|
this.overflowCoalescer.schedule();
|
|
304
296
|
}
|
|
305
297
|
|
|
306
|
-
private syncTriggerLabelObserver(disconnectOnly = false) {
|
|
307
|
-
this.triggerLabelObserver?.disconnect();
|
|
308
|
-
this.triggerLabelObserver = null;
|
|
309
|
-
|
|
310
|
-
if (disconnectOnly || !this.tabsCollapsed || !this.triggerLabelTextEl) {
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
this.updateTriggerLabelTruncation();
|
|
315
|
-
|
|
316
|
-
if (typeof ResizeObserver === 'undefined') return;
|
|
317
|
-
|
|
318
|
-
this.triggerLabelObserver = new ResizeObserver(() => this.updateTriggerLabelTruncation());
|
|
319
|
-
this.triggerLabelObserver.observe(this.triggerLabelTextEl);
|
|
320
|
-
if (this.triggerEl) {
|
|
321
|
-
this.triggerLabelObserver.observe(this.triggerEl);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
private updateTriggerLabelTruncation() {
|
|
326
|
-
const el = this.triggerLabelTextEl;
|
|
327
|
-
if (!el || !this.tabsCollapsed) {
|
|
328
|
-
if (this.triggerLabelTruncated) {
|
|
329
|
-
this.triggerLabelTruncated = false;
|
|
330
|
-
}
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const truncated = el.scrollWidth > el.clientWidth + 1;
|
|
335
|
-
if (truncated !== this.triggerLabelTruncated) {
|
|
336
|
-
this.triggerLabelTruncated = truncated;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
298
|
/** Width available for the tab row or collapsed trigger. */
|
|
341
299
|
private getLeftZoneAvailableWidth(): number {
|
|
342
300
|
if (!this.headerEl) return 0;
|
|
@@ -353,6 +311,63 @@ export class BarNav {
|
|
|
353
311
|
return tabList?.scrollWidth ?? 0;
|
|
354
312
|
}
|
|
355
313
|
|
|
314
|
+
private getHeaderGap(): number {
|
|
315
|
+
if (!this.headerEl) return 0;
|
|
316
|
+
const style = getComputedStyle(this.headerEl);
|
|
317
|
+
return parseFloat(style.columnGap || style.gap) || 0;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private getOverflowTriggerReserveWidth(): number {
|
|
321
|
+
if (!this.triggerEl) {
|
|
322
|
+
return 40;
|
|
323
|
+
}
|
|
324
|
+
return this.triggerEl.getBoundingClientRect().width + this.getHeaderGap();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private getProbeTabMetrics():
|
|
328
|
+
| {
|
|
329
|
+
itemWidths: number[];
|
|
330
|
+
itemGap: number;
|
|
331
|
+
listExtraWidth: number;
|
|
332
|
+
}
|
|
333
|
+
| null {
|
|
334
|
+
const tabList = getTabListFromTabGroup(this.probeTabGroupEl);
|
|
335
|
+
if (!tabList) return null;
|
|
336
|
+
|
|
337
|
+
const children = Array.from(tabList.children) as HTMLElement[];
|
|
338
|
+
const itemWidths = children.map(child => child.getBoundingClientRect().width);
|
|
339
|
+
if (itemWidths.length === 0 || itemWidths.some(width => width === 0)) return null;
|
|
340
|
+
|
|
341
|
+
const style = getComputedStyle(tabList);
|
|
342
|
+
const itemGap = parseFloat(style.columnGap || style.gap) || 0;
|
|
343
|
+
const childrenWidth = itemWidths.reduce((sum, width) => sum + width, 0);
|
|
344
|
+
const gapWidth = itemGap * Math.max(0, itemWidths.length - 1);
|
|
345
|
+
const listExtraWidth = Math.max(0, tabList.scrollWidth - childrenWidth - gapWidth);
|
|
346
|
+
|
|
347
|
+
return { itemWidths, itemGap, listExtraWidth };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private setOverflowTabLayout(visibleTabs: BarNavTab[], overflowTabs: BarNavTab[]) {
|
|
351
|
+
const nextCollapsed = overflowTabs.length > 0;
|
|
352
|
+
const visibleChanged =
|
|
353
|
+
this.digestTabsConfig(visibleTabs) !== this.digestTabsConfig(this.visibleTabs);
|
|
354
|
+
const overflowChanged =
|
|
355
|
+
this.digestTabsConfig(overflowTabs) !== this.digestTabsConfig(this.overflowTabs);
|
|
356
|
+
|
|
357
|
+
if (visibleChanged) {
|
|
358
|
+
this.visibleTabs = visibleTabs;
|
|
359
|
+
}
|
|
360
|
+
if (overflowChanged) {
|
|
361
|
+
this.overflowTabs = overflowTabs;
|
|
362
|
+
}
|
|
363
|
+
if (nextCollapsed !== this.tabsCollapsed) {
|
|
364
|
+
this.tabsCollapsed = nextCollapsed;
|
|
365
|
+
if (!nextCollapsed) {
|
|
366
|
+
this.menuOpen = false;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
356
371
|
private scheduleIntrinsicWidthRetry() {
|
|
357
372
|
if (this.intrinsicWidthRetryCount >= BarNav.INTRINSIC_WIDTH_RETRY_MAX) return;
|
|
358
373
|
this.intrinsicWidthRetryCount++;
|
|
@@ -362,18 +377,17 @@ export class BarNav {
|
|
|
362
377
|
private forceTabLayoutCommit(fallbackCollapsed: boolean) {
|
|
363
378
|
this.intrinsicWidthRetryCount = 0;
|
|
364
379
|
this.tabLayoutPendingFrames = 0;
|
|
365
|
-
this.
|
|
366
|
-
|
|
367
|
-
this.
|
|
368
|
-
|
|
380
|
+
this.setOverflowTabLayout(
|
|
381
|
+
fallbackCollapsed ? [] : this.resolvedTabs,
|
|
382
|
+
fallbackCollapsed ? this.resolvedTabs : [],
|
|
383
|
+
);
|
|
369
384
|
this.tabLayoutCommitted = true;
|
|
370
385
|
}
|
|
371
386
|
|
|
372
387
|
private updateTabsCollapsed() {
|
|
373
388
|
if (!this.headerEl || this.resolvedTabs.length === 0 || this.hideTabsForDetailRoute) {
|
|
374
389
|
if (this.tabsCollapsed) {
|
|
375
|
-
this.
|
|
376
|
-
this.menuOpen = false;
|
|
390
|
+
this.setOverflowTabLayout([], []);
|
|
377
391
|
}
|
|
378
392
|
this.intrinsicWidthRetryCount = 0;
|
|
379
393
|
this.tabLayoutPendingFrames = 0;
|
|
@@ -381,8 +395,9 @@ export class BarNav {
|
|
|
381
395
|
return;
|
|
382
396
|
}
|
|
383
397
|
|
|
398
|
+
const metrics = this.getProbeTabMetrics();
|
|
384
399
|
const intrinsicWidth = this.getTabsIntrinsicWidth();
|
|
385
|
-
if (intrinsicWidth === 0) {
|
|
400
|
+
if (!metrics || intrinsicWidth === 0) {
|
|
386
401
|
this.tabLayoutPendingFrames += 1;
|
|
387
402
|
if (this.intrinsicWidthRetryCount >= BarNav.INTRINSIC_WIDTH_RETRY_MAX) {
|
|
388
403
|
this.forceTabLayoutCommit(false);
|
|
@@ -403,18 +418,27 @@ export class BarNav {
|
|
|
403
418
|
BarNav.OVERFLOW_HYSTERESIS_PX,
|
|
404
419
|
);
|
|
405
420
|
|
|
406
|
-
if (shouldCollapse
|
|
407
|
-
this.
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}
|
|
421
|
+
if (!shouldCollapse) {
|
|
422
|
+
this.setOverflowTabLayout(this.resolvedTabs, []);
|
|
423
|
+
this.tabLayoutCommitted = true;
|
|
424
|
+
return;
|
|
411
425
|
}
|
|
412
426
|
|
|
427
|
+
const visibleCount = visibleTabCountForWidth(
|
|
428
|
+
metrics.itemWidths,
|
|
429
|
+
this.getLeftZoneAvailableWidth(),
|
|
430
|
+
this.getOverflowTriggerReserveWidth(),
|
|
431
|
+
metrics.itemGap,
|
|
432
|
+
metrics.listExtraWidth,
|
|
433
|
+
);
|
|
434
|
+
const visibleTabs = trimTrailingDividers(this.resolvedTabs.slice(0, visibleCount));
|
|
435
|
+
const overflowTabs = this.resolvedTabs.slice(visibleTabs.length);
|
|
436
|
+
this.setOverflowTabLayout(visibleTabs, overflowTabs);
|
|
413
437
|
this.tabLayoutCommitted = true;
|
|
414
438
|
}
|
|
415
439
|
|
|
416
440
|
private syncMenuAnchor() {
|
|
417
|
-
if (!this.menuEl || !this.triggerEl || !this.
|
|
441
|
+
if (!this.menuEl || !this.triggerEl || !this.hasOverflowTabs) return;
|
|
418
442
|
this.menuEl.anchor = this.triggerEl;
|
|
419
443
|
this.syncMenuSections();
|
|
420
444
|
}
|
|
@@ -425,10 +449,12 @@ export class BarNav {
|
|
|
425
449
|
this.selectTab(String(detail.value));
|
|
426
450
|
}
|
|
427
451
|
this.menuOpen = false;
|
|
452
|
+
this.menuInitialFocusVisible = false;
|
|
428
453
|
}
|
|
429
454
|
|
|
430
455
|
private handleMenuClose() {
|
|
431
456
|
this.menuOpen = false;
|
|
457
|
+
this.menuInitialFocusVisible = false;
|
|
432
458
|
}
|
|
433
459
|
|
|
434
460
|
/** Batch tabs/basePath/currentUrl updates so URL derivation never runs with a mixed section. */
|
|
@@ -483,7 +509,7 @@ export class BarNav {
|
|
|
483
509
|
|
|
484
510
|
private syncMenuSections() {
|
|
485
511
|
if (!this.menuEl) return;
|
|
486
|
-
this.menuEl.sections =
|
|
512
|
+
this.menuEl.sections = tabsToOverflowMenuSections(this.overflowTabs, this.effectiveValue);
|
|
487
513
|
}
|
|
488
514
|
|
|
489
515
|
private syncValueFromUrl() {
|
|
@@ -528,28 +554,30 @@ export class BarNav {
|
|
|
528
554
|
this.selectTab((e as CustomEvent<string>).detail);
|
|
529
555
|
}
|
|
530
556
|
|
|
531
|
-
private toggleTabMenu() {
|
|
557
|
+
private toggleTabMenu(options?: { focusVisible?: boolean }) {
|
|
532
558
|
if (this.menuOpen) {
|
|
533
559
|
this.menuOpen = false;
|
|
560
|
+
this.menuInitialFocusVisible = false;
|
|
534
561
|
return;
|
|
535
562
|
}
|
|
536
563
|
|
|
537
564
|
if (this.menuEl && this.triggerEl) {
|
|
538
565
|
this.menuEl.anchor = this.triggerEl;
|
|
539
|
-
this.menuEl.minWidth =
|
|
566
|
+
this.menuEl.minWidth = '';
|
|
540
567
|
this.syncMenuSections();
|
|
541
568
|
}
|
|
542
569
|
|
|
570
|
+
this.menuInitialFocusVisible = options?.focusVisible ?? false;
|
|
543
571
|
this.menuOpen = true;
|
|
544
572
|
}
|
|
545
573
|
|
|
546
574
|
private handleTriggerKeyDown(e: KeyboardEvent) {
|
|
547
|
-
if (!this.
|
|
575
|
+
if (!this.hasOverflowTabs) return;
|
|
548
576
|
|
|
549
577
|
if (!this.menuOpen) {
|
|
550
578
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter' || e.key === ' ') {
|
|
551
579
|
e.preventDefault();
|
|
552
|
-
this.toggleTabMenu();
|
|
580
|
+
this.toggleTabMenu({ focusVisible: true });
|
|
553
581
|
}
|
|
554
582
|
return;
|
|
555
583
|
}
|
|
@@ -557,7 +585,8 @@ export class BarNav {
|
|
|
557
585
|
if (e.key === 'Escape') {
|
|
558
586
|
e.preventDefault();
|
|
559
587
|
this.menuOpen = false;
|
|
560
|
-
this.
|
|
588
|
+
this.menuInitialFocusVisible = false;
|
|
589
|
+
void this.triggerEl?.setFocus?.();
|
|
561
590
|
}
|
|
562
591
|
}
|
|
563
592
|
|
|
@@ -567,6 +596,7 @@ export class BarNav {
|
|
|
567
596
|
const hasTabs =
|
|
568
597
|
sectionReady && this.resolvedTabs.length > 0 && !this.hideTabsForDetailRoute;
|
|
569
598
|
const tabGroupKey = this.basePath || 'no-section';
|
|
599
|
+
const renderedTabs = this.hasOverflowTabs ? this.visibleTabs : this.resolvedTabs;
|
|
570
600
|
|
|
571
601
|
return (
|
|
572
602
|
<Host>
|
|
@@ -576,7 +606,7 @@ export class BarNav {
|
|
|
576
606
|
'bar-nav--dashboard': this.navStyle === 'dashboard',
|
|
577
607
|
'bar-nav--settings': this.navStyle === 'settings',
|
|
578
608
|
'bar-nav--tabs-collapsed':
|
|
579
|
-
hasTabs && this.
|
|
609
|
+
hasTabs && this.hasOverflowTabs,
|
|
580
610
|
}}
|
|
581
611
|
ref={el => {
|
|
582
612
|
this.headerEl = el as HTMLElement;
|
|
@@ -605,7 +635,7 @@ export class BarNav {
|
|
|
605
635
|
</div>
|
|
606
636
|
)}
|
|
607
637
|
|
|
608
|
-
{hasTabs && this.tabLayoutCommitted &&
|
|
638
|
+
{hasTabs && this.tabLayoutCommitted && renderedTabs.length > 0 && (
|
|
609
639
|
<div class="bar-nav__left">
|
|
610
640
|
<ds-tab-group-nav
|
|
611
641
|
key={`visible-${tabGroupKey}`}
|
|
@@ -613,7 +643,7 @@ export class BarNav {
|
|
|
613
643
|
ref={el => {
|
|
614
644
|
this.visibleTabGroupEl = el ?? null;
|
|
615
645
|
}}
|
|
616
|
-
tabs={
|
|
646
|
+
tabs={renderedTabs}
|
|
617
647
|
value={this.effectiveValue}
|
|
618
648
|
onDsChange={(e: Event) => this.handleTabChange(e)}
|
|
619
649
|
/>
|
|
@@ -628,76 +658,39 @@ export class BarNav {
|
|
|
628
658
|
<span class="bar-nav__heading text-body-medium-emphasis">{this.heading}</span>
|
|
629
659
|
)}
|
|
630
660
|
|
|
631
|
-
{hasTabs && this.
|
|
632
|
-
<button
|
|
633
|
-
type="button"
|
|
661
|
+
{hasTabs && this.hasOverflowTabs && (
|
|
662
|
+
<ds-button-unfilled-icon
|
|
634
663
|
class={{
|
|
635
|
-
'bar-
|
|
636
|
-
'bar-nav__tab-trigger--open': this.menuOpen,
|
|
664
|
+
'bar-nav__overflow-trigger': true,
|
|
637
665
|
}}
|
|
638
|
-
|
|
639
|
-
|
|
666
|
+
icon="Ellipses"
|
|
667
|
+
isActive={this.menuOpen}
|
|
668
|
+
activeFill={false}
|
|
669
|
+
ref={(el?: HTMLDsButtonUnfilledIconElement) => {
|
|
670
|
+
this.triggerEl = (el as (HTMLElement & { setFocus?: () => Promise<void> })) ?? null;
|
|
640
671
|
}}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
aria-label=
|
|
644
|
-
|
|
672
|
+
haspopup="menu"
|
|
673
|
+
expanded={this.menuOpen}
|
|
674
|
+
aria-label="More tabs"
|
|
675
|
+
onDsClick={() => this.toggleTabMenu({ focusVisible: false })}
|
|
645
676
|
onKeyDown={(e: KeyboardEvent) => this.handleTriggerKeyDown(e)}
|
|
646
|
-
|
|
647
|
-
<span
|
|
648
|
-
class={{
|
|
649
|
-
'bar-nav__tab-trigger-label': true,
|
|
650
|
-
'bar-nav__tab-trigger-label--dot': this.activeTabHasDot,
|
|
651
|
-
'bar-nav__tab-trigger-label--truncated': this.triggerLabelTruncated,
|
|
652
|
-
}}
|
|
653
|
-
>
|
|
654
|
-
<span
|
|
655
|
-
class={{
|
|
656
|
-
'bar-nav__tab-trigger-label-text': true,
|
|
657
|
-
'bar-nav__tab-trigger-label-text--truncated': this.triggerLabelTruncated,
|
|
658
|
-
'text-body-medium-emphasis': true,
|
|
659
|
-
}}
|
|
660
|
-
ref={el => {
|
|
661
|
-
const next = el ?? null;
|
|
662
|
-
if (next === this.triggerLabelTextEl) return;
|
|
663
|
-
this.triggerLabelTextEl = next;
|
|
664
|
-
if (this.tabsCollapsed) {
|
|
665
|
-
this.syncTriggerLabelObserver();
|
|
666
|
-
}
|
|
667
|
-
}}
|
|
668
|
-
>
|
|
669
|
-
{this.activeTabLabel}
|
|
670
|
-
</span>
|
|
671
|
-
{this.activeTabHasDot && (
|
|
672
|
-
<ds-badge
|
|
673
|
-
class="bar-nav__tab-trigger-dot"
|
|
674
|
-
variant="dot"
|
|
675
|
-
background="var(--_bar-nav-bg)"
|
|
676
|
-
label=""
|
|
677
|
-
aria-hidden="true"
|
|
678
|
-
/>
|
|
679
|
-
)}
|
|
680
|
-
</span>
|
|
681
|
-
<ds-icon
|
|
682
|
-
name="ChevronDown"
|
|
683
|
-
size="md"
|
|
684
|
-
color="inherit"
|
|
685
|
-
class="bar-nav__tab-trigger-chevron"
|
|
686
|
-
/>
|
|
687
|
-
</button>
|
|
677
|
+
/>
|
|
688
678
|
)}
|
|
689
679
|
|
|
690
680
|
</header>
|
|
691
681
|
|
|
692
|
-
{hasTabs && this.
|
|
682
|
+
{hasTabs && this.hasOverflowTabs && (
|
|
693
683
|
<ds-menu
|
|
694
684
|
ref={el => {
|
|
695
685
|
this.menuEl = (el as HTMLDsMenuElement) ?? null;
|
|
696
686
|
}}
|
|
697
687
|
open={this.menuOpen}
|
|
698
|
-
sections={
|
|
688
|
+
sections={tabsToOverflowMenuSections(this.overflowTabs, this.effectiveValue)}
|
|
699
689
|
side="bottom"
|
|
700
|
-
align="
|
|
690
|
+
align="end"
|
|
691
|
+
sideOffset="calc(var(--dimension-space-100) + var(--dimension-space-050))"
|
|
692
|
+
alignOffset="var(--dimension-space-050)"
|
|
693
|
+
initialFocusVisible={this.menuInitialFocusVisible}
|
|
701
694
|
onDsSelect={(e: Event) => this.handleMenuSelect(e)}
|
|
702
695
|
onDsClose={() => this.handleMenuClose()}
|
|
703
696
|
/>
|