@jskit-ai/shell-web 0.1.148 → 0.1.150
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/CHANGELOG.md +9 -0
- package/fixtures/adaptive-shell/index.html +12 -0
- package/fixtures/adaptive-shell/src/App.vue +27 -0
- package/fixtures/adaptive-shell/src/ScreenPage.vue +14 -0
- package/fixtures/adaptive-shell/src/main.js +197 -0
- package/fixtures/adaptive-shell/vite.config.mjs +17 -0
- package/package.descriptor.mjs +2 -2
- package/package.json +2 -2
- package/src/client/components/ShellLayout.vue +283 -13
- package/src/client/components/ShellMenuLinkItem.vue +18 -10
- package/src/client/components/ShellNavigationTooltip.vue +37 -0
- package/src/client/components/ShellSurfaceAwareMenuLinkItem.vue +18 -10
- package/src/client/support/drawerPresentation.js +58 -0
- package/src/client/support/drawerWidth.js +74 -0
- package/src/client/support/navigationLinkKeyboard.js +10 -0
- package/src/test/adaptiveShellSmoke.js +180 -4
- package/templates/src/components/ShellLayout.vue +1 -0
- package/templates/src/pages/home/settings/general/index.vue +2 -2
- package/test/adaptiveShell.browser.test.js +290 -0
- package/test/drawerPresentation.test.js +68 -0
- package/test/drawerWidth.test.js +48 -0
- package/test/linkItemScaffoldContract.test.js +20 -0
- package/test/navigationLinkKeyboard.test.js +36 -0
- package/test/playwrightContract.test.js +15 -0
- package/test/settingsPlacementContract.test.js +21 -3
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
computed,
|
|
4
4
|
inject,
|
|
5
|
+
nextTick,
|
|
5
6
|
onBeforeUnmount,
|
|
6
7
|
onMounted,
|
|
7
8
|
ref,
|
|
@@ -9,6 +10,18 @@ import {
|
|
|
9
10
|
} from "vue";
|
|
10
11
|
import { useDisplay } from "vuetify";
|
|
11
12
|
import { useShellLayoutState } from "../composables/useShellLayoutState.js";
|
|
13
|
+
import {
|
|
14
|
+
resolveShellDrawerPresentation,
|
|
15
|
+
resolveShellDrawerToggleLabel
|
|
16
|
+
} from "../support/drawerPresentation.js";
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_SHELL_DRAWER_WIDTH,
|
|
19
|
+
DEFAULT_SHELL_RAIL_WIDTH,
|
|
20
|
+
SHELL_DRAWER_LABEL_END_GAP,
|
|
21
|
+
normalizeShellDrawerWidth,
|
|
22
|
+
normalizeShellRailWidth,
|
|
23
|
+
resolveContentAwareDrawerWidth
|
|
24
|
+
} from "../support/drawerWidth.js";
|
|
12
25
|
import ShellOutlet from "./ShellOutlet.vue";
|
|
13
26
|
import ShellRouteTransition from "./ShellRouteTransition.vue";
|
|
14
27
|
|
|
@@ -28,6 +41,18 @@ const props = defineProps({
|
|
|
28
41
|
subtitle: {
|
|
29
42
|
type: String,
|
|
30
43
|
default: ""
|
|
44
|
+
},
|
|
45
|
+
desktopDrawerClosedMode: {
|
|
46
|
+
type: String,
|
|
47
|
+
default: "rail"
|
|
48
|
+
},
|
|
49
|
+
drawerWidth: {
|
|
50
|
+
type: Number,
|
|
51
|
+
default: null
|
|
52
|
+
},
|
|
53
|
+
railWidth: {
|
|
54
|
+
type: Number,
|
|
55
|
+
default: DEFAULT_SHELL_RAIL_WIDTH
|
|
31
56
|
}
|
|
32
57
|
});
|
|
33
58
|
|
|
@@ -47,7 +72,14 @@ const display = useDisplay();
|
|
|
47
72
|
const refreshRuntime = inject("jskit.shell-web.runtime.web-refresh.client", null);
|
|
48
73
|
const pullDistance = ref(0);
|
|
49
74
|
const pullRefreshing = ref(false);
|
|
75
|
+
const wideDrawerOpen = ref(Boolean(drawerDefaultOpen.value));
|
|
76
|
+
const navigationToggle = ref(null);
|
|
77
|
+
const navigationDrawer = ref(null);
|
|
78
|
+
const measuredDrawerWidth = ref(DEFAULT_SHELL_DRAWER_WIDTH);
|
|
50
79
|
let activePull = null;
|
|
80
|
+
let drawerContentObserver = null;
|
|
81
|
+
let drawerMeasurementFrame = null;
|
|
82
|
+
let shellMounted = false;
|
|
51
83
|
|
|
52
84
|
const PULL_REFRESH_TRIGGER_DISTANCE = 72;
|
|
53
85
|
const PULL_REFRESH_MAX_DISTANCE = 112;
|
|
@@ -63,6 +95,22 @@ const layoutClass = computed(() => {
|
|
|
63
95
|
return "expanded";
|
|
64
96
|
});
|
|
65
97
|
const isCompactLayout = computed(() => layoutClass.value === "compact");
|
|
98
|
+
const drawerPresentation = computed(() => resolveShellDrawerPresentation({
|
|
99
|
+
compact: isCompactLayout.value,
|
|
100
|
+
open: drawerOpen.value,
|
|
101
|
+
desktopClosedMode: props.desktopDrawerClosedMode
|
|
102
|
+
}));
|
|
103
|
+
const drawerToggleLabel = computed(() => resolveShellDrawerToggleLabel({
|
|
104
|
+
compact: isCompactLayout.value,
|
|
105
|
+
open: drawerOpen.value
|
|
106
|
+
}));
|
|
107
|
+
const resolvedDrawerWidth = computed(() => {
|
|
108
|
+
if (props.drawerWidth !== null && props.drawerWidth !== undefined) {
|
|
109
|
+
return normalizeShellDrawerWidth(props.drawerWidth);
|
|
110
|
+
}
|
|
111
|
+
return normalizeShellDrawerWidth(measuredDrawerWidth.value);
|
|
112
|
+
});
|
|
113
|
+
const resolvedRailWidth = computed(() => normalizeShellRailWidth(props.railWidth));
|
|
66
114
|
const pullProgress = computed(() =>
|
|
67
115
|
Math.min(100, Math.round((pullDistance.value / PULL_REFRESH_TRIGGER_DISTANCE) * 100))
|
|
68
116
|
);
|
|
@@ -79,19 +127,31 @@ const pullRefreshStyle = computed(() => ({
|
|
|
79
127
|
"--shell-pull-refresh-distance": `${Math.round(Math.min(pullDistance.value, PULL_REFRESH_MAX_DISTANCE))}px`
|
|
80
128
|
}));
|
|
81
129
|
|
|
130
|
+
watch(
|
|
131
|
+
drawerOpen,
|
|
132
|
+
handleDrawerOpenChange
|
|
133
|
+
);
|
|
134
|
+
|
|
82
135
|
watch(
|
|
83
136
|
isCompactLayout,
|
|
84
|
-
|
|
85
|
-
setDrawerOpen(compact ? false : drawerDefaultOpen.value);
|
|
86
|
-
},
|
|
137
|
+
handleLayoutClassChange,
|
|
87
138
|
{ immediate: true }
|
|
88
139
|
);
|
|
89
140
|
|
|
90
|
-
|
|
141
|
+
watch(drawerPresentation, scheduleDrawerWidthMeasurement, { flush: "post" });
|
|
142
|
+
watch(resolvedSurface, scheduleDrawerWidthMeasurement, { flush: "post" });
|
|
143
|
+
|
|
144
|
+
onMounted(attachShellListeners);
|
|
145
|
+
onBeforeUnmount(detachShellListeners);
|
|
146
|
+
|
|
147
|
+
function attachShellListeners() {
|
|
148
|
+
shellMounted = true;
|
|
91
149
|
if (typeof window !== "object") {
|
|
92
150
|
return;
|
|
93
151
|
}
|
|
94
152
|
|
|
153
|
+
window.addEventListener("keydown", handleShellKeydown);
|
|
154
|
+
window.addEventListener("resize", scheduleDrawerWidthMeasurement, { passive: true });
|
|
95
155
|
window.addEventListener("pointerdown", handlePullPointerDown, { capture: true, passive: true });
|
|
96
156
|
window.addEventListener("pointermove", handlePullPointerMove, { capture: true, passive: false });
|
|
97
157
|
window.addEventListener("pointerup", handlePullPointerEnd, { capture: true, passive: true });
|
|
@@ -100,13 +160,22 @@ onMounted(() => {
|
|
|
100
160
|
window.addEventListener("touchmove", handlePullTouchMove, { capture: true, passive: false });
|
|
101
161
|
window.addEventListener("touchend", handlePullTouchEnd, { capture: true, passive: true });
|
|
102
162
|
window.addEventListener("touchcancel", handlePullTouchCancel, { capture: true, passive: true });
|
|
103
|
-
});
|
|
104
163
|
|
|
105
|
-
|
|
164
|
+
if (document?.fonts) {
|
|
165
|
+
void document.fonts.ready.then(scheduleDrawerWidthMeasurement);
|
|
166
|
+
document.fonts.addEventListener?.("loadingdone", scheduleDrawerWidthMeasurement);
|
|
167
|
+
}
|
|
168
|
+
void nextTick().then(initializeDrawerMeasurement);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function detachShellListeners() {
|
|
172
|
+
shellMounted = false;
|
|
106
173
|
if (typeof window !== "object") {
|
|
107
174
|
return;
|
|
108
175
|
}
|
|
109
176
|
|
|
177
|
+
window.removeEventListener("keydown", handleShellKeydown);
|
|
178
|
+
window.removeEventListener("resize", scheduleDrawerWidthMeasurement);
|
|
110
179
|
window.removeEventListener("pointerdown", handlePullPointerDown, { capture: true });
|
|
111
180
|
window.removeEventListener("pointermove", handlePullPointerMove, { capture: true });
|
|
112
181
|
window.removeEventListener("pointerup", handlePullPointerEnd, { capture: true });
|
|
@@ -115,7 +184,135 @@ onBeforeUnmount(() => {
|
|
|
115
184
|
window.removeEventListener("touchmove", handlePullTouchMove, { capture: true });
|
|
116
185
|
window.removeEventListener("touchend", handlePullTouchEnd, { capture: true });
|
|
117
186
|
window.removeEventListener("touchcancel", handlePullTouchCancel, { capture: true });
|
|
118
|
-
|
|
187
|
+
document?.fonts?.removeEventListener?.("loadingdone", scheduleDrawerWidthMeasurement);
|
|
188
|
+
drawerContentObserver?.disconnect();
|
|
189
|
+
drawerContentObserver = null;
|
|
190
|
+
if (drawerMeasurementFrame !== null) {
|
|
191
|
+
window.cancelAnimationFrame(drawerMeasurementFrame);
|
|
192
|
+
drawerMeasurementFrame = null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function handleDrawerOpenChange(open) {
|
|
197
|
+
if (!isCompactLayout.value) {
|
|
198
|
+
wideDrawerOpen.value = Boolean(open);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function handleLayoutClassChange(compact) {
|
|
203
|
+
setDrawerOpen(compact ? false : wideDrawerOpen.value);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function handleShellKeydown(event) {
|
|
207
|
+
if (
|
|
208
|
+
event?.defaultPrevented ||
|
|
209
|
+
event?.key !== "Escape" ||
|
|
210
|
+
!isCompactLayout.value ||
|
|
211
|
+
!drawerOpen.value
|
|
212
|
+
) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
setDrawerOpen(false);
|
|
218
|
+
void nextTick().then(focusNavigationToggle);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function focusNavigationToggle() {
|
|
222
|
+
const toggleElement = navigationToggle.value?.$el || navigationToggle.value;
|
|
223
|
+
toggleElement?.focus?.({ preventScroll: true });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function initializeDrawerMeasurement() {
|
|
227
|
+
const drawerElement = resolveNavigationDrawerElement();
|
|
228
|
+
const drawerContent = drawerElement?.querySelector?.(".v-navigation-drawer__content") || drawerElement;
|
|
229
|
+
if (typeof MutationObserver === "function" && drawerContent) {
|
|
230
|
+
drawerContentObserver?.disconnect();
|
|
231
|
+
drawerContentObserver = new MutationObserver(scheduleDrawerWidthMeasurement);
|
|
232
|
+
drawerContentObserver.observe(drawerContent, {
|
|
233
|
+
childList: true,
|
|
234
|
+
characterData: true,
|
|
235
|
+
subtree: true
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
scheduleDrawerWidthMeasurement();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function resolveNavigationDrawerElement() {
|
|
242
|
+
const exposedElement = navigationDrawer.value?.$el || navigationDrawer.value || null;
|
|
243
|
+
if (typeof exposedElement?.getBoundingClientRect === "function") {
|
|
244
|
+
return exposedElement;
|
|
245
|
+
}
|
|
246
|
+
if (typeof document === "object") {
|
|
247
|
+
return document.getElementById("jskit-shell-drawer");
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function scheduleDrawerWidthMeasurement() {
|
|
253
|
+
if (
|
|
254
|
+
!shellMounted ||
|
|
255
|
+
props.drawerWidth !== null && props.drawerWidth !== undefined ||
|
|
256
|
+
!drawerPresentation.value.visible ||
|
|
257
|
+
drawerPresentation.value.rail ||
|
|
258
|
+
typeof window !== "object"
|
|
259
|
+
) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (drawerMeasurementFrame !== null) {
|
|
264
|
+
window.cancelAnimationFrame(drawerMeasurementFrame);
|
|
265
|
+
}
|
|
266
|
+
drawerMeasurementFrame = window.requestAnimationFrame(measureDrawerContentWidth);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function measureDrawerContentWidth() {
|
|
270
|
+
drawerMeasurementFrame = null;
|
|
271
|
+
const drawerElement = resolveNavigationDrawerElement();
|
|
272
|
+
if (!drawerElement || typeof document !== "object") {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const drawerRect = drawerElement.getBoundingClientRect();
|
|
277
|
+
const drawerStyle = window.getComputedStyle(drawerElement);
|
|
278
|
+
const rightToLeft = drawerStyle.direction === "rtl";
|
|
279
|
+
const endBorderWidth = Number.parseFloat(
|
|
280
|
+
rightToLeft ? drawerStyle.borderLeftWidth : drawerStyle.borderRightWidth
|
|
281
|
+
) || 0;
|
|
282
|
+
const measurements = [];
|
|
283
|
+
|
|
284
|
+
for (const label of drawerElement.querySelectorAll(".v-list-item-title")) {
|
|
285
|
+
const labelStyle = window.getComputedStyle(label);
|
|
286
|
+
if (labelStyle.display === "none" || labelStyle.visibility === "hidden") {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const textRect = measureRenderedText(label);
|
|
290
|
+
if (!textRect || textRect.width <= 0) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
measurements.push({
|
|
294
|
+
logicalStart: rightToLeft
|
|
295
|
+
? drawerRect.right - textRect.right
|
|
296
|
+
: textRect.left - drawerRect.left,
|
|
297
|
+
textWidth: textRect.width
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const nextWidth = resolveContentAwareDrawerWidth(measurements, {
|
|
302
|
+
endGap: SHELL_DRAWER_LABEL_END_GAP + endBorderWidth
|
|
303
|
+
});
|
|
304
|
+
if (Math.abs(nextWidth - measuredDrawerWidth.value) >= 0.25) {
|
|
305
|
+
measuredDrawerWidth.value = nextWidth;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function measureRenderedText(element) {
|
|
310
|
+
const range = document.createRange();
|
|
311
|
+
range.selectNodeContents(element);
|
|
312
|
+
const rect = range.getBoundingClientRect();
|
|
313
|
+
range.detach?.();
|
|
314
|
+
return rect;
|
|
315
|
+
}
|
|
119
316
|
|
|
120
317
|
function handlePullPointerDown(event) {
|
|
121
318
|
if (!canStartPullRefresh(event)) {
|
|
@@ -132,6 +329,12 @@ function handlePullPointerDown(event) {
|
|
|
132
329
|
};
|
|
133
330
|
}
|
|
134
331
|
|
|
332
|
+
function handleDrawerVisibilityChange(open) {
|
|
333
|
+
if (isCompactLayout.value || drawerPresentation.value.closedMode === "hidden" || open === false) {
|
|
334
|
+
setDrawerOpen(open);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
135
338
|
function handlePullPointerMove(event) {
|
|
136
339
|
if (!activePull || event.pointerId !== activePull.pointerId) {
|
|
137
340
|
return;
|
|
@@ -358,8 +561,12 @@ function touchListIncludesActiveTouch(touchList) {
|
|
|
358
561
|
data-testid="jskit-shell-app-bar"
|
|
359
562
|
>
|
|
360
563
|
<v-app-bar-nav-icon
|
|
564
|
+
ref="navigationToggle"
|
|
361
565
|
class="shell-layout__nav-toggle"
|
|
362
|
-
|
|
566
|
+
data-testid="jskit-shell-nav-toggle"
|
|
567
|
+
:aria-label="drawerToggleLabel"
|
|
568
|
+
:aria-expanded="drawerOpen"
|
|
569
|
+
aria-controls="jskit-shell-drawer"
|
|
363
570
|
@click="toggleDrawer"
|
|
364
571
|
/>
|
|
365
572
|
|
|
@@ -400,17 +607,30 @@ function touchListIncludesActiveTouch(touchList) {
|
|
|
400
607
|
</div>
|
|
401
608
|
|
|
402
609
|
<v-navigation-drawer
|
|
403
|
-
|
|
610
|
+
ref="navigationDrawer"
|
|
611
|
+
id="jskit-shell-drawer"
|
|
612
|
+
:model-value="drawerPresentation.visible"
|
|
404
613
|
border
|
|
405
|
-
class="bg-surface"
|
|
614
|
+
class="shell-layout__drawer bg-surface"
|
|
406
615
|
data-testid="jskit-shell-drawer"
|
|
616
|
+
:data-layout="layoutClass"
|
|
617
|
+
:data-presentation="drawerPresentation.kind"
|
|
618
|
+
:data-drawer-width-mode="props.drawerWidth === null || props.drawerWidth === undefined ? 'content' : 'fixed'"
|
|
619
|
+
:data-drawer-width="resolvedDrawerWidth"
|
|
620
|
+
:data-rail-width="resolvedRailWidth"
|
|
621
|
+
:style="{ '--shell-drawer-label-end-gap': `${SHELL_DRAWER_LABEL_END_GAP}px` }"
|
|
407
622
|
:temporary="isCompactLayout"
|
|
408
623
|
:permanent="!isCompactLayout"
|
|
409
|
-
:width="
|
|
624
|
+
:width="resolvedDrawerWidth"
|
|
625
|
+
:rail="drawerPresentation.rail"
|
|
626
|
+
:rail-width="resolvedRailWidth"
|
|
627
|
+
@update:model-value="handleDrawerVisibilityChange"
|
|
410
628
|
>
|
|
411
|
-
<slot name="menu" :surface="resolvedSurface">
|
|
629
|
+
<slot name="menu" :surface="resolvedSurface" :rail="drawerPresentation.rail">
|
|
412
630
|
<v-list nav density="comfortable" class="pt-2">
|
|
413
|
-
<v-list-subheader class="text-uppercase text-caption">
|
|
631
|
+
<v-list-subheader v-if="!drawerPresentation.rail" class="text-uppercase text-caption">
|
|
632
|
+
{{ resolvedSurfaceLabel }}
|
|
633
|
+
</v-list-subheader>
|
|
414
634
|
<ShellOutlet
|
|
415
635
|
target="shell-layout:primary-menu"
|
|
416
636
|
default
|
|
@@ -495,6 +715,56 @@ function touchListIncludesActiveTouch(touchList) {
|
|
|
495
715
|
min-width: 48px;
|
|
496
716
|
}
|
|
497
717
|
|
|
718
|
+
.shell-layout__drawer :deep(.v-list-item-title) {
|
|
719
|
+
white-space: nowrap;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.shell-layout__drawer[data-presentation="drawer"] :deep(.v-list--nav),
|
|
723
|
+
.shell-layout__drawer[data-presentation="drawer"] :deep(.v-list-item) {
|
|
724
|
+
padding-inline-end: calc(var(--shell-drawer-label-end-gap) / 2);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item) {
|
|
728
|
+
grid-template-areas: "prepend";
|
|
729
|
+
grid-template-columns: minmax(0, 1fr);
|
|
730
|
+
inline-size: 100%;
|
|
731
|
+
min-height: 48px;
|
|
732
|
+
min-width: 48px;
|
|
733
|
+
padding-inline: 0;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__prepend) {
|
|
737
|
+
inline-size: 100%;
|
|
738
|
+
justify-content: center;
|
|
739
|
+
min-width: 0;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__prepend .v-list-item__spacer),
|
|
743
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__content),
|
|
744
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__append) {
|
|
745
|
+
display: none;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__overlay) {
|
|
749
|
+
border-radius: 999px;
|
|
750
|
+
inset-block: 8px;
|
|
751
|
+
inset-inline: 4px;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item--active > .v-list-item__overlay) {
|
|
755
|
+
background: color-mix(
|
|
756
|
+
in srgb,
|
|
757
|
+
rgb(var(--v-theme-primary)) 18%,
|
|
758
|
+
rgb(var(--v-theme-surface))
|
|
759
|
+
);
|
|
760
|
+
opacity: 1;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item--active .v-icon) {
|
|
764
|
+
color: rgb(var(--v-theme-primary));
|
|
765
|
+
opacity: 1;
|
|
766
|
+
}
|
|
767
|
+
|
|
498
768
|
.shell-layout__top-right {
|
|
499
769
|
max-width: min(45vw, 18rem);
|
|
500
770
|
overflow: hidden;
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
useWebPlacementContext
|
|
6
6
|
} from "../placement/index.js";
|
|
7
7
|
import { resolveMenuLinkIcon } from "../lib/menuIcons.js";
|
|
8
|
+
import { activateShellNavigationLinkOnSpace } from "../support/navigationLinkKeyboard.js";
|
|
9
|
+
import ShellNavigationTooltip from "./ShellNavigationTooltip.vue";
|
|
8
10
|
|
|
9
11
|
const props = defineProps({
|
|
10
12
|
label: {
|
|
@@ -59,16 +61,22 @@ const resolvedIcon = computed(() =>
|
|
|
59
61
|
</script>
|
|
60
62
|
|
|
61
63
|
<template>
|
|
62
|
-
<v-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
<ShellNavigationTooltip v-if="resolvedTarget.href" :label="props.label">
|
|
65
|
+
<template #default="{ activatorProps }">
|
|
66
|
+
<v-list-item
|
|
67
|
+
v-bind="activatorProps"
|
|
68
|
+
class="shell-menu-link-item"
|
|
69
|
+
:title="props.label"
|
|
70
|
+
:to="resolvedTarget.sameOrigin ? resolvedTarget.href : undefined"
|
|
71
|
+
:href="resolvedTarget.sameOrigin ? undefined : resolvedTarget.href"
|
|
72
|
+
:prepend-icon="resolvedIcon || undefined"
|
|
73
|
+
:disabled="props.disabled"
|
|
74
|
+
:exact="props.exact"
|
|
75
|
+
:aria-label="props.label"
|
|
76
|
+
@keydown.space="activateShellNavigationLinkOnSpace"
|
|
77
|
+
/>
|
|
78
|
+
</template>
|
|
79
|
+
</ShellNavigationTooltip>
|
|
72
80
|
</template>
|
|
73
81
|
|
|
74
82
|
<style scoped>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useTheme } from "vuetify";
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
label: {
|
|
6
|
+
type: String,
|
|
7
|
+
default: ""
|
|
8
|
+
}
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const theme = useTheme();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<v-tooltip
|
|
16
|
+
:text="props.label"
|
|
17
|
+
:theme="theme.name.value"
|
|
18
|
+
location="end"
|
|
19
|
+
content-class="shell-navigation-tooltip"
|
|
20
|
+
:open-on-focus="true"
|
|
21
|
+
:open-on-hover="true"
|
|
22
|
+
>
|
|
23
|
+
<template #activator="{ props: activatorProps }">
|
|
24
|
+
<slot :activator-props="activatorProps" />
|
|
25
|
+
</template>
|
|
26
|
+
</v-tooltip>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<style>
|
|
30
|
+
.shell-navigation-tooltip {
|
|
31
|
+
background-color:
|
|
32
|
+
rgb(var(--v-theme-inverse-surface, var(--v-theme-on-surface))) !important;
|
|
33
|
+
color:
|
|
34
|
+
rgb(var(--v-theme-inverse-on-surface, var(--v-theme-surface))) !important;
|
|
35
|
+
opacity: 1 !important;
|
|
36
|
+
}
|
|
37
|
+
</style>
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
} from "../placement/index.js";
|
|
10
10
|
import { resolveMenuLinkIcon } from "../lib/menuIcons.js";
|
|
11
11
|
import { resolveMenuLinkTarget } from "../support/menuLinkTarget.js";
|
|
12
|
+
import { activateShellNavigationLinkOnSpace } from "../support/navigationLinkKeyboard.js";
|
|
13
|
+
import ShellNavigationTooltip from "./ShellNavigationTooltip.vue";
|
|
12
14
|
|
|
13
15
|
const props = defineProps({
|
|
14
16
|
label: {
|
|
@@ -104,16 +106,22 @@ const resolvedIcon = computed(() =>
|
|
|
104
106
|
</script>
|
|
105
107
|
|
|
106
108
|
<template>
|
|
107
|
-
<v-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
109
|
+
<ShellNavigationTooltip v-if="resolvedTarget.href" :label="props.label">
|
|
110
|
+
<template #default="{ activatorProps }">
|
|
111
|
+
<v-list-item
|
|
112
|
+
v-bind="activatorProps"
|
|
113
|
+
class="shell-menu-link-item"
|
|
114
|
+
:title="props.label"
|
|
115
|
+
:to="resolvedTarget.sameOrigin ? resolvedTarget.href : undefined"
|
|
116
|
+
:href="resolvedTarget.sameOrigin ? undefined : resolvedTarget.href"
|
|
117
|
+
:prepend-icon="resolvedIcon || undefined"
|
|
118
|
+
:disabled="props.disabled"
|
|
119
|
+
:exact="props.exact"
|
|
120
|
+
:aria-label="props.label"
|
|
121
|
+
@keydown.space="activateShellNavigationLinkOnSpace"
|
|
122
|
+
/>
|
|
123
|
+
</template>
|
|
124
|
+
</ShellNavigationTooltip>
|
|
117
125
|
</template>
|
|
118
126
|
|
|
119
127
|
<style scoped>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const DESKTOP_DRAWER_CLOSED_MODES = Object.freeze(["rail", "hidden"]);
|
|
2
|
+
|
|
3
|
+
function normalizeDesktopDrawerClosedMode(value = "rail") {
|
|
4
|
+
const normalized = String(value || "rail").trim().toLowerCase();
|
|
5
|
+
return DESKTOP_DRAWER_CLOSED_MODES.includes(normalized) ? normalized : "rail";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function resolveShellDrawerPresentation({
|
|
9
|
+
compact = false,
|
|
10
|
+
open = false,
|
|
11
|
+
desktopClosedMode = "rail"
|
|
12
|
+
} = {}) {
|
|
13
|
+
const closedMode = normalizeDesktopDrawerClosedMode(desktopClosedMode);
|
|
14
|
+
if (compact) {
|
|
15
|
+
return Object.freeze({
|
|
16
|
+
visible: Boolean(open),
|
|
17
|
+
rail: false,
|
|
18
|
+
kind: "modal",
|
|
19
|
+
closedMode
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (open) {
|
|
23
|
+
return Object.freeze({
|
|
24
|
+
visible: true,
|
|
25
|
+
rail: false,
|
|
26
|
+
kind: "drawer",
|
|
27
|
+
closedMode
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (closedMode === "hidden") {
|
|
31
|
+
return Object.freeze({
|
|
32
|
+
visible: false,
|
|
33
|
+
rail: false,
|
|
34
|
+
kind: "hidden",
|
|
35
|
+
closedMode
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return Object.freeze({
|
|
39
|
+
visible: true,
|
|
40
|
+
rail: true,
|
|
41
|
+
kind: "rail",
|
|
42
|
+
closedMode
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveShellDrawerToggleLabel({ compact = false, open = false } = {}) {
|
|
47
|
+
if (compact) {
|
|
48
|
+
return open ? "Close navigation menu" : "Open navigation menu";
|
|
49
|
+
}
|
|
50
|
+
return open ? "Collapse navigation drawer" : "Expand navigation drawer";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
DESKTOP_DRAWER_CLOSED_MODES,
|
|
55
|
+
normalizeDesktopDrawerClosedMode,
|
|
56
|
+
resolveShellDrawerPresentation,
|
|
57
|
+
resolveShellDrawerToggleLabel
|
|
58
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const DEFAULT_SHELL_DRAWER_WIDTH = 248;
|
|
2
|
+
const DEFAULT_SHELL_RAIL_WIDTH = 80;
|
|
3
|
+
const MINIMUM_SHELL_DRAWER_WIDTH = 120;
|
|
4
|
+
const MAXIMUM_SHELL_DRAWER_WIDTH = 360;
|
|
5
|
+
const MINIMUM_SHELL_RAIL_WIDTH = 48;
|
|
6
|
+
const MAXIMUM_SHELL_RAIL_WIDTH = 160;
|
|
7
|
+
const SHELL_DRAWER_LABEL_END_GAP = 10;
|
|
8
|
+
|
|
9
|
+
function clampNumber(value, minimum, maximum) {
|
|
10
|
+
return Math.min(maximum, Math.max(minimum, value));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeShellDrawerWidth(value, fallback = DEFAULT_SHELL_DRAWER_WIDTH) {
|
|
14
|
+
const parsed = Number(value);
|
|
15
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
return clampNumber(
|
|
19
|
+
parsed,
|
|
20
|
+
MINIMUM_SHELL_DRAWER_WIDTH,
|
|
21
|
+
MAXIMUM_SHELL_DRAWER_WIDTH
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeShellRailWidth(value, fallback = DEFAULT_SHELL_RAIL_WIDTH) {
|
|
26
|
+
const parsed = Number(value);
|
|
27
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
return clampNumber(
|
|
31
|
+
Math.round(parsed),
|
|
32
|
+
MINIMUM_SHELL_RAIL_WIDTH,
|
|
33
|
+
MAXIMUM_SHELL_RAIL_WIDTH
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveContentAwareDrawerWidth(
|
|
38
|
+
measurements = [],
|
|
39
|
+
{
|
|
40
|
+
endGap = SHELL_DRAWER_LABEL_END_GAP,
|
|
41
|
+
minimum = MINIMUM_SHELL_DRAWER_WIDTH,
|
|
42
|
+
maximum = MAXIMUM_SHELL_DRAWER_WIDTH,
|
|
43
|
+
fallback = DEFAULT_SHELL_DRAWER_WIDTH
|
|
44
|
+
} = {}
|
|
45
|
+
) {
|
|
46
|
+
let requiredWidth = 0;
|
|
47
|
+
for (const measurement of measurements) {
|
|
48
|
+
const logicalStart = Number(measurement?.logicalStart);
|
|
49
|
+
const textWidth = Number(measurement?.textWidth);
|
|
50
|
+
if (!Number.isFinite(logicalStart) || logicalStart < 0 || !Number.isFinite(textWidth) || textWidth <= 0) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
requiredWidth = Math.max(requiredWidth, logicalStart + textWidth + Number(endGap || 0));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (requiredWidth <= 0) {
|
|
57
|
+
return normalizeShellDrawerWidth(fallback);
|
|
58
|
+
}
|
|
59
|
+
// Layout boxes expose integer client widths even when the rendered glyph
|
|
60
|
+
// range is fractional. Round outward so the final glyph is never ellipsized
|
|
61
|
+
// at the exact end-gap boundary.
|
|
62
|
+
return clampNumber(Math.ceil(requiredWidth), minimum, maximum);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
DEFAULT_SHELL_DRAWER_WIDTH,
|
|
67
|
+
DEFAULT_SHELL_RAIL_WIDTH,
|
|
68
|
+
MAXIMUM_SHELL_DRAWER_WIDTH,
|
|
69
|
+
MINIMUM_SHELL_DRAWER_WIDTH,
|
|
70
|
+
SHELL_DRAWER_LABEL_END_GAP,
|
|
71
|
+
normalizeShellDrawerWidth,
|
|
72
|
+
normalizeShellRailWidth,
|
|
73
|
+
resolveContentAwareDrawerWidth
|
|
74
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
function activateShellNavigationLinkOnSpace(event) {
|
|
2
|
+
if (event?.key !== " " || typeof event.currentTarget?.click !== "function") {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
5
|
+
event.preventDefault();
|
|
6
|
+
event.stopPropagation();
|
|
7
|
+
event.currentTarget.click();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export { activateShellNavigationLinkOnSpace };
|