@ds-mo/ui 1.0.2 → 1.1.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/components/ds-bar-nav-action.js +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.js +1 -1
- package/dist/components/ds-card.js +1 -1
- package/dist/components/ds-card.js.map +1 -1
- package/dist/components/ds-checkbox.js +1 -1
- package/dist/components/ds-divider.js +1 -1
- package/dist/components/ds-empty-state.js +1 -1
- package/dist/components/ds-empty-state.js.map +1 -1
- package/dist/components/ds-fade.js +1 -1
- package/dist/components/ds-field.js +1 -1
- package/dist/components/ds-header.js +1 -1
- package/dist/components/ds-header.js.map +1 -1
- package/dist/components/ds-icon.js +1 -1
- package/dist/components/ds-input.js +1 -1
- package/dist/components/ds-label-wrap.js +1 -1
- package/dist/components/ds-menu.js +1 -1
- package/dist/components/ds-modal.js +1 -1
- package/dist/components/ds-panel-nav.js +1 -1
- package/dist/components/ds-radio-group.js +1 -1
- package/dist/components/ds-radio-group.js.map +1 -1
- package/dist/components/ds-scrollbar.js +1 -1
- package/dist/components/ds-select.js +1 -1
- package/dist/components/ds-slider.js +1 -1
- package/dist/components/ds-slider.js.map +1 -1
- package/dist/components/ds-surface.js +1 -1
- package/dist/components/ds-tab-group-nav.d.ts +11 -0
- package/dist/components/ds-tab-group-nav.js +2 -0
- package/dist/components/ds-tab-group-nav.js.map +1 -0
- package/dist/components/ds-tab-group.js +1 -1
- package/dist/components/ds-tab-group.js.map +1 -1
- package/dist/components/ds-table.js +1 -1
- package/dist/components/ds-table.js.map +1 -1
- package/dist/components/ds-tag.js +1 -1
- package/dist/components/ds-text.js +1 -1
- package/dist/components/ds-toast-provider.js +1 -1
- package/dist/components/ds-toggle-button-group.js +1 -1
- package/dist/components/ds-toggle-button.js +1 -1
- package/dist/components/ds-toggle.js +1 -1
- package/dist/components/ds-tooltip.js +1 -1
- package/dist/components/p-BQq26pt9.js +2 -0
- package/dist/components/p-BQq26pt9.js.map +1 -0
- package/dist/components/p-CMyQsk0K.js +2 -0
- package/dist/components/p-CMyQsk0K.js.map +1 -0
- package/dist/components/{p-D3CwGoIu.js → p-COsKOwZR.js} +2 -2
- package/dist/components/p-CpbnmEu1.js +2 -0
- package/dist/components/p-CpbnmEu1.js.map +1 -0
- package/dist/components/{p-DouEVPmi.js → p-Dd0GrKaF.js} +2 -2
- package/dist/components/{p-BvxKLqNx.js → p-H89PzqGU.js} +2 -2
- package/dist/components/{p-BYe8Rfdg.js → p-Vwlmhl4E.js} +2 -2
- package/dist/components/{p-C4p1fG67.js → p-woooYulR.js} +2 -2
- package/dist/types/.stencil/BarNav/BarNav.d.ts +7 -2
- package/dist/types/.stencil/BarNav/bar-nav-dom-utils.d.ts +1 -2
- package/dist/types/.stencil/TabGroupNav/TabGroupNav.d.ts +27 -0
- package/dist/types/components.d.ts +68 -0
- package/package.json +37 -7
- package/src/angular/index.ts +1 -0
- package/src/angular/proxies.ts +28 -0
- package/src/react/components.ts +46 -0
- package/src/react/ds-accordion.ts +27 -0
- package/src/react/ds-badge.ts +26 -0
- package/src/react/ds-banner.ts +27 -0
- package/src/react/ds-bar-nav-action.ts +27 -0
- package/src/react/ds-bar-nav.ts +33 -0
- package/src/react/ds-breadcrumb.ts +27 -0
- package/src/react/ds-button-group.ts +27 -0
- package/src/react/ds-button.ts +35 -0
- package/src/react/ds-card.ts +26 -0
- package/src/react/ds-checkbox.ts +27 -0
- package/src/react/ds-divider.ts +26 -0
- package/src/react/ds-empty-state.ts +26 -0
- package/src/react/ds-fade.ts +26 -0
- package/src/react/ds-field.ts +26 -0
- package/src/react/ds-header.ts +26 -0
- package/src/react/ds-icon.ts +26 -0
- package/src/react/ds-input.ts +33 -0
- package/src/react/ds-label-wrap.ts +26 -0
- package/src/react/ds-loader.ts +26 -0
- package/src/react/ds-menu.ts +33 -0
- package/src/react/ds-modal.ts +27 -0
- package/src/react/ds-pagination.ts +27 -0
- package/src/react/ds-panel-nav.ts +37 -0
- package/src/react/ds-radio-group.ts +27 -0
- package/src/react/ds-scrollbar.ts +26 -0
- package/src/react/ds-select.ts +27 -0
- package/src/react/ds-sidebar.ts +33 -0
- package/src/react/ds-skeleton.ts +26 -0
- package/src/react/ds-slider.ts +27 -0
- package/src/react/ds-surface.ts +26 -0
- package/src/react/ds-tab-group-nav.ts +27 -0
- package/src/react/ds-tab-group.ts +27 -0
- package/src/react/ds-table.ts +35 -0
- package/src/react/ds-tag.ts +35 -0
- package/src/react/ds-text.ts +26 -0
- package/src/react/ds-toast-provider.ts +26 -0
- package/src/react/ds-toggle-button-group.ts +27 -0
- package/src/react/ds-toggle-button.ts +27 -0
- package/src/react/ds-toggle.ts +27 -0
- package/src/react/ds-tooltip.ts +26 -0
- package/src/wc/components/Accordion/Accordion.tsx +144 -0
- package/src/wc/components/Badge/Badge.tsx +34 -0
- package/src/wc/components/Badge/index.ts +1 -0
- package/src/wc/components/Banner/Banner.tsx +148 -0
- package/src/wc/components/BarNav/BarNav.tsx +605 -0
- package/src/wc/components/BarNav/bar-nav-dom-utils.ts +23 -0
- package/src/wc/components/BarNav/bar-nav-tabs-menu-utils.ts +59 -0
- package/src/wc/components/BarNav/bar-nav-utils.ts +76 -0
- package/src/wc/components/BarNav/index.ts +1 -0
- package/src/wc/components/BarNavAction/BarNavAction.tsx +67 -0
- package/src/wc/components/BarNavAction/index.ts +1 -0
- package/src/wc/components/Breadcrumb/Breadcrumb.tsx +61 -0
- package/src/wc/components/Button/Button.tsx +158 -0
- package/src/wc/components/Button/index.ts +1 -0
- package/src/wc/components/ButtonGroup/ButtonGroup.tsx +110 -0
- package/src/wc/components/Card/Card.tsx +26 -0
- package/src/wc/components/Checkbox/Checkbox.tsx +60 -0
- package/src/wc/components/Checkbox/index.ts +1 -0
- package/src/wc/components/Divider/Divider.tsx +23 -0
- package/src/wc/components/Divider/index.ts +2 -0
- package/src/wc/components/EmptyState/EmptyState.tsx +32 -0
- package/src/wc/components/Fade/Fade.tsx +32 -0
- package/src/wc/components/Fade/index.ts +1 -0
- package/src/wc/components/Field/Field.tsx +28 -0
- package/src/wc/components/Header/Header.tsx +33 -0
- package/src/wc/components/Icon/Icon.tsx +112 -0
- package/src/wc/components/Icon/flag-icon-catalog.ts +68 -0
- package/src/wc/components/Icon/index.ts +1 -0
- package/src/wc/components/Icon/system-icon-catalog.ts +798 -0
- package/src/wc/components/Input/Input.tsx +100 -0
- package/src/wc/components/Input/index.ts +1 -0
- package/src/wc/components/LabelWrap/LabelWrap.tsx +38 -0
- package/src/wc/components/Loader/Loader.tsx +47 -0
- package/src/wc/components/Loader/index.ts +1 -0
- package/src/wc/components/Menu/Menu.tsx +340 -0
- package/src/wc/components/Modal/Modal.tsx +197 -0
- package/src/wc/components/Pagination/Pagination.tsx +97 -0
- package/src/wc/components/PanelNav/PanelNav.tsx +666 -0
- package/src/wc/components/PanelNav/index.ts +7 -0
- package/src/wc/components/PanelNav/panel-nav-utils.ts +124 -0
- package/src/wc/components/RadioGroup/RadioGroup.tsx +128 -0
- package/src/wc/components/RadioGroup/index.ts +1 -0
- package/src/wc/components/Scrollbar/Scrollbar.tsx +189 -0
- package/src/wc/components/Select/Select.tsx +195 -0
- package/src/wc/components/Sidebar/Sidebar.tsx +245 -0
- package/src/wc/components/Skeleton/Skeleton.tsx +64 -0
- package/src/wc/components/Skeleton/index.ts +1 -0
- package/src/wc/components/Slider/Slider.tsx +68 -0
- package/src/wc/components/Slider/index.ts +1 -0
- package/src/wc/components/Surface/Surface.tsx +81 -0
- package/src/wc/components/TabGroup/TabGroup.tsx +188 -0
- package/src/wc/components/TabGroup/tab-item-utils.ts +24 -0
- package/src/wc/components/TabGroupNav/TabGroupNav.tsx +188 -0
- package/src/wc/components/Table/Table.tsx +268 -0
- package/src/wc/components/Tag/Tag.tsx +135 -0
- package/src/wc/components/Tag/index.ts +1 -0
- package/src/wc/components/Text/Text.tsx +100 -0
- package/src/wc/components/Text/index.ts +1 -0
- package/src/wc/components/Toast/Toast.tsx +174 -0
- package/src/wc/components/Toast/toast-service.ts +60 -0
- package/src/wc/components/Toggle/Toggle.tsx +43 -0
- package/src/wc/components/Toggle/index.ts +1 -0
- package/src/wc/components/ToggleButton/ToggleButton.tsx +120 -0
- package/src/wc/components/ToggleButtonGroup/ToggleButtonGroup.tsx +118 -0
- package/src/wc/components/Tooltip/Tooltip.tsx +196 -0
- package/src/wc/components.d.ts +3028 -0
- package/dist/components/p-DV8pmt1m.js +0 -2
- package/dist/components/p-DV8pmt1m.js.map +0 -1
- package/dist/components/p-STnE_bmQ.js +0 -2
- package/dist/components/p-STnE_bmQ.js.map +0 -1
- /package/dist/components/{p-D3CwGoIu.js.map → p-COsKOwZR.js.map} +0 -0
- /package/dist/components/{p-DouEVPmi.js.map → p-Dd0GrKaF.js.map} +0 -0
- /package/dist/components/{p-BvxKLqNx.js.map → p-H89PzqGU.js.map} +0 -0
- /package/dist/components/{p-BYe8Rfdg.js.map → p-Vwlmhl4E.js.map} +0 -0
- /package/dist/components/{p-C4p1fG67.js.map → p-woooYulR.js.map} +0 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
Prop,
|
|
4
|
+
Event,
|
|
5
|
+
EventEmitter,
|
|
6
|
+
Watch,
|
|
7
|
+
State,
|
|
8
|
+
h,
|
|
9
|
+
Host,
|
|
10
|
+
} from '@stencil/core';
|
|
11
|
+
import type { MenuItemData } from '../Menu/Menu';
|
|
12
|
+
import type { BarNavActionBackground } from '../BarNavAction/BarNavAction';
|
|
13
|
+
import type { TabItem } from '../TabGroup/tab-item-utils';
|
|
14
|
+
import {
|
|
15
|
+
deriveBarNavValueFromUrl,
|
|
16
|
+
parseJsonArrayProp,
|
|
17
|
+
shouldResyncBarNavProps,
|
|
18
|
+
} from './bar-nav-utils';
|
|
19
|
+
import {
|
|
20
|
+
getTabListFromTabGroup,
|
|
21
|
+
queryWithinComponentHost,
|
|
22
|
+
type QueryableHost,
|
|
23
|
+
} from './bar-nav-dom-utils';
|
|
24
|
+
import {
|
|
25
|
+
getActiveTab,
|
|
26
|
+
getActiveTabLabel,
|
|
27
|
+
tabsOverflowContainer,
|
|
28
|
+
tabsToMenuSections,
|
|
29
|
+
} from './bar-nav-tabs-menu-utils';
|
|
30
|
+
|
|
31
|
+
export type BarNavBackground = 'primary' | 'secondary' | 'transparent' | 'translucent';
|
|
32
|
+
|
|
33
|
+
/** Same shape as `TabItem` — supports `{ type: 'divider' }` between tab groups. */
|
|
34
|
+
export type BarNavTab = TabItem;
|
|
35
|
+
|
|
36
|
+
export interface BarNavActionItem {
|
|
37
|
+
id: string;
|
|
38
|
+
/** Icon name for <ds-icon>. */
|
|
39
|
+
icon: string;
|
|
40
|
+
/** Whether this action button is currently pressed/active. */
|
|
41
|
+
selected?: boolean;
|
|
42
|
+
/** Show a notification dot. */
|
|
43
|
+
dot?: boolean;
|
|
44
|
+
inactive?: boolean;
|
|
45
|
+
ariaLabel?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Component({
|
|
49
|
+
tag: 'ds-bar-nav',
|
|
50
|
+
styleUrl: 'BarNav.css',
|
|
51
|
+
scoped: true,
|
|
52
|
+
})
|
|
53
|
+
export class BarNav {
|
|
54
|
+
/**
|
|
55
|
+
* Tab items for the left section.
|
|
56
|
+
* Set via JS property: `el.tabs = [...]`. Replace the array reference to update.
|
|
57
|
+
*/
|
|
58
|
+
@Prop() tabs: BarNavTab[] = [];
|
|
59
|
+
|
|
60
|
+
/** JSON fallback for `tabs` — useful when framework bindings don't propagate arrays. */
|
|
61
|
+
@Prop({ attribute: 'tabs-json' }) tabsJson: string = '';
|
|
62
|
+
|
|
63
|
+
/** ID of the currently active tab. Overridden when `currentUrl` + `basePath` are set. */
|
|
64
|
+
@Prop({ mutable: true }) value: string = '';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Action items rendered in the right section.
|
|
68
|
+
* Set via JS property: `el.actions = [...]`. Replace the array reference to update.
|
|
69
|
+
*/
|
|
70
|
+
@Prop() actions: BarNavActionItem[] = [];
|
|
71
|
+
|
|
72
|
+
/** JSON fallback for `actions` — useful when framework bindings don't propagate arrays. */
|
|
73
|
+
@Prop({ attribute: 'actions-json' }) actionsJson: string = '';
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Fallback heading shown when no tabs are provided.
|
|
77
|
+
* When tabs are present the heading is hidden.
|
|
78
|
+
*/
|
|
79
|
+
@Prop() heading: string | undefined;
|
|
80
|
+
|
|
81
|
+
/** Surface background variant. */
|
|
82
|
+
@Prop() background: BarNavBackground = 'secondary';
|
|
83
|
+
|
|
84
|
+
/** Section base path (e.g. `/dashboard/safety`). Used with `currentUrl` to derive `value`. */
|
|
85
|
+
@Prop() basePath: string = '';
|
|
86
|
+
|
|
87
|
+
/** Current route URL. When set with `basePath`, the active tab is derived automatically. */
|
|
88
|
+
@Prop() currentUrl: string = '';
|
|
89
|
+
|
|
90
|
+
/** Emitted when the active tab changes. Detail = tab id. */
|
|
91
|
+
@Event() dsTabChange!: EventEmitter<string>;
|
|
92
|
+
|
|
93
|
+
/** Emitted when an action button is toggled. Detail = { id, selected }. */
|
|
94
|
+
@Event() dsActionChange!: EventEmitter<{ id: string; selected: boolean }>;
|
|
95
|
+
|
|
96
|
+
@State() private resolvedTabs: BarNavTab[] = [];
|
|
97
|
+
@State() private resolvedActions: BarNavActionItem[] = [];
|
|
98
|
+
@State() private urlDerivedValue: string = '';
|
|
99
|
+
@State() private hideTabsForDetailRoute = false;
|
|
100
|
+
@State() private tabsCollapsed = false;
|
|
101
|
+
@State() private menuOpen = false;
|
|
102
|
+
/** 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). */
|
|
103
|
+
@State() private tabLayoutCommitted = false;
|
|
104
|
+
@State() private triggerLabelTruncated = false;
|
|
105
|
+
|
|
106
|
+
private static readonly HOST_PROP_SYNC_BUDGET = 8;
|
|
107
|
+
private static readonly INTRINSIC_WIDTH_RETRY_MAX = 3;
|
|
108
|
+
|
|
109
|
+
private headerEl: HTMLElement | null = null;
|
|
110
|
+
private triggerEl: HTMLButtonElement | null = null;
|
|
111
|
+
private triggerLabelTextEl: HTMLElement | null = null;
|
|
112
|
+
private triggerLabelObserver: ResizeObserver | null = null;
|
|
113
|
+
private menuEl: HTMLDsMenuElement | null = null;
|
|
114
|
+
private visibleTabGroupEl: QueryableHost | null = null;
|
|
115
|
+
private probeTabGroupEl: QueryableHost | null = null;
|
|
116
|
+
private resizeObserver: ResizeObserver | null = null;
|
|
117
|
+
private overflowCheckScheduled = false;
|
|
118
|
+
private intrinsicWidthRetryCount = 0;
|
|
119
|
+
|
|
120
|
+
private get effectiveValue(): string {
|
|
121
|
+
if (this.currentUrl && this.basePath) {
|
|
122
|
+
return this.urlDerivedValue;
|
|
123
|
+
}
|
|
124
|
+
return this.value;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private get activeTabLabel(): string {
|
|
128
|
+
return getActiveTabLabel(this.resolvedTabs, this.effectiveValue);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private get activeTabHasDot(): boolean {
|
|
132
|
+
return !!getActiveTab(this.resolvedTabs, this.effectiveValue)?.dot;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@Watch('tabs')
|
|
136
|
+
@Watch('tabsJson')
|
|
137
|
+
@Watch('actions')
|
|
138
|
+
@Watch('actionsJson')
|
|
139
|
+
onPropsChange() {
|
|
140
|
+
this.resolvedTabs = this.tabsJson
|
|
141
|
+
? parseJsonArrayProp(this.tabsJson, [])
|
|
142
|
+
: (this.tabs ?? []);
|
|
143
|
+
this.resolvedActions = this.actionsJson
|
|
144
|
+
? parseJsonArrayProp(this.actionsJson, [])
|
|
145
|
+
: (this.actions ?? []);
|
|
146
|
+
this.syncValueFromUrl();
|
|
147
|
+
this.intrinsicWidthRetryCount = 0;
|
|
148
|
+
this.scheduleOverflowCheck();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@Watch('currentUrl')
|
|
152
|
+
@Watch('basePath')
|
|
153
|
+
onUrlChange() {
|
|
154
|
+
this.syncValueFromUrl();
|
|
155
|
+
this.scheduleOverflowCheck();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@Watch('value')
|
|
159
|
+
@Watch('urlDerivedValue')
|
|
160
|
+
onSelectionChange() {
|
|
161
|
+
this.syncMenuSections();
|
|
162
|
+
this.scheduleOverflowCheck();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@Watch('menuOpen')
|
|
166
|
+
onMenuOpenChange(open: boolean) {
|
|
167
|
+
if (this.menuEl) {
|
|
168
|
+
this.menuEl.open = open;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@Watch('tabsCollapsed')
|
|
173
|
+
onTabsCollapsedChange(collapsed: boolean, prevCollapsed: boolean | undefined) {
|
|
174
|
+
if (!collapsed) {
|
|
175
|
+
this.menuOpen = false;
|
|
176
|
+
this.menuEl = null;
|
|
177
|
+
this.triggerLabelTruncated = false;
|
|
178
|
+
this.syncTriggerLabelObserver();
|
|
179
|
+
}
|
|
180
|
+
this.scheduleOverflowCheck();
|
|
181
|
+
if (collapsed) {
|
|
182
|
+
requestAnimationFrame(() => {
|
|
183
|
+
this.scheduleOverflowCheck();
|
|
184
|
+
this.syncTriggerLabelObserver();
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (collapsed || prevCollapsed === undefined || prevCollapsed === collapsed) return;
|
|
188
|
+
requestAnimationFrame(() => this.scheduleOverflowCheck());
|
|
189
|
+
queueMicrotask(() => this.focusVisibleSelectedTab());
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
componentWillLoad() {
|
|
193
|
+
this.onPropsChange();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
componentDidLoad() {
|
|
197
|
+
this.syncHostPropsIfNeeded();
|
|
198
|
+
this.scheduleDeferredHostPropSync();
|
|
199
|
+
this.setupOverflowObserver();
|
|
200
|
+
this.scheduleOverflowCheck();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
componentDidRender() {
|
|
204
|
+
this.scheduleOverflowCheck();
|
|
205
|
+
this.syncMenuAnchor();
|
|
206
|
+
if (this.tabsCollapsed) {
|
|
207
|
+
this.updateTriggerLabelTruncation();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
disconnectedCallback() {
|
|
212
|
+
this.resizeObserver?.disconnect();
|
|
213
|
+
this.resizeObserver = null;
|
|
214
|
+
this.syncTriggerLabelObserver(true);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Re-resolve props assigned by the host after componentWillLoad (Angular ngAfterViewInit). */
|
|
218
|
+
private syncHostPropsIfNeeded() {
|
|
219
|
+
if (
|
|
220
|
+
shouldResyncBarNavProps(
|
|
221
|
+
this.resolvedTabs,
|
|
222
|
+
this.tabs,
|
|
223
|
+
this.tabsJson,
|
|
224
|
+
this.resolvedActions,
|
|
225
|
+
this.actions,
|
|
226
|
+
this.actionsJson,
|
|
227
|
+
)
|
|
228
|
+
) {
|
|
229
|
+
this.onPropsChange();
|
|
230
|
+
} else if (this.currentUrl && this.basePath) {
|
|
231
|
+
this.syncValueFromUrl();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Poll across animation frames — host props may land without triggering @Watch. */
|
|
236
|
+
private scheduleDeferredHostPropSync() {
|
|
237
|
+
let remaining = BarNav.HOST_PROP_SYNC_BUDGET;
|
|
238
|
+
const tick = () => {
|
|
239
|
+
this.syncHostPropsIfNeeded();
|
|
240
|
+
if (--remaining > 0) {
|
|
241
|
+
requestAnimationFrame(tick);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
queueMicrotask(tick);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private setupOverflowObserver() {
|
|
248
|
+
if (typeof ResizeObserver === 'undefined' || !this.headerEl) return;
|
|
249
|
+
this.resizeObserver?.disconnect();
|
|
250
|
+
this.resizeObserver = new ResizeObserver(() => this.scheduleOverflowCheck());
|
|
251
|
+
this.resizeObserver.observe(this.headerEl);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private scheduleOverflowCheck() {
|
|
255
|
+
if (this.overflowCheckScheduled) return;
|
|
256
|
+
this.overflowCheckScheduled = true;
|
|
257
|
+
requestAnimationFrame(() => {
|
|
258
|
+
this.overflowCheckScheduled = false;
|
|
259
|
+
this.updateTabsCollapsed();
|
|
260
|
+
this.updateTriggerLabelTruncation();
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private syncTriggerLabelObserver(disconnectOnly = false) {
|
|
265
|
+
this.triggerLabelObserver?.disconnect();
|
|
266
|
+
this.triggerLabelObserver = null;
|
|
267
|
+
|
|
268
|
+
if (disconnectOnly || !this.tabsCollapsed || !this.triggerLabelTextEl) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
this.updateTriggerLabelTruncation();
|
|
273
|
+
|
|
274
|
+
if (typeof ResizeObserver === 'undefined') return;
|
|
275
|
+
|
|
276
|
+
this.triggerLabelObserver = new ResizeObserver(() => this.updateTriggerLabelTruncation());
|
|
277
|
+
this.triggerLabelObserver.observe(this.triggerLabelTextEl);
|
|
278
|
+
if (this.triggerEl) {
|
|
279
|
+
this.triggerLabelObserver.observe(this.triggerEl);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private updateTriggerLabelTruncation() {
|
|
284
|
+
const el = this.triggerLabelTextEl;
|
|
285
|
+
if (!el || !this.tabsCollapsed) {
|
|
286
|
+
if (this.triggerLabelTruncated) {
|
|
287
|
+
this.triggerLabelTruncated = false;
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const truncated = el.scrollWidth > el.clientWidth + 1;
|
|
293
|
+
if (truncated !== this.triggerLabelTruncated) {
|
|
294
|
+
this.triggerLabelTruncated = truncated;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Width left for tabs after actions. */
|
|
299
|
+
private getLeftZoneAvailableWidth(): number {
|
|
300
|
+
if (!this.headerEl) return 0;
|
|
301
|
+
|
|
302
|
+
const headerStyle = getComputedStyle(this.headerEl);
|
|
303
|
+
const gap = parseFloat(headerStyle.columnGap || headerStyle.gap || '0') || 0;
|
|
304
|
+
const actionsEl = this.headerEl.querySelector('.bar-nav__actions') as HTMLElement | null;
|
|
305
|
+
const actionsWidth = actionsEl?.offsetWidth ?? 0;
|
|
306
|
+
const horizontalPadding =
|
|
307
|
+
(parseFloat(headerStyle.paddingLeft) || 0) + (parseFloat(headerStyle.paddingRight) || 0);
|
|
308
|
+
const spacing = this.tabsCollapsed
|
|
309
|
+
? parseFloat(
|
|
310
|
+
getComputedStyle(this.headerEl).getPropertyValue('--dimension-space-100').trim(),
|
|
311
|
+
) || 8
|
|
312
|
+
: actionsEl
|
|
313
|
+
? gap
|
|
314
|
+
: 0;
|
|
315
|
+
const available =
|
|
316
|
+
this.headerEl.clientWidth - horizontalPadding - actionsWidth - spacing;
|
|
317
|
+
|
|
318
|
+
return Math.max(0, available);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private getTabsIntrinsicWidth(): number {
|
|
322
|
+
const tabList = getTabListFromTabGroup(this.probeTabGroupEl);
|
|
323
|
+
return tabList?.scrollWidth ?? 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private scheduleIntrinsicWidthRetry() {
|
|
327
|
+
if (this.intrinsicWidthRetryCount >= BarNav.INTRINSIC_WIDTH_RETRY_MAX) return;
|
|
328
|
+
this.intrinsicWidthRetryCount++;
|
|
329
|
+
requestAnimationFrame(() => this.scheduleOverflowCheck());
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private updateTabsCollapsed() {
|
|
333
|
+
if (!this.headerEl || this.resolvedTabs.length === 0 || this.hideTabsForDetailRoute) {
|
|
334
|
+
if (this.tabsCollapsed) {
|
|
335
|
+
this.tabsCollapsed = false;
|
|
336
|
+
this.menuOpen = false;
|
|
337
|
+
}
|
|
338
|
+
this.intrinsicWidthRetryCount = 0;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const intrinsicWidth = this.getTabsIntrinsicWidth();
|
|
343
|
+
if (intrinsicWidth === 0) {
|
|
344
|
+
this.scheduleIntrinsicWidthRetry();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
this.intrinsicWidthRetryCount = 0;
|
|
348
|
+
|
|
349
|
+
const shouldCollapse = tabsOverflowContainer(
|
|
350
|
+
intrinsicWidth,
|
|
351
|
+
this.getLeftZoneAvailableWidth(),
|
|
352
|
+
this.tabsCollapsed,
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
if (shouldCollapse !== this.tabsCollapsed) {
|
|
356
|
+
this.tabsCollapsed = shouldCollapse;
|
|
357
|
+
if (!shouldCollapse) {
|
|
358
|
+
this.menuOpen = false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private syncMenuAnchor() {
|
|
364
|
+
if (!this.menuEl || !this.triggerEl || !this.tabsCollapsed) return;
|
|
365
|
+
this.menuEl.anchor = this.triggerEl;
|
|
366
|
+
this.syncMenuSections();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private handleMenuSelect(e: Event) {
|
|
370
|
+
const detail = (e as CustomEvent<MenuItemData>).detail;
|
|
371
|
+
if (detail?.value !== undefined) {
|
|
372
|
+
this.selectTab(String(detail.value));
|
|
373
|
+
}
|
|
374
|
+
this.menuOpen = false;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private handleMenuClose() {
|
|
378
|
+
this.menuOpen = false;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private syncMenuSections() {
|
|
382
|
+
if (!this.menuEl) return;
|
|
383
|
+
this.menuEl.sections = tabsToMenuSections(this.resolvedTabs, this.effectiveValue);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private syncValueFromUrl() {
|
|
387
|
+
if (!this.currentUrl || !this.basePath) {
|
|
388
|
+
this.urlDerivedValue = '';
|
|
389
|
+
this.hideTabsForDetailRoute = false;
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const { value, hideTabs } = deriveBarNavValueFromUrl(
|
|
394
|
+
this.currentUrl,
|
|
395
|
+
this.basePath,
|
|
396
|
+
this.resolvedTabs,
|
|
397
|
+
);
|
|
398
|
+
this.urlDerivedValue = value;
|
|
399
|
+
this.hideTabsForDetailRoute = hideTabs;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private selectTab(id: string) {
|
|
403
|
+
this.value = id;
|
|
404
|
+
this.dsTabChange.emit(id);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Move focus to the selected tab in the visible tab group (after expand). */
|
|
408
|
+
private focusVisibleSelectedTab() {
|
|
409
|
+
const tab = queryWithinComponentHost(
|
|
410
|
+
this.visibleTabGroupEl,
|
|
411
|
+
`[data-tab-id="${this.effectiveValue}"]`,
|
|
412
|
+
);
|
|
413
|
+
tab?.focus({ preventScroll: true });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private handleTabChange(e: Event) {
|
|
417
|
+
this.selectTab((e as CustomEvent<string>).detail);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private handleActionChange(actionId: string, e: Event) {
|
|
421
|
+
const selected = (e as CustomEvent<boolean>).detail;
|
|
422
|
+
this.dsActionChange.emit({ id: actionId, selected });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private toggleTabMenu() {
|
|
426
|
+
if (this.menuOpen) {
|
|
427
|
+
this.menuOpen = false;
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (this.menuEl && this.triggerEl) {
|
|
432
|
+
this.menuEl.anchor = this.triggerEl;
|
|
433
|
+
this.menuEl.minWidth = `${this.triggerEl.offsetWidth}px`;
|
|
434
|
+
this.syncMenuSections();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
this.menuOpen = true;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private handleTriggerKeyDown(e: KeyboardEvent) {
|
|
441
|
+
if (!this.tabsCollapsed) return;
|
|
442
|
+
|
|
443
|
+
if (!this.menuOpen) {
|
|
444
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter' || e.key === ' ') {
|
|
445
|
+
e.preventDefault();
|
|
446
|
+
this.toggleTabMenu();
|
|
447
|
+
}
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (e.key === 'Escape') {
|
|
452
|
+
e.preventDefault();
|
|
453
|
+
this.menuOpen = false;
|
|
454
|
+
this.triggerEl?.focus();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
render() {
|
|
459
|
+
const hasTabs = this.resolvedTabs.length > 0 && !this.hideTabsForDetailRoute;
|
|
460
|
+
|
|
461
|
+
const bgToActionBg: Partial<Record<BarNavBackground, BarNavActionBackground>> = {};
|
|
462
|
+
const actionBg: BarNavActionBackground | undefined = bgToActionBg[this.background];
|
|
463
|
+
|
|
464
|
+
return (
|
|
465
|
+
<Host>
|
|
466
|
+
<header
|
|
467
|
+
class={{
|
|
468
|
+
'bar-nav': true,
|
|
469
|
+
[`bg-${this.background}`]: true,
|
|
470
|
+
'bar-nav--tabs-collapsed': hasTabs && this.tabsCollapsed,
|
|
471
|
+
}}
|
|
472
|
+
ref={el => {
|
|
473
|
+
this.headerEl = el as HTMLElement;
|
|
474
|
+
if (el && !this.resizeObserver) {
|
|
475
|
+
this.setupOverflowObserver();
|
|
476
|
+
}
|
|
477
|
+
}}
|
|
478
|
+
>
|
|
479
|
+
{hasTabs && (
|
|
480
|
+
<div class="bar-nav__tabs-probe" aria-hidden="true" inert>
|
|
481
|
+
<ds-tab-group-nav
|
|
482
|
+
ref={el => {
|
|
483
|
+
this.probeTabGroupEl = el ?? null;
|
|
484
|
+
if (el) {
|
|
485
|
+
this.intrinsicWidthRetryCount = 0;
|
|
486
|
+
this.scheduleOverflowCheck();
|
|
487
|
+
}
|
|
488
|
+
}}
|
|
489
|
+
tabs={this.resolvedTabs}
|
|
490
|
+
value={this.effectiveValue}
|
|
491
|
+
/>
|
|
492
|
+
</div>
|
|
493
|
+
)}
|
|
494
|
+
|
|
495
|
+
{hasTabs && !this.tabsCollapsed && (
|
|
496
|
+
<div class="bar-nav__left">
|
|
497
|
+
<ds-tab-group-nav
|
|
498
|
+
class="bar-nav__tabs-visible"
|
|
499
|
+
ref={el => {
|
|
500
|
+
this.visibleTabGroupEl = el ?? null;
|
|
501
|
+
}}
|
|
502
|
+
tabs={this.resolvedTabs}
|
|
503
|
+
value={this.effectiveValue}
|
|
504
|
+
onDsChange={(e: Event) => this.handleTabChange(e)}
|
|
505
|
+
/>
|
|
506
|
+
</div>
|
|
507
|
+
)}
|
|
508
|
+
|
|
509
|
+
{!hasTabs && this.heading && (
|
|
510
|
+
<span class="bar-nav__heading text-body-medium-emphasis">{this.heading}</span>
|
|
511
|
+
)}
|
|
512
|
+
|
|
513
|
+
{hasTabs && this.tabsCollapsed && (
|
|
514
|
+
<button
|
|
515
|
+
type="button"
|
|
516
|
+
class={{
|
|
517
|
+
'bar-nav__tab-trigger': true,
|
|
518
|
+
'bar-nav__tab-trigger--open': this.menuOpen,
|
|
519
|
+
}}
|
|
520
|
+
ref={el => {
|
|
521
|
+
this.triggerEl = el as HTMLButtonElement;
|
|
522
|
+
}}
|
|
523
|
+
aria-haspopup="menu"
|
|
524
|
+
aria-expanded={this.menuOpen}
|
|
525
|
+
aria-label={`${this.activeTabLabel} tabs`}
|
|
526
|
+
onClick={() => this.toggleTabMenu()}
|
|
527
|
+
onKeyDown={(e: KeyboardEvent) => this.handleTriggerKeyDown(e)}
|
|
528
|
+
>
|
|
529
|
+
<span
|
|
530
|
+
class={{
|
|
531
|
+
'bar-nav__tab-trigger-label': true,
|
|
532
|
+
'bar-nav__tab-trigger-label--dot': this.activeTabHasDot,
|
|
533
|
+
'bar-nav__tab-trigger-label--truncated': this.triggerLabelTruncated,
|
|
534
|
+
}}
|
|
535
|
+
>
|
|
536
|
+
<span
|
|
537
|
+
class={{
|
|
538
|
+
'bar-nav__tab-trigger-label-text': true,
|
|
539
|
+
'bar-nav__tab-trigger-label-text--truncated': this.triggerLabelTruncated,
|
|
540
|
+
'text-body-medium-emphasis': true,
|
|
541
|
+
}}
|
|
542
|
+
ref={el => {
|
|
543
|
+
const next = el ?? null;
|
|
544
|
+
if (next === this.triggerLabelTextEl) return;
|
|
545
|
+
this.triggerLabelTextEl = next;
|
|
546
|
+
if (this.tabsCollapsed) {
|
|
547
|
+
this.syncTriggerLabelObserver();
|
|
548
|
+
}
|
|
549
|
+
}}
|
|
550
|
+
>
|
|
551
|
+
{this.activeTabLabel}
|
|
552
|
+
</span>
|
|
553
|
+
{this.activeTabHasDot && (
|
|
554
|
+
<span class="bar-nav__tab-trigger-dot" aria-hidden="true" />
|
|
555
|
+
)}
|
|
556
|
+
</span>
|
|
557
|
+
<ds-icon
|
|
558
|
+
name="ChevronDown"
|
|
559
|
+
size="md"
|
|
560
|
+
color="inherit"
|
|
561
|
+
class="bar-nav__tab-trigger-chevron"
|
|
562
|
+
/>
|
|
563
|
+
</button>
|
|
564
|
+
)}
|
|
565
|
+
|
|
566
|
+
{hasTabs && this.tabsCollapsed && this.resolvedActions.length > 0 && (
|
|
567
|
+
<div class="bar-nav__between" aria-hidden="true" />
|
|
568
|
+
)}
|
|
569
|
+
|
|
570
|
+
{this.resolvedActions.length > 0 && (
|
|
571
|
+
<div class="bar-nav__actions">
|
|
572
|
+
{this.resolvedActions.map(action => (
|
|
573
|
+
<ds-bar-nav-action
|
|
574
|
+
key={action.id}
|
|
575
|
+
icon={action.icon}
|
|
576
|
+
selected={action.selected ?? false}
|
|
577
|
+
dot={action.dot ?? false}
|
|
578
|
+
inactive={action.inactive}
|
|
579
|
+
aria-label={action.ariaLabel ?? action.id}
|
|
580
|
+
background={actionBg}
|
|
581
|
+
onDsChange={(e: Event) => this.handleActionChange(action.id, e)}
|
|
582
|
+
/>
|
|
583
|
+
))}
|
|
584
|
+
</div>
|
|
585
|
+
)}
|
|
586
|
+
|
|
587
|
+
</header>
|
|
588
|
+
|
|
589
|
+
{hasTabs && this.tabsCollapsed && (
|
|
590
|
+
<ds-menu
|
|
591
|
+
ref={el => {
|
|
592
|
+
this.menuEl = (el as HTMLDsMenuElement) ?? null;
|
|
593
|
+
}}
|
|
594
|
+
open={this.menuOpen}
|
|
595
|
+
sections={tabsToMenuSections(this.resolvedTabs, this.effectiveValue)}
|
|
596
|
+
side="bottom"
|
|
597
|
+
align="start"
|
|
598
|
+
onDsSelect={(e: Event) => this.handleMenuSelect(e)}
|
|
599
|
+
onDsClose={() => this.handleMenuClose()}
|
|
600
|
+
/>
|
|
601
|
+
)}
|
|
602
|
+
</Host>
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type QueryableHost = {
|
|
2
|
+
shadowRoot?: ShadowRoot | null;
|
|
3
|
+
querySelector: (selectors: string) => Element | null;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Query within a Stencil custom-element host (light DOM today, shadow DOM–safe).
|
|
8
|
+
* Use for child nodes owned by ds-tab-group and similar components.
|
|
9
|
+
*/
|
|
10
|
+
export function queryWithinComponentHost(
|
|
11
|
+
host: QueryableHost | null | undefined,
|
|
12
|
+
selector: string,
|
|
13
|
+
): HTMLElement | null {
|
|
14
|
+
if (!host) return null;
|
|
15
|
+
const root = host.shadowRoot ?? host;
|
|
16
|
+
return root.querySelector(selector) as HTMLElement | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getTabListFromTabGroup(
|
|
20
|
+
tabGroupEl: QueryableHost | null | undefined,
|
|
21
|
+
): HTMLElement | null {
|
|
22
|
+
return queryWithinComponentHost(tabGroupEl, '[role="tablist"]');
|
|
23
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { MenuSection } from '../Menu/Menu';
|
|
2
|
+
import {
|
|
3
|
+
getSelectableTabs,
|
|
4
|
+
isTabDivider,
|
|
5
|
+
type TabItem,
|
|
6
|
+
type TabItemTab,
|
|
7
|
+
} from '../TabGroup/tab-item-utils';
|
|
8
|
+
|
|
9
|
+
/** Map BarNav tabs to menu sections, preserving `{ type: 'divider' }` as section breaks. */
|
|
10
|
+
export function tabsToMenuSections(tabs: TabItem[], selectedId: string): MenuSection[] {
|
|
11
|
+
const sections: MenuSection[] = [];
|
|
12
|
+
let batch: MenuSection['items'] = [];
|
|
13
|
+
|
|
14
|
+
for (const item of tabs) {
|
|
15
|
+
if (isTabDivider(item)) {
|
|
16
|
+
if (batch.length > 0) {
|
|
17
|
+
sections.push({ items: batch });
|
|
18
|
+
batch = [];
|
|
19
|
+
}
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
batch.push({
|
|
24
|
+
label: item.label,
|
|
25
|
+
value: item.id,
|
|
26
|
+
isSelected: item.id === selectedId,
|
|
27
|
+
isInactive: item.disabled,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (batch.length > 0) {
|
|
32
|
+
sections.push({ items: batch });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return sections;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getActiveTab(tabs: TabItem[], selectedId: string): TabItemTab | undefined {
|
|
39
|
+
const selectable = getSelectableTabs(tabs);
|
|
40
|
+
return selectable.find(tab => tab.id === selectedId) ?? selectable[0];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getActiveTabLabel(tabs: TabItem[], selectedId: string): string {
|
|
44
|
+
return getActiveTab(tabs, selectedId)?.label ?? 'Tabs';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** True when tablist content exceeds the available width (with optional hysteresis). */
|
|
48
|
+
export function tabsOverflowContainer(
|
|
49
|
+
tabListScrollWidth: number,
|
|
50
|
+
containerClientWidth: number,
|
|
51
|
+
collapsed: boolean,
|
|
52
|
+
hysteresis = 2,
|
|
53
|
+
): boolean {
|
|
54
|
+
const diff = tabListScrollWidth - containerClientWidth;
|
|
55
|
+
if (collapsed) {
|
|
56
|
+
return diff > -hysteresis;
|
|
57
|
+
}
|
|
58
|
+
return diff > hysteresis;
|
|
59
|
+
}
|