@jskit-ai/shell-web 0.1.149 → 0.1.151
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 +14 -0
- package/fixtures/adaptive-shell/index.html +12 -0
- package/fixtures/adaptive-shell/src/App.vue +34 -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 +258 -16
- package/src/client/components/ShellMenuLinkItem.vue +18 -12
- package/src/client/components/ShellNavigationTooltip.vue +37 -0
- package/src/client/components/ShellSurfaceAwareMenuLinkItem.vue +18 -12
- package/src/client/support/drawerWidth.js +91 -0
- package/src/client/support/navigationLinkKeyboard.js +10 -0
- package/src/test/adaptiveShellSmoke.js +192 -16
- package/templates/src/components/ShellLayout.vue +1 -0
- package/test/adaptiveShell.browser.test.js +324 -0
- package/test/drawerWidth.test.js +59 -0
- package/test/linkItemScaffoldContract.test.js +18 -2
- package/test/navigationLinkKeyboard.test.js +36 -0
- package/test/playwrightContract.test.js +17 -0
- package/test/settingsPlacementContract.test.js +16 -2
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
computed,
|
|
4
4
|
inject,
|
|
5
|
+
nextTick,
|
|
5
6
|
onBeforeUnmount,
|
|
6
7
|
onMounted,
|
|
7
8
|
ref,
|
|
@@ -13,6 +14,15 @@ import {
|
|
|
13
14
|
resolveShellDrawerPresentation,
|
|
14
15
|
resolveShellDrawerToggleLabel
|
|
15
16
|
} from "../support/drawerPresentation.js";
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_SHELL_DRAWER_WIDTH,
|
|
19
|
+
DEFAULT_SHELL_NAVIGATION_ITEM_SPACING,
|
|
20
|
+
DEFAULT_SHELL_RAIL_WIDTH,
|
|
21
|
+
normalizeShellDrawerWidth,
|
|
22
|
+
normalizeShellNavigationItemSpacing,
|
|
23
|
+
normalizeShellRailWidth,
|
|
24
|
+
resolveContentAwareDrawerWidth
|
|
25
|
+
} from "../support/drawerWidth.js";
|
|
16
26
|
import ShellOutlet from "./ShellOutlet.vue";
|
|
17
27
|
import ShellRouteTransition from "./ShellRouteTransition.vue";
|
|
18
28
|
|
|
@@ -36,6 +46,18 @@ const props = defineProps({
|
|
|
36
46
|
desktopDrawerClosedMode: {
|
|
37
47
|
type: String,
|
|
38
48
|
default: "rail"
|
|
49
|
+
},
|
|
50
|
+
drawerWidth: {
|
|
51
|
+
type: Number,
|
|
52
|
+
default: null
|
|
53
|
+
},
|
|
54
|
+
railWidth: {
|
|
55
|
+
type: Number,
|
|
56
|
+
default: DEFAULT_SHELL_RAIL_WIDTH
|
|
57
|
+
},
|
|
58
|
+
navigationItemSpacing: {
|
|
59
|
+
type: Number,
|
|
60
|
+
default: DEFAULT_SHELL_NAVIGATION_ITEM_SPACING
|
|
39
61
|
}
|
|
40
62
|
});
|
|
41
63
|
|
|
@@ -56,7 +78,13 @@ const refreshRuntime = inject("jskit.shell-web.runtime.web-refresh.client", null
|
|
|
56
78
|
const pullDistance = ref(0);
|
|
57
79
|
const pullRefreshing = ref(false);
|
|
58
80
|
const wideDrawerOpen = ref(Boolean(drawerDefaultOpen.value));
|
|
81
|
+
const navigationToggle = ref(null);
|
|
82
|
+
const navigationDrawer = ref(null);
|
|
83
|
+
const measuredDrawerWidth = ref(DEFAULT_SHELL_DRAWER_WIDTH);
|
|
59
84
|
let activePull = null;
|
|
85
|
+
let drawerContentObserver = null;
|
|
86
|
+
let drawerMeasurementFrame = null;
|
|
87
|
+
let shellMounted = false;
|
|
60
88
|
|
|
61
89
|
const PULL_REFRESH_TRIGGER_DISTANCE = 72;
|
|
62
90
|
const PULL_REFRESH_MAX_DISTANCE = 112;
|
|
@@ -81,6 +109,16 @@ const drawerToggleLabel = computed(() => resolveShellDrawerToggleLabel({
|
|
|
81
109
|
compact: isCompactLayout.value,
|
|
82
110
|
open: drawerOpen.value
|
|
83
111
|
}));
|
|
112
|
+
const resolvedDrawerWidth = computed(() => {
|
|
113
|
+
if (props.drawerWidth !== null && props.drawerWidth !== undefined) {
|
|
114
|
+
return normalizeShellDrawerWidth(props.drawerWidth);
|
|
115
|
+
}
|
|
116
|
+
return normalizeShellDrawerWidth(measuredDrawerWidth.value);
|
|
117
|
+
});
|
|
118
|
+
const resolvedRailWidth = computed(() => normalizeShellRailWidth(props.railWidth));
|
|
119
|
+
const resolvedNavigationItemSpacing = computed(() =>
|
|
120
|
+
normalizeShellNavigationItemSpacing(props.navigationItemSpacing)
|
|
121
|
+
);
|
|
84
122
|
const pullProgress = computed(() =>
|
|
85
123
|
Math.min(100, Math.round((pullDistance.value / PULL_REFRESH_TRIGGER_DISTANCE) * 100))
|
|
86
124
|
);
|
|
@@ -99,26 +137,30 @@ const pullRefreshStyle = computed(() => ({
|
|
|
99
137
|
|
|
100
138
|
watch(
|
|
101
139
|
drawerOpen,
|
|
102
|
-
|
|
103
|
-
if (!isCompactLayout.value) {
|
|
104
|
-
wideDrawerOpen.value = Boolean(open);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
140
|
+
handleDrawerOpenChange
|
|
107
141
|
);
|
|
108
142
|
|
|
109
143
|
watch(
|
|
110
144
|
isCompactLayout,
|
|
111
|
-
|
|
112
|
-
setDrawerOpen(compact ? false : wideDrawerOpen.value);
|
|
113
|
-
},
|
|
145
|
+
handleLayoutClassChange,
|
|
114
146
|
{ immediate: true }
|
|
115
147
|
);
|
|
116
148
|
|
|
117
|
-
|
|
149
|
+
watch(drawerPresentation, scheduleDrawerWidthMeasurement, { flush: "post" });
|
|
150
|
+
watch(resolvedSurface, scheduleDrawerWidthMeasurement, { flush: "post" });
|
|
151
|
+
watch(resolvedNavigationItemSpacing, scheduleDrawerWidthMeasurement, { flush: "post" });
|
|
152
|
+
|
|
153
|
+
onMounted(attachShellListeners);
|
|
154
|
+
onBeforeUnmount(detachShellListeners);
|
|
155
|
+
|
|
156
|
+
function attachShellListeners() {
|
|
157
|
+
shellMounted = true;
|
|
118
158
|
if (typeof window !== "object") {
|
|
119
159
|
return;
|
|
120
160
|
}
|
|
121
161
|
|
|
162
|
+
window.addEventListener("keydown", handleShellKeydown);
|
|
163
|
+
window.addEventListener("resize", scheduleDrawerWidthMeasurement, { passive: true });
|
|
122
164
|
window.addEventListener("pointerdown", handlePullPointerDown, { capture: true, passive: true });
|
|
123
165
|
window.addEventListener("pointermove", handlePullPointerMove, { capture: true, passive: false });
|
|
124
166
|
window.addEventListener("pointerup", handlePullPointerEnd, { capture: true, passive: true });
|
|
@@ -127,13 +169,22 @@ onMounted(() => {
|
|
|
127
169
|
window.addEventListener("touchmove", handlePullTouchMove, { capture: true, passive: false });
|
|
128
170
|
window.addEventListener("touchend", handlePullTouchEnd, { capture: true, passive: true });
|
|
129
171
|
window.addEventListener("touchcancel", handlePullTouchCancel, { capture: true, passive: true });
|
|
130
|
-
});
|
|
131
172
|
|
|
132
|
-
|
|
173
|
+
if (document?.fonts) {
|
|
174
|
+
void document.fonts.ready.then(scheduleDrawerWidthMeasurement);
|
|
175
|
+
document.fonts.addEventListener?.("loadingdone", scheduleDrawerWidthMeasurement);
|
|
176
|
+
}
|
|
177
|
+
void nextTick().then(initializeDrawerMeasurement);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function detachShellListeners() {
|
|
181
|
+
shellMounted = false;
|
|
133
182
|
if (typeof window !== "object") {
|
|
134
183
|
return;
|
|
135
184
|
}
|
|
136
185
|
|
|
186
|
+
window.removeEventListener("keydown", handleShellKeydown);
|
|
187
|
+
window.removeEventListener("resize", scheduleDrawerWidthMeasurement);
|
|
137
188
|
window.removeEventListener("pointerdown", handlePullPointerDown, { capture: true });
|
|
138
189
|
window.removeEventListener("pointermove", handlePullPointerMove, { capture: true });
|
|
139
190
|
window.removeEventListener("pointerup", handlePullPointerEnd, { capture: true });
|
|
@@ -142,7 +193,135 @@ onBeforeUnmount(() => {
|
|
|
142
193
|
window.removeEventListener("touchmove", handlePullTouchMove, { capture: true });
|
|
143
194
|
window.removeEventListener("touchend", handlePullTouchEnd, { capture: true });
|
|
144
195
|
window.removeEventListener("touchcancel", handlePullTouchCancel, { capture: true });
|
|
145
|
-
|
|
196
|
+
document?.fonts?.removeEventListener?.("loadingdone", scheduleDrawerWidthMeasurement);
|
|
197
|
+
drawerContentObserver?.disconnect();
|
|
198
|
+
drawerContentObserver = null;
|
|
199
|
+
if (drawerMeasurementFrame !== null) {
|
|
200
|
+
window.cancelAnimationFrame(drawerMeasurementFrame);
|
|
201
|
+
drawerMeasurementFrame = null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function handleDrawerOpenChange(open) {
|
|
206
|
+
if (!isCompactLayout.value) {
|
|
207
|
+
wideDrawerOpen.value = Boolean(open);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function handleLayoutClassChange(compact) {
|
|
212
|
+
setDrawerOpen(compact ? false : wideDrawerOpen.value);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function handleShellKeydown(event) {
|
|
216
|
+
if (
|
|
217
|
+
event?.defaultPrevented ||
|
|
218
|
+
event?.key !== "Escape" ||
|
|
219
|
+
!isCompactLayout.value ||
|
|
220
|
+
!drawerOpen.value
|
|
221
|
+
) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
event.preventDefault();
|
|
226
|
+
setDrawerOpen(false);
|
|
227
|
+
void nextTick().then(focusNavigationToggle);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function focusNavigationToggle() {
|
|
231
|
+
const toggleElement = navigationToggle.value?.$el || navigationToggle.value;
|
|
232
|
+
toggleElement?.focus?.({ preventScroll: true });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function initializeDrawerMeasurement() {
|
|
236
|
+
const drawerElement = resolveNavigationDrawerElement();
|
|
237
|
+
const drawerContent = drawerElement?.querySelector?.(".v-navigation-drawer__content") || drawerElement;
|
|
238
|
+
if (typeof MutationObserver === "function" && drawerContent) {
|
|
239
|
+
drawerContentObserver?.disconnect();
|
|
240
|
+
drawerContentObserver = new MutationObserver(scheduleDrawerWidthMeasurement);
|
|
241
|
+
drawerContentObserver.observe(drawerContent, {
|
|
242
|
+
childList: true,
|
|
243
|
+
characterData: true,
|
|
244
|
+
subtree: true
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
scheduleDrawerWidthMeasurement();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function resolveNavigationDrawerElement() {
|
|
251
|
+
const exposedElement = navigationDrawer.value?.$el || navigationDrawer.value || null;
|
|
252
|
+
if (typeof exposedElement?.getBoundingClientRect === "function") {
|
|
253
|
+
return exposedElement;
|
|
254
|
+
}
|
|
255
|
+
if (typeof document === "object") {
|
|
256
|
+
return document.getElementById("jskit-shell-drawer");
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function scheduleDrawerWidthMeasurement() {
|
|
262
|
+
if (
|
|
263
|
+
!shellMounted ||
|
|
264
|
+
props.drawerWidth !== null && props.drawerWidth !== undefined ||
|
|
265
|
+
!drawerPresentation.value.visible ||
|
|
266
|
+
drawerPresentation.value.rail ||
|
|
267
|
+
typeof window !== "object"
|
|
268
|
+
) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (drawerMeasurementFrame !== null) {
|
|
273
|
+
window.cancelAnimationFrame(drawerMeasurementFrame);
|
|
274
|
+
}
|
|
275
|
+
drawerMeasurementFrame = window.requestAnimationFrame(measureDrawerContentWidth);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function measureDrawerContentWidth() {
|
|
279
|
+
drawerMeasurementFrame = null;
|
|
280
|
+
const drawerElement = resolveNavigationDrawerElement();
|
|
281
|
+
if (!drawerElement || typeof document !== "object") {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const drawerRect = drawerElement.getBoundingClientRect();
|
|
286
|
+
const drawerStyle = window.getComputedStyle(drawerElement);
|
|
287
|
+
const rightToLeft = drawerStyle.direction === "rtl";
|
|
288
|
+
const endBorderWidth = Number.parseFloat(
|
|
289
|
+
rightToLeft ? drawerStyle.borderLeftWidth : drawerStyle.borderRightWidth
|
|
290
|
+
) || 0;
|
|
291
|
+
const measurements = [];
|
|
292
|
+
|
|
293
|
+
for (const label of drawerElement.querySelectorAll(".v-list-item-title")) {
|
|
294
|
+
const labelStyle = window.getComputedStyle(label);
|
|
295
|
+
if (labelStyle.display === "none" || labelStyle.visibility === "hidden") {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const textRect = measureRenderedText(label);
|
|
299
|
+
if (!textRect || textRect.width <= 0) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
measurements.push({
|
|
303
|
+
logicalStart: rightToLeft
|
|
304
|
+
? drawerRect.right - textRect.right
|
|
305
|
+
: textRect.left - drawerRect.left,
|
|
306
|
+
textWidth: textRect.width
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const nextWidth = resolveContentAwareDrawerWidth(measurements, {
|
|
311
|
+
endGap: resolvedNavigationItemSpacing.value + endBorderWidth
|
|
312
|
+
});
|
|
313
|
+
if (Math.abs(nextWidth - measuredDrawerWidth.value) >= 0.25) {
|
|
314
|
+
measuredDrawerWidth.value = nextWidth;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function measureRenderedText(element) {
|
|
319
|
+
const range = document.createRange();
|
|
320
|
+
range.selectNodeContents(element);
|
|
321
|
+
const rect = range.getBoundingClientRect();
|
|
322
|
+
range.detach?.();
|
|
323
|
+
return rect;
|
|
324
|
+
}
|
|
146
325
|
|
|
147
326
|
function handlePullPointerDown(event) {
|
|
148
327
|
if (!canStartPullRefresh(event)) {
|
|
@@ -391,6 +570,7 @@ function touchListIncludesActiveTouch(touchList) {
|
|
|
391
570
|
data-testid="jskit-shell-app-bar"
|
|
392
571
|
>
|
|
393
572
|
<v-app-bar-nav-icon
|
|
573
|
+
ref="navigationToggle"
|
|
394
574
|
class="shell-layout__nav-toggle"
|
|
395
575
|
data-testid="jskit-shell-nav-toggle"
|
|
396
576
|
:aria-label="drawerToggleLabel"
|
|
@@ -436,21 +616,33 @@ function touchListIncludesActiveTouch(touchList) {
|
|
|
436
616
|
</div>
|
|
437
617
|
|
|
438
618
|
<v-navigation-drawer
|
|
619
|
+
ref="navigationDrawer"
|
|
439
620
|
id="jskit-shell-drawer"
|
|
440
621
|
:model-value="drawerPresentation.visible"
|
|
441
622
|
border
|
|
442
|
-
class="bg-surface"
|
|
623
|
+
class="shell-layout__drawer bg-surface"
|
|
443
624
|
data-testid="jskit-shell-drawer"
|
|
625
|
+
:data-layout="layoutClass"
|
|
444
626
|
:data-presentation="drawerPresentation.kind"
|
|
627
|
+
:data-drawer-width-mode="props.drawerWidth === null || props.drawerWidth === undefined ? 'content' : 'fixed'"
|
|
628
|
+
:data-drawer-width="resolvedDrawerWidth"
|
|
629
|
+
:data-navigation-item-spacing="resolvedNavigationItemSpacing"
|
|
630
|
+
:data-rail-width="resolvedRailWidth"
|
|
631
|
+
:style="{ '--shell-navigation-item-spacing': `${resolvedNavigationItemSpacing}px` }"
|
|
445
632
|
:temporary="isCompactLayout"
|
|
446
633
|
:permanent="!isCompactLayout"
|
|
447
|
-
:width="
|
|
634
|
+
:width="resolvedDrawerWidth"
|
|
448
635
|
:rail="drawerPresentation.rail"
|
|
449
|
-
:rail-width="
|
|
636
|
+
:rail-width="resolvedRailWidth"
|
|
450
637
|
@update:model-value="handleDrawerVisibilityChange"
|
|
451
638
|
>
|
|
452
639
|
<slot name="menu" :surface="resolvedSurface" :rail="drawerPresentation.rail">
|
|
453
|
-
<v-list
|
|
640
|
+
<v-list
|
|
641
|
+
nav
|
|
642
|
+
density="comfortable"
|
|
643
|
+
class="pt-2"
|
|
644
|
+
:prepend-gap="resolvedNavigationItemSpacing"
|
|
645
|
+
>
|
|
454
646
|
<v-list-subheader v-if="!drawerPresentation.rail" class="text-uppercase text-caption">
|
|
455
647
|
{{ resolvedSurfaceLabel }}
|
|
456
648
|
</v-list-subheader>
|
|
@@ -538,6 +730,56 @@ function touchListIncludesActiveTouch(touchList) {
|
|
|
538
730
|
min-width: 48px;
|
|
539
731
|
}
|
|
540
732
|
|
|
733
|
+
.shell-layout__drawer :deep(.v-list-item-title) {
|
|
734
|
+
white-space: nowrap;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
.shell-layout__drawer[data-presentation="drawer"] :deep(.v-list--nav),
|
|
738
|
+
.shell-layout__drawer[data-presentation="drawer"] :deep(.v-list-item) {
|
|
739
|
+
padding-inline-end: calc(var(--shell-navigation-item-spacing) / 2);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item) {
|
|
743
|
+
grid-template-areas: "prepend";
|
|
744
|
+
grid-template-columns: minmax(0, 1fr);
|
|
745
|
+
inline-size: 100%;
|
|
746
|
+
min-height: 48px;
|
|
747
|
+
min-width: 48px;
|
|
748
|
+
padding-inline: 0;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__prepend) {
|
|
752
|
+
inline-size: 100%;
|
|
753
|
+
justify-content: center;
|
|
754
|
+
min-width: 0;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__prepend .v-list-item__spacer),
|
|
758
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__content),
|
|
759
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__append) {
|
|
760
|
+
display: none;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item__overlay) {
|
|
764
|
+
border-radius: 999px;
|
|
765
|
+
inset-block: 8px;
|
|
766
|
+
inset-inline: 4px;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item--active > .v-list-item__overlay) {
|
|
770
|
+
background: color-mix(
|
|
771
|
+
in srgb,
|
|
772
|
+
rgb(var(--v-theme-primary)) 18%,
|
|
773
|
+
rgb(var(--v-theme-surface))
|
|
774
|
+
);
|
|
775
|
+
opacity: 1;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
.shell-layout__drawer[data-presentation="rail"] :deep(.v-list-item--active .v-icon) {
|
|
779
|
+
color: rgb(var(--v-theme-primary));
|
|
780
|
+
opacity: 1;
|
|
781
|
+
}
|
|
782
|
+
|
|
541
783
|
.shell-layout__top-right {
|
|
542
784
|
max-width: min(45vw, 18rem);
|
|
543
785
|
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,18 +61,22 @@ const resolvedIcon = computed(() =>
|
|
|
59
61
|
</script>
|
|
60
62
|
|
|
61
63
|
<template>
|
|
62
|
-
<v-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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>
|
|
74
80
|
</template>
|
|
75
81
|
|
|
76
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,18 +106,22 @@ const resolvedIcon = computed(() =>
|
|
|
104
106
|
</script>
|
|
105
107
|
|
|
106
108
|
<template>
|
|
107
|
-
<v-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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>
|
|
119
125
|
</template>
|
|
120
126
|
|
|
121
127
|
<style scoped>
|
|
@@ -0,0 +1,91 @@
|
|
|
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 DEFAULT_SHELL_NAVIGATION_ITEM_SPACING = 12;
|
|
8
|
+
const MINIMUM_SHELL_NAVIGATION_ITEM_SPACING = 8;
|
|
9
|
+
const MAXIMUM_SHELL_NAVIGATION_ITEM_SPACING = 24;
|
|
10
|
+
|
|
11
|
+
function clampNumber(value, minimum, maximum) {
|
|
12
|
+
return Math.min(maximum, Math.max(minimum, value));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeShellDrawerWidth(value, fallback = DEFAULT_SHELL_DRAWER_WIDTH) {
|
|
16
|
+
const parsed = Number(value);
|
|
17
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
18
|
+
return fallback;
|
|
19
|
+
}
|
|
20
|
+
return clampNumber(
|
|
21
|
+
parsed,
|
|
22
|
+
MINIMUM_SHELL_DRAWER_WIDTH,
|
|
23
|
+
MAXIMUM_SHELL_DRAWER_WIDTH
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeShellRailWidth(value, fallback = DEFAULT_SHELL_RAIL_WIDTH) {
|
|
28
|
+
const parsed = Number(value);
|
|
29
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
return clampNumber(
|
|
33
|
+
Math.round(parsed),
|
|
34
|
+
MINIMUM_SHELL_RAIL_WIDTH,
|
|
35
|
+
MAXIMUM_SHELL_RAIL_WIDTH
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeShellNavigationItemSpacing(
|
|
40
|
+
value,
|
|
41
|
+
fallback = DEFAULT_SHELL_NAVIGATION_ITEM_SPACING
|
|
42
|
+
) {
|
|
43
|
+
const parsed = Number(value);
|
|
44
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
45
|
+
return fallback;
|
|
46
|
+
}
|
|
47
|
+
return clampNumber(
|
|
48
|
+
Math.round(parsed),
|
|
49
|
+
MINIMUM_SHELL_NAVIGATION_ITEM_SPACING,
|
|
50
|
+
MAXIMUM_SHELL_NAVIGATION_ITEM_SPACING
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveContentAwareDrawerWidth(
|
|
55
|
+
measurements = [],
|
|
56
|
+
{
|
|
57
|
+
endGap = DEFAULT_SHELL_NAVIGATION_ITEM_SPACING,
|
|
58
|
+
minimum = MINIMUM_SHELL_DRAWER_WIDTH,
|
|
59
|
+
maximum = MAXIMUM_SHELL_DRAWER_WIDTH,
|
|
60
|
+
fallback = DEFAULT_SHELL_DRAWER_WIDTH
|
|
61
|
+
} = {}
|
|
62
|
+
) {
|
|
63
|
+
let requiredWidth = 0;
|
|
64
|
+
for (const measurement of measurements) {
|
|
65
|
+
const logicalStart = Number(measurement?.logicalStart);
|
|
66
|
+
const textWidth = Number(measurement?.textWidth);
|
|
67
|
+
if (!Number.isFinite(logicalStart) || logicalStart < 0 || !Number.isFinite(textWidth) || textWidth <= 0) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
requiredWidth = Math.max(requiredWidth, logicalStart + textWidth + Number(endGap || 0));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (requiredWidth <= 0) {
|
|
74
|
+
return normalizeShellDrawerWidth(fallback);
|
|
75
|
+
}
|
|
76
|
+
// Preserve subpixel font metrics while rounding outward enough to avoid
|
|
77
|
+
// clipping the final glyph at the exact spacing boundary.
|
|
78
|
+
return clampNumber(Math.ceil(requiredWidth * 4) / 4, minimum, maximum);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export {
|
|
82
|
+
DEFAULT_SHELL_DRAWER_WIDTH,
|
|
83
|
+
DEFAULT_SHELL_NAVIGATION_ITEM_SPACING,
|
|
84
|
+
DEFAULT_SHELL_RAIL_WIDTH,
|
|
85
|
+
MAXIMUM_SHELL_DRAWER_WIDTH,
|
|
86
|
+
MINIMUM_SHELL_DRAWER_WIDTH,
|
|
87
|
+
normalizeShellDrawerWidth,
|
|
88
|
+
normalizeShellNavigationItemSpacing,
|
|
89
|
+
normalizeShellRailWidth,
|
|
90
|
+
resolveContentAwareDrawerWidth
|
|
91
|
+
};
|
|
@@ -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 };
|