@ds-mo/ui 1.10.0 → 1.10.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/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-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/p-DN65lYYc.js +2 -0
- package/dist/components/p-DN65lYYc.js.map +1 -0
- package/dist/types/components/AppShell/AppShell.d.ts +12 -1
- package/dist/types/components/BarNav/BarNav.d.ts +10 -0
- package/dist/types/components/PanelNav/PanelNav.d.ts +4 -0
- package/dist/types/components/PanelTools/PanelTools.d.ts +4 -0
- package/dist/types/components.d.ts +16 -0
- package/dist/types/nav/chrome-transition.d.ts +8 -0
- package/dist/types/nav/index.d.ts +4 -0
- package/dist/types/nav/shell-chrome-metrics.d.ts +12 -0
- package/package.json +2 -2
- package/src/angular/proxies.ts +20 -2
- package/src/react/ds-panel-nav.ts +5 -1
- package/src/react/ds-panel-tools.ts +11 -3
- package/src/wc/components/AppShell/AppShell.tsx +106 -16
- package/src/wc/components/BarNav/BarNav.tsx +91 -2
- package/src/wc/components/PanelNav/PanelNav.tsx +10 -0
- package/src/wc/components/PanelTools/PanelTools.tsx +10 -0
- package/src/wc/components.d.ts +16 -0
- package/src/wc/nav/chrome-transition.ts +10 -0
- package/src/wc/nav/index.ts +13 -0
- package/src/wc/nav/shell-chrome-metrics.ts +49 -0
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { Component, Prop, Element, Watch, h, Host } from '@stencil/core';
|
|
2
2
|
import type { NavChromeStyle } from '../../nav/nav-chrome';
|
|
3
|
+
import {
|
|
4
|
+
CHROME_TRANSITION_END,
|
|
5
|
+
CHROME_TRANSITION_START,
|
|
6
|
+
type ChromeTransitionDetail,
|
|
7
|
+
} from '../../nav/chrome-transition';
|
|
8
|
+
import {
|
|
9
|
+
isPanelNavCollapsed,
|
|
10
|
+
panelWidthPxFromTokens,
|
|
11
|
+
readPanelNavWidthTokens,
|
|
12
|
+
type PanelNavWidthTokens,
|
|
13
|
+
} from '../../nav/shell-chrome-metrics';
|
|
3
14
|
import {
|
|
4
15
|
SHELL_GRADIENT_IMAGE_VAR,
|
|
5
16
|
SHELL_GRADIENT_OPACITY,
|
|
@@ -34,21 +45,32 @@ export class AppShell {
|
|
|
34
45
|
@Element() el!: HTMLElement;
|
|
35
46
|
|
|
36
47
|
private resizeObserver: ResizeObserver | null = null;
|
|
48
|
+
private chromeSyncRafId = 0;
|
|
49
|
+
private chromeTransitionDepth = 0;
|
|
50
|
+
private panelWidthTokens: PanelNavWidthTokens = { expandedPx: 0, collapsedPx: 0 };
|
|
51
|
+
private cachedShellWidth = 0;
|
|
52
|
+
private cachedShellHeight = 0;
|
|
37
53
|
|
|
38
54
|
componentDidLoad() {
|
|
39
55
|
this.syncSlottedNavStyle();
|
|
40
56
|
this.connectMetricsObserver();
|
|
41
|
-
this.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
this.el.addEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);
|
|
58
|
+
this.el.addEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);
|
|
59
|
+
requestAnimationFrame(() => {
|
|
60
|
+
this.cachePanelWidthTokens();
|
|
61
|
+
this.scheduleChromeSync();
|
|
62
|
+
});
|
|
47
63
|
}
|
|
48
64
|
|
|
49
65
|
disconnectedCallback() {
|
|
50
66
|
this.resizeObserver?.disconnect();
|
|
51
67
|
this.resizeObserver = null;
|
|
68
|
+
if (this.chromeSyncRafId) {
|
|
69
|
+
cancelAnimationFrame(this.chromeSyncRafId);
|
|
70
|
+
this.chromeSyncRafId = 0;
|
|
71
|
+
}
|
|
72
|
+
this.el.removeEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);
|
|
73
|
+
this.el.removeEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);
|
|
52
74
|
}
|
|
53
75
|
|
|
54
76
|
@Watch('navStyle')
|
|
@@ -59,6 +81,35 @@ export class AppShell {
|
|
|
59
81
|
this.scheduleChromeSync();
|
|
60
82
|
}
|
|
61
83
|
|
|
84
|
+
private onChromeTransitionStart = (event: Event) => {
|
|
85
|
+
const source = (event as CustomEvent<ChromeTransitionDetail>).detail?.source;
|
|
86
|
+
if (source === 'panel-nav') {
|
|
87
|
+
this.chromeTransitionDepth += 1;
|
|
88
|
+
this.cachedShellWidth = this.el.clientWidth;
|
|
89
|
+
this.cachedShellHeight = this.el.clientHeight;
|
|
90
|
+
this.syncChrome();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
private onChromeTransitionEnd = (event: Event) => {
|
|
95
|
+
const source = (event as CustomEvent<ChromeTransitionDetail>).detail?.source;
|
|
96
|
+
if (source !== 'panel-nav') return;
|
|
97
|
+
|
|
98
|
+
this.chromeTransitionDepth = Math.max(0, this.chromeTransitionDepth - 1);
|
|
99
|
+
if (this.chromeTransitionDepth === 0) {
|
|
100
|
+
this.scheduleChromeSync();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** Coalesce ResizeObserver bursts to one layout read per frame. */
|
|
105
|
+
private scheduleChromeSync() {
|
|
106
|
+
if (this.chromeSyncRafId) return;
|
|
107
|
+
this.chromeSyncRafId = requestAnimationFrame(() => {
|
|
108
|
+
this.chromeSyncRafId = 0;
|
|
109
|
+
this.syncChrome();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
62
113
|
private syncSlottedNavStyle() {
|
|
63
114
|
const panel = this.el.querySelector('ds-panel-nav') as (HTMLElement & { navStyle: NavChromeStyle }) | null;
|
|
64
115
|
const bar = this.el.querySelector('ds-bar-nav') as (HTMLElement & { navStyle: NavChromeStyle }) | null;
|
|
@@ -73,11 +124,19 @@ export class AppShell {
|
|
|
73
124
|
}
|
|
74
125
|
|
|
75
126
|
private connectMetricsObserver() {
|
|
76
|
-
this.resizeObserver = new ResizeObserver(() =>
|
|
127
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
128
|
+
if (this.chromeTransitionDepth > 0) return;
|
|
129
|
+
this.scheduleChromeSync();
|
|
130
|
+
});
|
|
77
131
|
this.resizeObserver.observe(this.el);
|
|
78
132
|
const panelWrap = this.el.querySelector('.app-shell__panel');
|
|
79
133
|
if (panelWrap) this.resizeObserver.observe(panelWrap);
|
|
80
|
-
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private cachePanelWidthTokens() {
|
|
137
|
+
const navRoot = this.el.querySelector('ds-panel-nav .panel-nav') as HTMLElement | null;
|
|
138
|
+
if (!navRoot) return;
|
|
139
|
+
this.panelWidthTokens = readPanelNavWidthTokens(navRoot);
|
|
81
140
|
}
|
|
82
141
|
|
|
83
142
|
private applyGradientVars(target: HTMLElement, vars: Record<string, string | null>) {
|
|
@@ -114,19 +173,50 @@ export class AppShell {
|
|
|
114
173
|
}
|
|
115
174
|
}
|
|
116
175
|
|
|
176
|
+
private resolvePanelWidthPx(panelNav: HTMLElement | null): number {
|
|
177
|
+
const navRoot = panelNav?.querySelector('.panel-nav') as HTMLElement | null;
|
|
178
|
+
|
|
179
|
+
if (this.chromeTransitionDepth > 0 && this.panelWidthTokens.expandedPx > 0) {
|
|
180
|
+
const collapsed = isPanelNavCollapsed(panelNav, navRoot);
|
|
181
|
+
return panelWidthPxFromTokens(this.panelWidthTokens, collapsed);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const panelWrap = this.el.querySelector('.app-shell__panel') as HTMLElement | null;
|
|
185
|
+
const measured = panelWrap?.getBoundingClientRect().width ?? 0;
|
|
186
|
+
if (measured > 0) return measured;
|
|
187
|
+
|
|
188
|
+
if (this.panelWidthTokens.expandedPx > 0) {
|
|
189
|
+
const collapsed = isPanelNavCollapsed(panelNav, navRoot);
|
|
190
|
+
return panelWidthPxFromTokens(this.panelWidthTokens, collapsed);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private resolveShellDimensions(): { width: number; height: number } {
|
|
197
|
+
if (this.chromeTransitionDepth > 0 && this.cachedShellWidth > 0) {
|
|
198
|
+
return {
|
|
199
|
+
width: this.cachedShellWidth,
|
|
200
|
+
height: this.cachedShellHeight || this.el.clientHeight,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const rect = this.el.getBoundingClientRect();
|
|
205
|
+
return { width: rect.width, height: rect.height };
|
|
206
|
+
}
|
|
207
|
+
|
|
117
208
|
private syncChrome() {
|
|
118
|
-
const
|
|
209
|
+
const panelNav = this.el.querySelector('ds-panel-nav') as HTMLElement | null;
|
|
119
210
|
const bar = this.el.querySelector('ds-bar-nav') as HTMLElement | null;
|
|
120
|
-
const targets = [this.el,
|
|
211
|
+
const targets = [this.el, panelNav, bar].filter((el): el is HTMLElement => el !== null);
|
|
121
212
|
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
const panelWidth = panelWrap?.getBoundingClientRect().width ?? 0;
|
|
213
|
+
const { width: shellWidth, height: shellHeight } = this.resolveShellDimensions();
|
|
214
|
+
const panelWidth = this.resolvePanelWidthPx(panelNav);
|
|
125
215
|
|
|
126
216
|
const panelPosition = shellGradientPositionPanel();
|
|
127
217
|
const barPosition = shellGradientPositionBar(panelWidth);
|
|
128
218
|
|
|
129
|
-
this.syncChromeLayoutVars(
|
|
219
|
+
this.syncChromeLayoutVars(panelNav, bar, panelPosition, barPosition);
|
|
130
220
|
|
|
131
221
|
if (!this.gradient) {
|
|
132
222
|
this.clearGradientPaintVars(targets);
|
|
@@ -138,8 +228,8 @@ export class AppShell {
|
|
|
138
228
|
: buildShellRadialGradient();
|
|
139
229
|
|
|
140
230
|
const size = shellGradientSize({
|
|
141
|
-
width:
|
|
142
|
-
height:
|
|
231
|
+
width: shellWidth,
|
|
232
|
+
height: shellHeight,
|
|
143
233
|
panelWidth,
|
|
144
234
|
});
|
|
145
235
|
const opacity = SHELL_GRADIENT_OPACITY;
|
|
@@ -28,6 +28,12 @@ import {
|
|
|
28
28
|
tabsOverflowContainer,
|
|
29
29
|
tabsToMenuSections,
|
|
30
30
|
} from './bar-nav-tabs-menu-utils';
|
|
31
|
+
import {
|
|
32
|
+
CHROME_TRANSITION_END,
|
|
33
|
+
CHROME_TRANSITION_START,
|
|
34
|
+
type ChromeTransitionDetail,
|
|
35
|
+
} from '../../nav/chrome-transition';
|
|
36
|
+
import { readCssVarWidthPx } from '../../nav/shell-chrome-metrics';
|
|
31
37
|
|
|
32
38
|
@Component({
|
|
33
39
|
tag: 'ds-bar-nav',
|
|
@@ -91,6 +97,7 @@ export class BarNav {
|
|
|
91
97
|
|
|
92
98
|
private static readonly HOST_PROP_SYNC_BUDGET = 8;
|
|
93
99
|
private static readonly INTRINSIC_WIDTH_RETRY_MAX = 3;
|
|
100
|
+
private static readonly OVERFLOW_HYSTERESIS_PX = 8;
|
|
94
101
|
|
|
95
102
|
private headerEl: HTMLElement | null = null;
|
|
96
103
|
private triggerEl: HTMLButtonElement | null = null;
|
|
@@ -102,6 +109,9 @@ export class BarNav {
|
|
|
102
109
|
private resizeObserver: ResizeObserver | null = null;
|
|
103
110
|
private overflowCheckScheduled = false;
|
|
104
111
|
private intrinsicWidthRetryCount = 0;
|
|
112
|
+
private chromeTransitionDepth = 0;
|
|
113
|
+
private chromeTransitionShell: HTMLElement | null = null;
|
|
114
|
+
private toolsDrawerOpening = false;
|
|
105
115
|
/** Matches `basePath` once tabs + URL are reconciled for the active section. */
|
|
106
116
|
@State() private committedSection = '';
|
|
107
117
|
/** Section waiting for URL + tabs to catch up after a cross-area navigation. */
|
|
@@ -174,10 +184,15 @@ export class BarNav {
|
|
|
174
184
|
this.syncHostPropsIfNeeded();
|
|
175
185
|
this.scheduleDeferredHostPropSync();
|
|
176
186
|
this.setupOverflowObserver();
|
|
187
|
+
this.bindChromeTransitionListeners();
|
|
177
188
|
this.scheduleOverflowCheck();
|
|
178
189
|
}
|
|
179
190
|
|
|
180
191
|
componentDidRender() {
|
|
192
|
+
if (this.chromeTransitionDepth > 0) {
|
|
193
|
+
this.syncMenuAnchor();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
181
196
|
this.scheduleOverflowCheck();
|
|
182
197
|
this.syncMenuAnchor();
|
|
183
198
|
if (this.tabsCollapsed) {
|
|
@@ -189,8 +204,63 @@ export class BarNav {
|
|
|
189
204
|
this.resizeObserver?.disconnect();
|
|
190
205
|
this.resizeObserver = null;
|
|
191
206
|
this.syncTriggerLabelObserver(true);
|
|
207
|
+
this.unbindChromeTransitionListeners();
|
|
192
208
|
}
|
|
193
209
|
|
|
210
|
+
private bindChromeTransitionListeners() {
|
|
211
|
+
const shell = this.el.closest('ds-app-shell');
|
|
212
|
+
if (!shell) return;
|
|
213
|
+
this.chromeTransitionShell = shell;
|
|
214
|
+
shell.addEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);
|
|
215
|
+
shell.addEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private unbindChromeTransitionListeners() {
|
|
219
|
+
if (!this.chromeTransitionShell) return;
|
|
220
|
+
this.chromeTransitionShell.removeEventListener(
|
|
221
|
+
CHROME_TRANSITION_START,
|
|
222
|
+
this.onChromeTransitionStart,
|
|
223
|
+
);
|
|
224
|
+
this.chromeTransitionShell.removeEventListener(
|
|
225
|
+
CHROME_TRANSITION_END,
|
|
226
|
+
this.onChromeTransitionEnd,
|
|
227
|
+
);
|
|
228
|
+
this.chromeTransitionShell = null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private onChromeTransitionStart = (event: Event) => {
|
|
232
|
+
const source = (event as CustomEvent<ChromeTransitionDetail>).detail?.source;
|
|
233
|
+
if (source === 'panel-nav') {
|
|
234
|
+
this.chromeTransitionDepth += 1;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (source === 'panel-tools') {
|
|
238
|
+
this.toolsDrawerOpening = true;
|
|
239
|
+
this.el.classList.add('bar-nav--tools-drawer-opening');
|
|
240
|
+
if (this.resolvedTabs.length > 0 && !this.hideTabsForDetailRoute) {
|
|
241
|
+
this.tabsCollapsed = true;
|
|
242
|
+
this.menuOpen = false;
|
|
243
|
+
}
|
|
244
|
+
this.scheduleOverflowCheck();
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
private onChromeTransitionEnd = (event: Event) => {
|
|
249
|
+
const source = (event as CustomEvent<ChromeTransitionDetail>).detail?.source;
|
|
250
|
+
if (source === 'panel-nav') {
|
|
251
|
+
this.chromeTransitionDepth = Math.max(0, this.chromeTransitionDepth - 1);
|
|
252
|
+
if (this.chromeTransitionDepth === 0) {
|
|
253
|
+
this.scheduleOverflowCheck();
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (source === 'panel-tools') {
|
|
258
|
+
this.toolsDrawerOpening = false;
|
|
259
|
+
this.el.classList.remove('bar-nav--tools-drawer-opening');
|
|
260
|
+
this.scheduleOverflowCheck();
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
194
264
|
/** Re-resolve props assigned by the host after componentWillLoad (Angular ngAfterViewInit). */
|
|
195
265
|
private syncHostPropsIfNeeded() {
|
|
196
266
|
if (
|
|
@@ -224,15 +294,20 @@ export class BarNav {
|
|
|
224
294
|
private setupOverflowObserver() {
|
|
225
295
|
if (typeof ResizeObserver === 'undefined' || !this.headerEl) return;
|
|
226
296
|
this.resizeObserver?.disconnect();
|
|
227
|
-
this.resizeObserver = new ResizeObserver(() =>
|
|
297
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
298
|
+
if (this.chromeTransitionDepth > 0) return;
|
|
299
|
+
this.scheduleOverflowCheck();
|
|
300
|
+
});
|
|
228
301
|
this.resizeObserver.observe(this.headerEl);
|
|
229
302
|
}
|
|
230
303
|
|
|
231
304
|
private scheduleOverflowCheck() {
|
|
305
|
+
if (this.chromeTransitionDepth > 0) return;
|
|
232
306
|
if (this.overflowCheckScheduled) return;
|
|
233
307
|
this.overflowCheckScheduled = true;
|
|
234
308
|
requestAnimationFrame(() => {
|
|
235
309
|
this.overflowCheckScheduled = false;
|
|
310
|
+
if (this.chromeTransitionDepth > 0) return;
|
|
236
311
|
this.updateTabsCollapsed();
|
|
237
312
|
this.updateTriggerLabelTruncation();
|
|
238
313
|
});
|
|
@@ -292,7 +367,20 @@ export class BarNav {
|
|
|
292
367
|
const available =
|
|
293
368
|
this.headerEl.clientWidth - horizontalPadding - actionsWidth - spacing;
|
|
294
369
|
|
|
295
|
-
return Math.max(0, available);
|
|
370
|
+
return Math.max(0, available - this.toolsDrawerOpeningReservePx());
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** While the tools drawer is opening, reserve width not yet claimed by flex shrink. */
|
|
374
|
+
private toolsDrawerOpeningReservePx(): number {
|
|
375
|
+
if (!this.toolsDrawerOpening) return 0;
|
|
376
|
+
|
|
377
|
+
const tools = this.el.closest('ds-app-shell')?.querySelector('ds-panel-tools') as HTMLElement | null;
|
|
378
|
+
if (!tools) return 0;
|
|
379
|
+
|
|
380
|
+
const drawerTarget = readCssVarWidthPx(tools, '--_panel-tools-drawer-width');
|
|
381
|
+
const drawerNow =
|
|
382
|
+
tools.querySelector('.panel-tools__drawer')?.getBoundingClientRect().width ?? 0;
|
|
383
|
+
return Math.max(0, drawerTarget - drawerNow);
|
|
296
384
|
}
|
|
297
385
|
|
|
298
386
|
private getTabsIntrinsicWidth(): number {
|
|
@@ -327,6 +415,7 @@ export class BarNav {
|
|
|
327
415
|
intrinsicWidth,
|
|
328
416
|
this.getLeftZoneAvailableWidth(),
|
|
329
417
|
this.tabsCollapsed,
|
|
418
|
+
BarNav.OVERFLOW_HYSTERESIS_PX,
|
|
330
419
|
);
|
|
331
420
|
|
|
332
421
|
if (shouldCollapse !== this.tabsCollapsed) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Component, Prop, Event, EventEmitter, Watch, State, Element, h, Host } from '@stencil/core';
|
|
2
|
+
import type { ChromeTransitionDetail } from '../../nav/chrome-transition';
|
|
2
3
|
import type { NavChromeStyle } from '../../nav/nav-chrome';
|
|
3
4
|
import {
|
|
4
5
|
deriveActiveIdFromUrl,
|
|
@@ -69,6 +70,13 @@ export class PanelNav {
|
|
|
69
70
|
/** Emitted when the collapse toggle is clicked. Detail = new collapsed state. */
|
|
70
71
|
@Event() dsNavToggle!: EventEmitter<boolean>;
|
|
71
72
|
|
|
73
|
+
/** Bubbling lifecycle — `ds-app-shell` pauses chrome metrics during width motion. */
|
|
74
|
+
@Event({ bubbles: true, composed: true })
|
|
75
|
+
dsChromeTransitionStart!: EventEmitter<ChromeTransitionDetail>;
|
|
76
|
+
|
|
77
|
+
@Event({ bubbles: true, composed: true })
|
|
78
|
+
dsChromeTransitionEnd!: EventEmitter<ChromeTransitionDetail>;
|
|
79
|
+
|
|
72
80
|
/** Emitted when the footer left button (gear / dashboard) is clicked. */
|
|
73
81
|
@Event() dsNavFooterAction!: EventEmitter<void>;
|
|
74
82
|
|
|
@@ -181,6 +189,7 @@ export class PanelNav {
|
|
|
181
189
|
|
|
182
190
|
private startCollapseAnimation() {
|
|
183
191
|
this.isAnimating = true;
|
|
192
|
+
this.dsChromeTransitionStart.emit({ source: 'panel-nav' });
|
|
184
193
|
const panel = this.el.querySelector('.panel-nav') as HTMLElement | null;
|
|
185
194
|
if (this.transitionEndHandler) {
|
|
186
195
|
panel?.removeEventListener('transitionend', this.transitionEndHandler);
|
|
@@ -188,6 +197,7 @@ export class PanelNav {
|
|
|
188
197
|
this.transitionEndHandler = (e: TransitionEvent) => {
|
|
189
198
|
if (e.propertyName === 'width') {
|
|
190
199
|
this.isAnimating = false;
|
|
200
|
+
this.dsChromeTransitionEnd.emit({ source: 'panel-nav' });
|
|
191
201
|
panel?.removeEventListener('transitionend', this.transitionEndHandler!);
|
|
192
202
|
}
|
|
193
203
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Component, Prop, Event, EventEmitter, Element, State, Watch, h, Host } from '@stencil/core';
|
|
2
|
+
import type { ChromeTransitionDetail } from '../../nav/chrome-transition';
|
|
2
3
|
import {
|
|
3
4
|
PANEL_TOOLS_LABELS,
|
|
4
5
|
PANEL_TOOLS_PRIMARY_TOOL_ID,
|
|
@@ -37,6 +38,13 @@ export class PanelTools {
|
|
|
37
38
|
selected: boolean;
|
|
38
39
|
}>;
|
|
39
40
|
|
|
41
|
+
/** Bubbling lifecycle — `ds-bar-nav` defers overflow checks during drawer motion. */
|
|
42
|
+
@Event({ bubbles: true, composed: true })
|
|
43
|
+
dsChromeTransitionStart!: EventEmitter<ChromeTransitionDetail>;
|
|
44
|
+
|
|
45
|
+
@Event({ bubbles: true, composed: true })
|
|
46
|
+
dsChromeTransitionEnd!: EventEmitter<ChromeTransitionDetail>;
|
|
47
|
+
|
|
40
48
|
/** Arms open vs close easing for the in-flight width transition. */
|
|
41
49
|
@State() private motion: 'opening' | 'closing' | 'idle' = 'idle';
|
|
42
50
|
|
|
@@ -74,6 +82,7 @@ export class PanelTools {
|
|
|
74
82
|
openChanged(isOpen: boolean, wasOpen?: boolean) {
|
|
75
83
|
if (this.readyForMotion && wasOpen !== undefined && wasOpen !== isOpen) {
|
|
76
84
|
this.motion = isOpen ? 'opening' : 'closing';
|
|
85
|
+
this.dsChromeTransitionStart.emit({ source: 'panel-tools' });
|
|
77
86
|
}
|
|
78
87
|
this.deferMotionEnable();
|
|
79
88
|
}
|
|
@@ -97,6 +106,7 @@ export class PanelTools {
|
|
|
97
106
|
if (event.target !== this.el.querySelector('.panel-tools__drawer')) return;
|
|
98
107
|
if (event.propertyName !== 'max-width') return;
|
|
99
108
|
this.motion = 'idle';
|
|
109
|
+
this.dsChromeTransitionEnd.emit({ source: 'panel-tools' });
|
|
100
110
|
};
|
|
101
111
|
|
|
102
112
|
/** Rail selection follows `open` immediately — independent of the slide animation. */
|
package/src/wc/components.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { MenuItemData, MenuSection } from "./components/Menu/menu-types";
|
|
|
26
26
|
import { MenuAlign, MenuSide } from "./components/Menu/menu-position";
|
|
27
27
|
import { ModalWidth } from "./components/Modal/Modal";
|
|
28
28
|
import { PanelNavGroup, PanelNavRouterMode, PanelNavUserActionDetail } from "./components/PanelNav/panel-nav-types";
|
|
29
|
+
import { ChromeTransitionDetail } from "./nav/chrome-transition";
|
|
29
30
|
import { PanelToolsItem, PanelToolsToolId } from "./components/PanelTools/panel-tools-types";
|
|
30
31
|
import { RadioOption } from "./components/RadioGroup/RadioGroup";
|
|
31
32
|
import { ScrollbarVariant } from "./components/Scrollbar/Scrollbar";
|
|
@@ -63,6 +64,7 @@ export { MenuItemData, MenuSection } from "./components/Menu/menu-types";
|
|
|
63
64
|
export { MenuAlign, MenuSide } from "./components/Menu/menu-position";
|
|
64
65
|
export { ModalWidth } from "./components/Modal/Modal";
|
|
65
66
|
export { PanelNavGroup, PanelNavRouterMode, PanelNavUserActionDetail } from "./components/PanelNav/panel-nav-types";
|
|
67
|
+
export { ChromeTransitionDetail } from "./nav/chrome-transition";
|
|
66
68
|
export { PanelToolsItem, PanelToolsToolId } from "./components/PanelTools/panel-tools-types";
|
|
67
69
|
export { RadioOption } from "./components/RadioGroup/RadioGroup";
|
|
68
70
|
export { ScrollbarVariant } from "./components/Scrollbar/Scrollbar";
|
|
@@ -1476,6 +1478,8 @@ declare global {
|
|
|
1476
1478
|
interface HTMLDsPanelNavElementEventMap {
|
|
1477
1479
|
"dsNavSelect": string;
|
|
1478
1480
|
"dsNavToggle": boolean;
|
|
1481
|
+
"dsChromeTransitionStart": ChromeTransitionDetail;
|
|
1482
|
+
"dsChromeTransitionEnd": ChromeTransitionDetail;
|
|
1479
1483
|
"dsNavFooterAction": void;
|
|
1480
1484
|
"dsNavUserAction": PanelNavUserActionDetail;
|
|
1481
1485
|
}
|
|
@@ -1498,6 +1502,8 @@ declare global {
|
|
|
1498
1502
|
id: PanelToolsToolId;
|
|
1499
1503
|
selected: boolean;
|
|
1500
1504
|
};
|
|
1505
|
+
"dsChromeTransitionStart": ChromeTransitionDetail;
|
|
1506
|
+
"dsChromeTransitionEnd": ChromeTransitionDetail;
|
|
1501
1507
|
}
|
|
1502
1508
|
interface HTMLDsPanelToolsElement extends Components.DsPanelTools, HTMLStencilElement {
|
|
1503
1509
|
addEventListener<K extends keyof HTMLDsPanelToolsElementEventMap>(type: K, listener: (this: HTMLDsPanelToolsElement, ev: DsPanelToolsCustomEvent<HTMLDsPanelToolsElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
|
@@ -2378,6 +2384,11 @@ declare namespace LocalJSX {
|
|
|
2378
2384
|
* @default 'dashboard'
|
|
2379
2385
|
*/
|
|
2380
2386
|
"navStyle"?: NavChromeStyle;
|
|
2387
|
+
"onDsChromeTransitionEnd"?: (event: DsPanelNavCustomEvent<ChromeTransitionDetail>) => void;
|
|
2388
|
+
/**
|
|
2389
|
+
* Bubbling lifecycle — `ds-app-shell` pauses chrome metrics during width motion.
|
|
2390
|
+
*/
|
|
2391
|
+
"onDsChromeTransitionStart"?: (event: DsPanelNavCustomEvent<ChromeTransitionDetail>) => void;
|
|
2381
2392
|
/**
|
|
2382
2393
|
* Emitted when the footer left button (gear / dashboard) is clicked.
|
|
2383
2394
|
*/
|
|
@@ -2431,6 +2442,11 @@ declare namespace LocalJSX {
|
|
|
2431
2442
|
* @default ''
|
|
2432
2443
|
*/
|
|
2433
2444
|
"itemsJson"?: string;
|
|
2445
|
+
"onDsChromeTransitionEnd"?: (event: DsPanelToolsCustomEvent<ChromeTransitionDetail>) => void;
|
|
2446
|
+
/**
|
|
2447
|
+
* Bubbling lifecycle — `ds-bar-nav` defers overflow checks during drawer motion.
|
|
2448
|
+
*/
|
|
2449
|
+
"onDsChromeTransitionStart"?: (event: DsPanelToolsCustomEvent<ChromeTransitionDetail>) => void;
|
|
2434
2450
|
/**
|
|
2435
2451
|
* Emitted when a rail button is toggled. Detail = { id, selected }.
|
|
2436
2452
|
*/
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Bubbling/composed events — `ds-app-shell` and `ds-bar-nav` coordinate during width motion. */
|
|
2
|
+
|
|
3
|
+
export const CHROME_TRANSITION_START = 'dsChromeTransitionStart';
|
|
4
|
+
export const CHROME_TRANSITION_END = 'dsChromeTransitionEnd';
|
|
5
|
+
|
|
6
|
+
export type ChromeTransitionSource = 'panel-nav' | 'panel-tools';
|
|
7
|
+
|
|
8
|
+
export interface ChromeTransitionDetail {
|
|
9
|
+
source: ChromeTransitionSource;
|
|
10
|
+
}
|
package/src/wc/nav/index.ts
CHANGED
|
@@ -35,6 +35,19 @@ export {
|
|
|
35
35
|
shellChromeSurfacePosition,
|
|
36
36
|
} from './shell-gradient';
|
|
37
37
|
export type { ShellGradientLayout } from './shell-gradient';
|
|
38
|
+
export type { ChromeTransitionDetail, ChromeTransitionSource } from './chrome-transition';
|
|
39
|
+
export {
|
|
40
|
+
CHROME_TRANSITION_END,
|
|
41
|
+
CHROME_TRANSITION_START,
|
|
42
|
+
} from './chrome-transition';
|
|
43
|
+
export {
|
|
44
|
+
barGradientPositionFromPanelWidth,
|
|
45
|
+
isPanelNavCollapsed,
|
|
46
|
+
panelWidthPxFromTokens,
|
|
47
|
+
readCssVarWidthPx,
|
|
48
|
+
readPanelNavWidthTokens,
|
|
49
|
+
} from './shell-chrome-metrics';
|
|
50
|
+
export type { PanelNavWidthTokens } from './shell-chrome-metrics';
|
|
38
51
|
export {
|
|
39
52
|
BADGE_GRADIENT_POSITION_VAR,
|
|
40
53
|
badgeGradientPosition,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { shellGradientPositionBar } from './shell-gradient';
|
|
2
|
+
|
|
3
|
+
/** Read a `var(--token)` width in px using a hidden probe (works with calc() tokens). */
|
|
4
|
+
export function readCssVarWidthPx(context: HTMLElement, cssVarName: string): number {
|
|
5
|
+
if (typeof document === 'undefined') return 0;
|
|
6
|
+
|
|
7
|
+
const probe = document.createElement('div');
|
|
8
|
+
probe.style.cssText =
|
|
9
|
+
'position:absolute;visibility:hidden;pointer-events:none;height:0;width:var(' +
|
|
10
|
+
cssVarName +
|
|
11
|
+
');';
|
|
12
|
+
context.appendChild(probe);
|
|
13
|
+
const width = probe.getBoundingClientRect().width;
|
|
14
|
+
context.removeChild(probe);
|
|
15
|
+
return width;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PanelNavWidthTokens {
|
|
19
|
+
expandedPx: number;
|
|
20
|
+
collapsedPx: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Resolve panel-nav expanded/collapsed widths from scoped CSS vars on `.panel-nav`. */
|
|
24
|
+
export function readPanelNavWidthTokens(navRoot: HTMLElement): PanelNavWidthTokens {
|
|
25
|
+
return {
|
|
26
|
+
expandedPx: readCssVarWidthPx(navRoot, '--_nav-width'),
|
|
27
|
+
collapsedPx: readCssVarWidthPx(navRoot, '--_nav-width-collapsed'),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isPanelNavCollapsed(
|
|
32
|
+
panelNavHost: HTMLElement | null,
|
|
33
|
+
navRoot: HTMLElement | null,
|
|
34
|
+
): boolean {
|
|
35
|
+
if (navRoot?.classList.contains('panel-nav--collapsed')) return true;
|
|
36
|
+
const collapsedProp = (panelNavHost as (HTMLElement & { collapsed?: boolean }) | null)?.collapsed;
|
|
37
|
+
return collapsedProp === true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function panelWidthPxFromTokens(
|
|
41
|
+
tokens: PanelNavWidthTokens,
|
|
42
|
+
collapsed: boolean,
|
|
43
|
+
): number {
|
|
44
|
+
return collapsed ? tokens.collapsedPx : tokens.expandedPx;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function barGradientPositionFromPanelWidth(panelWidthPx: number): string {
|
|
48
|
+
return shellGradientPositionBar(panelWidthPx);
|
|
49
|
+
}
|