@awc-ui/core 1.0.0-beta.10 → 1.0.0-beta.12
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/bin/templates/astro/README.md +2 -0
- package/bin/templates/astro/package.json +4 -4
- package/bin/templates/html/index.html +2 -2
- package/bin/templates/html/package.json +2 -2
- package/bin/templates/next/README.md +5 -2
- package/bin/templates/next/package.json +8 -8
- package/bin/templates/next/tsconfig.json +2 -2
- package/bin/templates/nuxt/package.json +2 -2
- package/bin/templates/sveltekit/package.json +2 -2
- package/custom-elements.json +8 -8
- package/dist/cjs/md-navigation-rail-tab.cjs.entry.js +185 -37
- package/dist/cjs/md-navigation-rail-tab.cjs.entry.js.map +1 -1
- package/dist/cjs/md-navigation-rail.cjs.entry.js +15 -16
- package/dist/cjs/md-navigation-rail.cjs.entry.js.map +1 -1
- package/dist/cjs/navigation-rail-motion-IOseZpRz.js +90 -0
- package/dist/cjs/navigation-rail-motion-IOseZpRz.js.map +1 -0
- package/dist/collection/components/md-navigation-rail/md-navigation-rail.css +90 -85
- package/dist/collection/components/md-navigation-rail/md-navigation-rail.js +14 -15
- package/dist/collection/components/md-navigation-rail/md-navigation-rail.js.map +1 -1
- package/dist/collection/components/md-navigation-rail-tab/md-navigation-rail-tab.css +8 -11
- package/dist/collection/components/md-navigation-rail-tab/md-navigation-rail-tab.js +184 -36
- package/dist/collection/components/md-navigation-rail-tab/md-navigation-rail-tab.js.map +1 -1
- package/dist/collection/utils/navigation-rail-motion.js +80 -0
- package/dist/collection/utils/navigation-rail-motion.js.map +1 -0
- package/dist/components/md-navigation-rail-tab.js +1 -1
- package/dist/components/md-navigation-rail-tab.js.map +1 -1
- package/dist/components/md-navigation-rail.js +1 -1
- package/dist/components/md-navigation-rail.js.map +1 -1
- package/dist/components/p-BTQOOQYQ.js +2 -0
- package/dist/components/p-BTQOOQYQ.js.map +1 -0
- package/dist/components-csr/md-navigation-rail-tab.js +1 -1
- package/dist/components-csr/md-navigation-rail-tab.js.map +1 -1
- package/dist/components-csr/md-navigation-rail.js +1 -1
- package/dist/components-csr/md-navigation-rail.js.map +1 -1
- package/dist/components-csr/p-BTQOOQYQ.js +2 -0
- package/dist/components-csr/p-BTQOOQYQ.js.map +1 -0
- package/dist/esm/md-navigation-rail-tab.entry.js +185 -37
- package/dist/esm/md-navigation-rail-tab.entry.js.map +1 -1
- package/dist/esm/md-navigation-rail.entry.js +15 -16
- package/dist/esm/md-navigation-rail.entry.js.map +1 -1
- package/dist/esm/navigation-rail-motion-BTQOOQYQ.js +84 -0
- package/dist/esm/navigation-rail-motion-BTQOOQYQ.js.map +1 -0
- package/dist/md3/md3.esm.js +1 -1
- package/dist/md3/p-88d49766.entry.js +2 -0
- package/dist/md3/p-88d49766.entry.js.map +1 -0
- package/dist/md3/p-BTQOOQYQ.js +2 -0
- package/dist/md3/p-BTQOOQYQ.js.map +1 -0
- package/dist/md3/p-c38e1473.entry.js +2 -0
- package/dist/md3/p-c38e1473.entry.js.map +1 -0
- package/dist/sources/{20812305a6a9bf6e0ac3bcaa49cf729c7e1f1d944f6467cc179222ebbfe7c036 → 0ff3480d9a3ad4d643358cd2d23546b2045fcacf8afbea2e0280ca4d1d7c2d09}/md-navigation-rail.tsx +12 -12
- package/dist/sources/{6c14a0a0c65f2aec64d62332f49f228b81d0cbbb288795543716266e9f70f444 → 2f4b6a7ed1aaf6a2a7b0e44123a4b5d30a18047aa5c2121896389bd6d11fcc66}/md-navigation-rail.css_tag_md-navigation-rail_encapsulation_shadow +90 -85
- package/dist/sources/{822acc654bb272fea6b5455c9869791448b125cae2c1eb1df0f97b27263a18f3 → 67e302a0ddf9f68ba82638d32533a0e69408184931f9f1268243f7246c714ae8}/md-navigation-rail-tab.css_tag_md-navigation-rail-tab_encapsulation_shadow +8 -11
- package/dist/sources/{4b1bf2d27bc87d12e416a1ae2bdc0f361033c40d13b252fe4c9f51a197ca95e1 → b3d606320b653bcbde553e6d61569edd0fc6cbdb1cb179f4d36ad796e76b133c}/md-navigation-rail-tab.tsx +208 -45
- package/dist/sources/f41e60167e8f17a6297648047951ef2d3dd96a3652adecfbe959df57bdef48dd/navigation-rail-motion.ts +97 -0
- package/dist/types/components/md-navigation-rail/md-navigation-rail.d.ts +3 -5
- package/dist/types/components/md-navigation-rail-tab/md-navigation-rail-tab.d.ts +17 -10
- package/dist/types/utils/navigation-rail-motion.d.ts +17 -0
- package/hydrate/index.js +278 -53
- package/hydrate/index.mjs +278 -53
- package/package.json +2 -2
- package/src/components/md-navigation-rail/readme.md +8 -0
- package/src/components/md-navigation-rail-tab/readme.md +6 -2
- package/dist/md3/p-7ca4c543.entry.js +0 -2
- package/dist/md3/p-7ca4c543.entry.js.map +0 -1
- package/dist/md3/p-9acadde5.entry.js +0 -2
- package/dist/md3/p-9acadde5.entry.js.map +0 -1
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
Method,
|
|
12
12
|
} from '@stencil/core';
|
|
13
13
|
import { triggerRipple } from '../../utils/ripple';
|
|
14
|
+
import { getRailTransitionTiming, parseRailMotionDuration, registerRailTabTransition, synchronizeRailAnimation } from '../../utils/navigation-rail-motion';
|
|
14
15
|
import { sanitizeHref, SAFE_LINK_REL } from '../../utils/url';
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -92,9 +93,52 @@ export class MdNavigationRailTab {
|
|
|
92
93
|
*/
|
|
93
94
|
@Event({ bubbles: true, composed: true }) mdSubmenuToggle: EventEmitter<{ open: boolean }>;
|
|
94
95
|
|
|
95
|
-
/**
|
|
96
|
+
/** Rendered geometry captured before the rail or destination changes layout. */
|
|
96
97
|
private iconFirst: { cx: number; cy: number } | null = null;
|
|
98
|
+
private indicatorFirst: { left: number; top: number; width: number; height: number } | null = null;
|
|
99
|
+
private labelFirst: {
|
|
100
|
+
rect: DOMRect;
|
|
101
|
+
opacity: number;
|
|
102
|
+
visible: boolean;
|
|
103
|
+
fontFamily: string;
|
|
104
|
+
fontSize: string;
|
|
105
|
+
fontWeight: string;
|
|
106
|
+
lineHeight: string;
|
|
107
|
+
letterSpacing: string;
|
|
108
|
+
paddingTop: string;
|
|
109
|
+
paddingRight: string;
|
|
110
|
+
paddingBottom: string;
|
|
111
|
+
paddingLeft: string;
|
|
112
|
+
textAlign: string;
|
|
113
|
+
boxSizing: string;
|
|
114
|
+
} | null = null;
|
|
115
|
+
private labelOverlay?: {
|
|
116
|
+
element: HTMLElement;
|
|
117
|
+
styles: Map<string, { value: string; priority: string }>;
|
|
118
|
+
};
|
|
97
119
|
private pendingTransition = false;
|
|
120
|
+
private transitionPrepared = false;
|
|
121
|
+
private iconAnimation?: Animation;
|
|
122
|
+
private indicatorAnimation?: Animation;
|
|
123
|
+
private labelAnimation?: Animation;
|
|
124
|
+
private unregisterTransition?: () => void;
|
|
125
|
+
|
|
126
|
+
connectedCallback() {
|
|
127
|
+
this.unregisterTransition = registerRailTabTransition(this.el, () => {
|
|
128
|
+
this.captureTransition();
|
|
129
|
+
this.transitionPrepared = true;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
disconnectedCallback() {
|
|
134
|
+
this.unregisterTransition?.();
|
|
135
|
+
this.cancelAnimations();
|
|
136
|
+
this.iconFirst = null;
|
|
137
|
+
this.indicatorFirst = null;
|
|
138
|
+
this.labelFirst = null;
|
|
139
|
+
this.pendingTransition = false;
|
|
140
|
+
this.transitionPrepared = false;
|
|
141
|
+
}
|
|
98
142
|
|
|
99
143
|
/** Mirrors the slotted submenu's open state for `aria-expanded`. */
|
|
100
144
|
@State() private submenuOpen: boolean = false;
|
|
@@ -131,20 +175,95 @@ export class MdNavigationRailTab {
|
|
|
131
175
|
}
|
|
132
176
|
}
|
|
133
177
|
|
|
134
|
-
/**
|
|
135
|
-
*
|
|
136
|
-
* slightly different spots. To avoid an abrupt snap we FLIP it: capture the
|
|
137
|
-
* old centre before the layout flips, then (in componentDidRender) glide it
|
|
138
|
-
* to the new centre with a smooth DECELERATE curve — deliberately not the
|
|
139
|
-
* rail's springy/overshoot easing, which made the small move look like a
|
|
140
|
-
* floaty wobble. The label can't slide cleanly (its path cuts through the
|
|
141
|
-
* icon), so it cross-fades into its new position instead.
|
|
142
|
-
*/
|
|
178
|
+
/** The rail prepares before its own padding changes. Direct prop changes
|
|
179
|
+
* still capture here, so a destination also behaves correctly in isolation. */
|
|
143
180
|
@Watch('expanded')
|
|
144
181
|
onExpandedChange() {
|
|
145
|
-
if (this.
|
|
182
|
+
if (!this.transitionPrepared) this.captureTransition();
|
|
183
|
+
this.transitionPrepared = false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private captureTransition() {
|
|
187
|
+
const sr = this.el.shadowRoot;
|
|
188
|
+
const indicator = sr?.querySelector<HTMLElement>('[part="indicator"]');
|
|
189
|
+
const label = sr?.querySelector<HTMLElement>('[part="label"]');
|
|
190
|
+
// Read the visible (possibly mid-animation) boxes BEFORE cancelling. A
|
|
191
|
+
// reversal then starts at the pixels on screen, not the previous target.
|
|
146
192
|
this.iconFirst = this.iconCentre();
|
|
147
|
-
this.
|
|
193
|
+
this.indicatorFirst = indicator?.getBoundingClientRect() ?? null;
|
|
194
|
+
const labelCss = label ? getComputedStyle(label) : null;
|
|
195
|
+
this.labelFirst = label && labelCss ? {
|
|
196
|
+
rect: label.getBoundingClientRect(),
|
|
197
|
+
opacity: Number.parseFloat(labelCss.opacity) || 0,
|
|
198
|
+
visible: labelCss.display !== 'none' && labelCss.visibility !== 'hidden',
|
|
199
|
+
fontFamily: labelCss.fontFamily,
|
|
200
|
+
fontSize: labelCss.fontSize,
|
|
201
|
+
fontWeight: labelCss.fontWeight,
|
|
202
|
+
lineHeight: labelCss.lineHeight,
|
|
203
|
+
letterSpacing: labelCss.letterSpacing,
|
|
204
|
+
paddingTop: labelCss.paddingTop,
|
|
205
|
+
paddingRight: labelCss.paddingRight,
|
|
206
|
+
paddingBottom: labelCss.paddingBottom,
|
|
207
|
+
paddingLeft: labelCss.paddingLeft,
|
|
208
|
+
textAlign: labelCss.textAlign,
|
|
209
|
+
boxSizing: labelCss.boxSizing,
|
|
210
|
+
} : null;
|
|
211
|
+
this.cancelAnimations();
|
|
212
|
+
this.pendingTransition = !this.prefersReducedMotion();
|
|
213
|
+
if (!this.pendingTransition) {
|
|
214
|
+
this.iconFirst = null;
|
|
215
|
+
this.indicatorFirst = null;
|
|
216
|
+
this.labelFirst = null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private cancelAnimations() {
|
|
221
|
+
this.iconAnimation?.cancel();
|
|
222
|
+
this.indicatorAnimation?.cancel();
|
|
223
|
+
this.labelAnimation?.cancel();
|
|
224
|
+
this.iconAnimation = undefined;
|
|
225
|
+
this.indicatorAnimation = undefined;
|
|
226
|
+
this.labelAnimation = undefined;
|
|
227
|
+
this.restoreLabelOverlay();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private restoreLabelOverlay() {
|
|
231
|
+
if (!this.labelOverlay) return;
|
|
232
|
+
const { element, styles } = this.labelOverlay;
|
|
233
|
+
for (const [name, previous] of styles) {
|
|
234
|
+
if (previous.value) element.style.setProperty(name, previous.value, previous.priority);
|
|
235
|
+
else element.style.removeProperty(name);
|
|
236
|
+
}
|
|
237
|
+
this.labelOverlay = undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private retainOutgoingLabel(label: HTMLElement, first: NonNullable<MdNavigationRailTab['labelFirst']>) {
|
|
241
|
+
const host = this.el.getBoundingClientRect();
|
|
242
|
+
const styles = new Map<string, { value: string; priority: string }>();
|
|
243
|
+
const overlay: Record<string, string> = {
|
|
244
|
+
display: 'inline-block', position: 'absolute',
|
|
245
|
+
left: `${first.rect.left - host.left - this.el.clientLeft + this.el.scrollLeft}px`,
|
|
246
|
+
top: `${first.rect.top - host.top - this.el.clientTop + this.el.scrollTop}px`,
|
|
247
|
+
width: `${first.rect.width}px`, height: `${first.rect.height}px`,
|
|
248
|
+
'max-inline-size': 'none', 'max-block-size': 'none',
|
|
249
|
+
'margin-top': '0', 'margin-right': '0', 'margin-bottom': '0', 'margin-left': '0',
|
|
250
|
+
transform: 'none', opacity: String(first.opacity), visibility: 'visible',
|
|
251
|
+
'pointer-events': 'none', 'transition-property': 'none',
|
|
252
|
+
// Longhands preserve authored font/margin/padding/transition values when
|
|
253
|
+
// cleanup restores each property; clearing a shorthand would erase them.
|
|
254
|
+
'font-family': first.fontFamily, 'font-size': first.fontSize,
|
|
255
|
+
'font-weight': first.fontWeight, 'line-height': first.lineHeight,
|
|
256
|
+
'letter-spacing': first.letterSpacing,
|
|
257
|
+
'padding-top': first.paddingTop, 'padding-right': first.paddingRight,
|
|
258
|
+
'padding-bottom': first.paddingBottom, 'padding-left': first.paddingLeft,
|
|
259
|
+
'text-align': first.textAlign, 'box-sizing': first.boxSizing,
|
|
260
|
+
};
|
|
261
|
+
for (const [name, value] of Object.entries(overlay)) {
|
|
262
|
+
if (!value) continue;
|
|
263
|
+
styles.set(name, { value: label.style.getPropertyValue(name), priority: label.style.getPropertyPriority(name) });
|
|
264
|
+
label.style.setProperty(name, value);
|
|
265
|
+
}
|
|
266
|
+
this.labelOverlay = { element: label, styles };
|
|
148
267
|
}
|
|
149
268
|
|
|
150
269
|
componentDidRender() {
|
|
@@ -180,54 +299,98 @@ export class MdNavigationRailTab {
|
|
|
180
299
|
const raw = getComputedStyle(this.el)
|
|
181
300
|
.getPropertyValue('--md-sys-motion-duration-short4')
|
|
182
301
|
.trim();
|
|
183
|
-
|
|
184
|
-
if (raw.endsWith('s')) return (parseFloat(raw) || 0.2) * 1000;
|
|
185
|
-
return 200;
|
|
302
|
+
return parseRailMotionDuration(raw);
|
|
186
303
|
}
|
|
187
304
|
|
|
188
305
|
private playTransition() {
|
|
189
306
|
const sr = this.el.shadowRoot;
|
|
190
307
|
if (!sr) return;
|
|
191
|
-
const
|
|
308
|
+
const fallbackEasing = getComputedStyle(this.el)
|
|
309
|
+
.getPropertyValue('--md-sys-motion-easing-standard').trim()
|
|
310
|
+
|| 'cubic-bezier(0.2, 0, 0, 1)';
|
|
311
|
+
const { duration, easing, source } = getRailTransitionTiming(this.el, {
|
|
312
|
+
duration: this.motionDuration(), easing: fallbackEasing,
|
|
313
|
+
});
|
|
314
|
+
if (duration === 0) {
|
|
315
|
+
this.iconFirst = null;
|
|
316
|
+
this.indicatorFirst = null;
|
|
317
|
+
this.labelFirst = null;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const timing: KeyframeAnimationOptions = { duration, easing, fill: 'none' };
|
|
192
321
|
|
|
193
|
-
|
|
194
|
-
const icon = sr.querySelector(
|
|
195
|
-
'.md-navigation-rail-tab__icon-wrapper',
|
|
196
|
-
) as HTMLElement | null;
|
|
322
|
+
const icon = sr.querySelector<HTMLElement>('[part="icon-wrapper"]');
|
|
197
323
|
if (icon && this.iconFirst) {
|
|
198
324
|
const r = icon.getBoundingClientRect();
|
|
199
325
|
const dx = this.iconFirst.cx - (r.left + r.width / 2);
|
|
200
326
|
const dy = this.iconFirst.cy - (r.top + r.height / 2);
|
|
201
327
|
if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) {
|
|
202
|
-
icon.animate(
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
duration,
|
|
209
|
-
// Standard easing — quick in, snappy settle, no bounce — matching
|
|
210
|
-
// the rail width morph and the button shape-morph feel.
|
|
211
|
-
easing:
|
|
212
|
-
getComputedStyle(this.el)
|
|
213
|
-
.getPropertyValue('--md-sys-motion-easing-standard')
|
|
214
|
-
.trim() || 'cubic-bezier(0.2, 0, 0, 1)',
|
|
215
|
-
fill: 'none',
|
|
216
|
-
},
|
|
217
|
-
);
|
|
328
|
+
this.iconAnimation = icon.animate([
|
|
329
|
+
{ transform: `translate(${dx}px, ${dy}px)` },
|
|
330
|
+
{ transform: 'translate(0, 0)' },
|
|
331
|
+
], timing);
|
|
332
|
+
const animation = this.iconAnimation;
|
|
333
|
+
synchronizeRailAnimation(animation, source, () => this.iconAnimation === animation);
|
|
218
334
|
}
|
|
219
335
|
}
|
|
220
336
|
this.iconFirst = null;
|
|
221
337
|
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
338
|
+
// CSS cannot interpolate a content-sized expanded pill to a fixed collapsed
|
|
339
|
+
// pill. Morph only its surface; scaling the whole destination distorts text.
|
|
340
|
+
const indicator = sr.querySelector<HTMLElement>('[part="indicator"]');
|
|
341
|
+
if (indicator && this.indicatorFirst) {
|
|
342
|
+
const first = this.indicatorFirst;
|
|
343
|
+
const last = indicator.getBoundingClientRect();
|
|
344
|
+
if (first.width > 0 && first.height > 0 && last.width > 0 && last.height > 0) {
|
|
345
|
+
this.indicatorAnimation = indicator.animate([
|
|
346
|
+
{
|
|
347
|
+
transform: `translate(${first.left - last.left}px, ${first.top - last.top}px) scale(${first.width / last.width}, ${first.height / last.height})`,
|
|
348
|
+
transformOrigin: 'top left',
|
|
349
|
+
},
|
|
350
|
+
{ transform: 'translate(0, 0) scale(1, 1)', transformOrigin: 'top left' },
|
|
351
|
+
], timing);
|
|
352
|
+
const animation = this.indicatorAnimation;
|
|
353
|
+
synchronizeRailAnimation(animation, source, () => this.indicatorAnimation === animation);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
this.indicatorFirst = null;
|
|
357
|
+
|
|
358
|
+
const label = sr.querySelector<HTMLElement>('[part="label"]');
|
|
359
|
+
const first = this.labelFirst;
|
|
360
|
+
this.labelFirst = null;
|
|
361
|
+
if (label && first) {
|
|
362
|
+
const css = getComputedStyle(label);
|
|
363
|
+
const targetOpacity = Number.parseFloat(css.opacity) || 0;
|
|
364
|
+
const visible = css.display !== 'none' && css.visibility !== 'hidden' && targetOpacity > 0;
|
|
365
|
+
if (!visible && first.visible && first.opacity > 0 && first.rect.width > 0 && first.rect.height > 0) {
|
|
366
|
+
// Preserve only the outgoing label's painted box. It no longer takes
|
|
367
|
+
// layout space, so the collapsed icon and pill can reach their targets.
|
|
368
|
+
this.retainOutgoingLabel(label, first);
|
|
369
|
+
this.labelAnimation = label.animate([
|
|
370
|
+
{ opacity: first.opacity }, { opacity: 0 },
|
|
371
|
+
], { ...timing, fill: 'forwards' });
|
|
372
|
+
const animation = this.labelAnimation;
|
|
373
|
+
synchronizeRailAnimation(animation, source, () => this.labelAnimation === animation);
|
|
374
|
+
void animation.finished.then(() => {
|
|
375
|
+
if (this.labelAnimation !== animation) return;
|
|
376
|
+
this.restoreLabelOverlay();
|
|
377
|
+
this.labelAnimation = undefined;
|
|
378
|
+
animation.cancel();
|
|
379
|
+
}, () => undefined);
|
|
380
|
+
} else if (visible) {
|
|
381
|
+
const last = label.getBoundingClientRect();
|
|
382
|
+
const dx = first.visible ? first.rect.left - last.left : 0;
|
|
383
|
+
const dy = first.visible ? first.rect.top - last.top : 0;
|
|
384
|
+
const opacity = first.visible ? first.opacity : 0;
|
|
385
|
+
if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5 || Math.abs(opacity - targetOpacity) > 0.01) {
|
|
386
|
+
this.labelAnimation = label.animate([
|
|
387
|
+
{ transform: `translate(${dx}px, ${dy}px)`, opacity },
|
|
388
|
+
{ transform: 'translate(0, 0)', opacity: targetOpacity },
|
|
389
|
+
], timing);
|
|
390
|
+
const animation = this.labelAnimation;
|
|
391
|
+
synchronizeRailAnimation(animation, source, () => this.labelAnimation === animation);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
231
394
|
}
|
|
232
395
|
}
|
|
233
396
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** Private bridge for capturing tab geometry before its rail changes layout. */
|
|
2
|
+
const transitionPreparations = new WeakMap<HTMLElement, () => void>();
|
|
3
|
+
|
|
4
|
+
export function registerRailTabTransition(
|
|
5
|
+
element: HTMLElement,
|
|
6
|
+
callback: () => void,
|
|
7
|
+
): () => void {
|
|
8
|
+
transitionPreparations.set(element, callback);
|
|
9
|
+
return () => {
|
|
10
|
+
// A disconnected instance must not unregister a newer connection's callback.
|
|
11
|
+
if (transitionPreparations.get(element) === callback) {
|
|
12
|
+
transitionPreparations.delete(element);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function prepareRailTabTransition(element: HTMLElement): void {
|
|
18
|
+
transitionPreparations.get(element)?.();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RailTransitionTiming {
|
|
22
|
+
duration: number;
|
|
23
|
+
easing: string;
|
|
24
|
+
/** The actual width transition, including its shortened reversal clock. */
|
|
25
|
+
source?: Animation;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseRailMotionDuration(raw: string, fallback = 200): number {
|
|
29
|
+
const value = raw.trim();
|
|
30
|
+
if (!/^(?:\d*\.)?\d+(?:ms|s)?$/.test(value)) return fallback;
|
|
31
|
+
const duration = Number.parseFloat(value) * (value.endsWith('s') && !value.endsWith('ms') ? 1000 : 1);
|
|
32
|
+
return Number.isFinite(duration) && duration >= 0 ? duration : fallback;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* CSS shortens a transition when it reverses. Reading the authored token alone
|
|
37
|
+
* would restart a destination's animation for the full duration while its rail
|
|
38
|
+
* had already finished closing. Use the browser's actual width-animation clock.
|
|
39
|
+
*/
|
|
40
|
+
export function getRailTransitionTiming(
|
|
41
|
+
destination: HTMLElement,
|
|
42
|
+
fallback: RailTransitionTiming,
|
|
43
|
+
): RailTransitionTiming {
|
|
44
|
+
const rail = destination.closest('md-navigation-rail') as HTMLElement | null;
|
|
45
|
+
if (!rail) return fallback;
|
|
46
|
+
const surface = rail.hasAttribute('modal')
|
|
47
|
+
? rail.shadowRoot?.querySelector<HTMLElement>('[part="container"]') ?? rail
|
|
48
|
+
: rail;
|
|
49
|
+
// Flush the target layout before reading its generated CSS transitions.
|
|
50
|
+
surface.getBoundingClientRect();
|
|
51
|
+
const animations = typeof surface.getAnimations === 'function' ? surface.getAnimations() : [];
|
|
52
|
+
const source = animations.find((animation) => {
|
|
53
|
+
const property = (animation as Animation & { transitionProperty?: string }).transitionProperty;
|
|
54
|
+
return (property === 'width' || property === 'inline-size') && animation.playState !== 'finished';
|
|
55
|
+
});
|
|
56
|
+
if (source?.effect) {
|
|
57
|
+
const effect = source.effect as KeyframeEffect;
|
|
58
|
+
const timing = effect.getTiming();
|
|
59
|
+
const duration = effect.getComputedTiming().duration;
|
|
60
|
+
if (typeof duration === 'number' && Number.isFinite(duration) && duration >= 0) {
|
|
61
|
+
// CSS transitions may put their curve on the first keyframe instead of
|
|
62
|
+
// the effect; preserve that curve instead of silently substituting linear.
|
|
63
|
+
const keyframeEasing = effect.getKeyframes?.()[0]?.easing;
|
|
64
|
+
return {
|
|
65
|
+
duration,
|
|
66
|
+
easing: keyframeEasing && keyframeEasing !== 'linear'
|
|
67
|
+
? keyframeEasing : timing.easing || fallback.easing,
|
|
68
|
+
source,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// A zero-duration transition does not appear in getAnimations().
|
|
73
|
+
const css = getComputedStyle(surface);
|
|
74
|
+
const properties = css.transitionProperty?.split(',').map((value) => value.trim()) ?? [];
|
|
75
|
+
const index = properties.findIndex((value) => ['all', 'width', 'inline-size'].includes(value));
|
|
76
|
+
const durations = css.transitionDuration?.split(',') ?? [];
|
|
77
|
+
if (properties.includes('none') || (index >= 0 && durations.length && parseRailMotionDuration(durations[index % durations.length]) === 0)) {
|
|
78
|
+
return { ...fallback, duration: 0 };
|
|
79
|
+
}
|
|
80
|
+
return fallback;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Align with a width transition that may still be waiting for its first frame. */
|
|
84
|
+
export function synchronizeRailAnimation(
|
|
85
|
+
animation: Animation,
|
|
86
|
+
source: Animation | undefined,
|
|
87
|
+
isCurrent: () => boolean,
|
|
88
|
+
): void {
|
|
89
|
+
if (!source) return;
|
|
90
|
+
const align = () => {
|
|
91
|
+
if (isCurrent() && typeof source.startTime === 'number') animation.startTime = source.startTime;
|
|
92
|
+
};
|
|
93
|
+
align();
|
|
94
|
+
if (source.startTime === null && source.ready) {
|
|
95
|
+
void source.ready.then(align, () => undefined);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -107,11 +107,9 @@ export declare class MdNavigationRail {
|
|
|
107
107
|
* context so the fixed-position menu is not trapped inside it. */
|
|
108
108
|
private submenuOpen;
|
|
109
109
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* animation has finished, then drops to the stacked resting layout — keeping
|
|
114
|
-
* the label reveal/conceal in sync with the width instead of snapping.
|
|
110
|
+
* Destination target layout, synchronized with the variant before rendering.
|
|
111
|
+
* Tabs capture their current geometry before this changes, then animate to the
|
|
112
|
+
* new layout while the rail changes width.
|
|
115
113
|
*/
|
|
116
114
|
private tabsExpanded;
|
|
117
115
|
/** Programmatically expand the rail (shows labels inline with icons). */
|
|
@@ -61,9 +61,19 @@ export declare class MdNavigationRailTab {
|
|
|
61
61
|
mdSubmenuToggle: EventEmitter<{
|
|
62
62
|
open: boolean;
|
|
63
63
|
}>;
|
|
64
|
-
/**
|
|
64
|
+
/** Rendered geometry captured before the rail or destination changes layout. */
|
|
65
65
|
private iconFirst;
|
|
66
|
+
private indicatorFirst;
|
|
67
|
+
private labelFirst;
|
|
68
|
+
private labelOverlay?;
|
|
66
69
|
private pendingTransition;
|
|
70
|
+
private transitionPrepared;
|
|
71
|
+
private iconAnimation?;
|
|
72
|
+
private indicatorAnimation?;
|
|
73
|
+
private labelAnimation?;
|
|
74
|
+
private unregisterTransition?;
|
|
75
|
+
connectedCallback(): void;
|
|
76
|
+
disconnectedCallback(): void;
|
|
67
77
|
/** Mirrors the slotted submenu's open state for `aria-expanded`. */
|
|
68
78
|
private submenuOpen;
|
|
69
79
|
/** True when a dropdown is slotted, so the label can show a disclosure caret. */
|
|
@@ -72,16 +82,13 @@ export declare class MdNavigationRailTab {
|
|
|
72
82
|
* destination itself reads as the active one. */
|
|
73
83
|
private submenuHasSelection;
|
|
74
84
|
componentDidLoad(): void;
|
|
75
|
-
/**
|
|
76
|
-
*
|
|
77
|
-
* slightly different spots. To avoid an abrupt snap we FLIP it: capture the
|
|
78
|
-
* old centre before the layout flips, then (in componentDidRender) glide it
|
|
79
|
-
* to the new centre with a smooth DECELERATE curve — deliberately not the
|
|
80
|
-
* rail's springy/overshoot easing, which made the small move look like a
|
|
81
|
-
* floaty wobble. The label can't slide cleanly (its path cuts through the
|
|
82
|
-
* icon), so it cross-fades into its new position instead.
|
|
83
|
-
*/
|
|
85
|
+
/** The rail prepares before its own padding changes. Direct prop changes
|
|
86
|
+
* still capture here, so a destination also behaves correctly in isolation. */
|
|
84
87
|
onExpandedChange(): void;
|
|
88
|
+
private captureTransition;
|
|
89
|
+
private cancelAnimations;
|
|
90
|
+
private restoreLabelOverlay;
|
|
91
|
+
private retainOutgoingLabel;
|
|
85
92
|
componentDidRender(): void;
|
|
86
93
|
private prefersReducedMotion;
|
|
87
94
|
private iconCentre;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare function registerRailTabTransition(element: HTMLElement, callback: () => void): () => void;
|
|
2
|
+
export declare function prepareRailTabTransition(element: HTMLElement): void;
|
|
3
|
+
export interface RailTransitionTiming {
|
|
4
|
+
duration: number;
|
|
5
|
+
easing: string;
|
|
6
|
+
/** The actual width transition, including its shortened reversal clock. */
|
|
7
|
+
source?: Animation;
|
|
8
|
+
}
|
|
9
|
+
export declare function parseRailMotionDuration(raw: string, fallback?: number): number;
|
|
10
|
+
/**
|
|
11
|
+
* CSS shortens a transition when it reverses. Reading the authored token alone
|
|
12
|
+
* would restart a destination's animation for the full duration while its rail
|
|
13
|
+
* had already finished closing. Use the browser's actual width-animation clock.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getRailTransitionTiming(destination: HTMLElement, fallback: RailTransitionTiming): RailTransitionTiming;
|
|
16
|
+
/** Align with a width transition that may still be waiting for its first frame. */
|
|
17
|
+
export declare function synchronizeRailAnimation(animation: Animation, source: Animation | undefined, isCurrent: () => boolean): void;
|