@jskit-ai/shell-web 0.1.148 → 0.1.149

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.
@@ -1,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/shell-web",
4
- version: "0.1.148",
4
+ version: "0.1.149",
5
5
  kind: "runtime",
6
6
  description: "Web shell layout runtime with outlet-based placement contributions.",
7
7
  dependsOn: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/shell-web",
3
- "version": "0.1.148",
3
+ "version": "0.1.149",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -9,6 +9,10 @@ import {
9
9
  } from "vue";
10
10
  import { useDisplay } from "vuetify";
11
11
  import { useShellLayoutState } from "../composables/useShellLayoutState.js";
12
+ import {
13
+ resolveShellDrawerPresentation,
14
+ resolveShellDrawerToggleLabel
15
+ } from "../support/drawerPresentation.js";
12
16
  import ShellOutlet from "./ShellOutlet.vue";
13
17
  import ShellRouteTransition from "./ShellRouteTransition.vue";
14
18
 
@@ -28,6 +32,10 @@ const props = defineProps({
28
32
  subtitle: {
29
33
  type: String,
30
34
  default: ""
35
+ },
36
+ desktopDrawerClosedMode: {
37
+ type: String,
38
+ default: "rail"
31
39
  }
32
40
  });
33
41
 
@@ -47,6 +55,7 @@ const display = useDisplay();
47
55
  const refreshRuntime = inject("jskit.shell-web.runtime.web-refresh.client", null);
48
56
  const pullDistance = ref(0);
49
57
  const pullRefreshing = ref(false);
58
+ const wideDrawerOpen = ref(Boolean(drawerDefaultOpen.value));
50
59
  let activePull = null;
51
60
 
52
61
  const PULL_REFRESH_TRIGGER_DISTANCE = 72;
@@ -63,6 +72,15 @@ const layoutClass = computed(() => {
63
72
  return "expanded";
64
73
  });
65
74
  const isCompactLayout = computed(() => layoutClass.value === "compact");
75
+ const drawerPresentation = computed(() => resolveShellDrawerPresentation({
76
+ compact: isCompactLayout.value,
77
+ open: drawerOpen.value,
78
+ desktopClosedMode: props.desktopDrawerClosedMode
79
+ }));
80
+ const drawerToggleLabel = computed(() => resolveShellDrawerToggleLabel({
81
+ compact: isCompactLayout.value,
82
+ open: drawerOpen.value
83
+ }));
66
84
  const pullProgress = computed(() =>
67
85
  Math.min(100, Math.round((pullDistance.value / PULL_REFRESH_TRIGGER_DISTANCE) * 100))
68
86
  );
@@ -79,10 +97,19 @@ const pullRefreshStyle = computed(() => ({
79
97
  "--shell-pull-refresh-distance": `${Math.round(Math.min(pullDistance.value, PULL_REFRESH_MAX_DISTANCE))}px`
80
98
  }));
81
99
 
100
+ watch(
101
+ drawerOpen,
102
+ (open) => {
103
+ if (!isCompactLayout.value) {
104
+ wideDrawerOpen.value = Boolean(open);
105
+ }
106
+ }
107
+ );
108
+
82
109
  watch(
83
110
  isCompactLayout,
84
111
  (compact) => {
85
- setDrawerOpen(compact ? false : drawerDefaultOpen.value);
112
+ setDrawerOpen(compact ? false : wideDrawerOpen.value);
86
113
  },
87
114
  { immediate: true }
88
115
  );
@@ -132,6 +159,12 @@ function handlePullPointerDown(event) {
132
159
  };
133
160
  }
134
161
 
162
+ function handleDrawerVisibilityChange(open) {
163
+ if (isCompactLayout.value || drawerPresentation.value.closedMode === "hidden" || open === false) {
164
+ setDrawerOpen(open);
165
+ }
166
+ }
167
+
135
168
  function handlePullPointerMove(event) {
136
169
  if (!activePull || event.pointerId !== activePull.pointerId) {
137
170
  return;
@@ -359,7 +392,10 @@ function touchListIncludesActiveTouch(touchList) {
359
392
  >
360
393
  <v-app-bar-nav-icon
361
394
  class="shell-layout__nav-toggle"
362
- aria-label="Toggle navigation menu"
395
+ data-testid="jskit-shell-nav-toggle"
396
+ :aria-label="drawerToggleLabel"
397
+ :aria-expanded="drawerOpen"
398
+ aria-controls="jskit-shell-drawer"
363
399
  @click="toggleDrawer"
364
400
  />
365
401
 
@@ -400,17 +436,24 @@ function touchListIncludesActiveTouch(touchList) {
400
436
  </div>
401
437
 
402
438
  <v-navigation-drawer
403
- v-model="drawerOpen"
439
+ id="jskit-shell-drawer"
440
+ :model-value="drawerPresentation.visible"
404
441
  border
405
442
  class="bg-surface"
406
443
  data-testid="jskit-shell-drawer"
444
+ :data-presentation="drawerPresentation.kind"
407
445
  :temporary="isCompactLayout"
408
446
  :permanent="!isCompactLayout"
409
447
  :width="248"
448
+ :rail="drawerPresentation.rail"
449
+ :rail-width="80"
450
+ @update:model-value="handleDrawerVisibilityChange"
410
451
  >
411
- <slot name="menu" :surface="resolvedSurface">
452
+ <slot name="menu" :surface="resolvedSurface" :rail="drawerPresentation.rail">
412
453
  <v-list nav density="comfortable" class="pt-2">
413
- <v-list-subheader class="text-uppercase text-caption">{{ resolvedSurfaceLabel }}</v-list-subheader>
454
+ <v-list-subheader v-if="!drawerPresentation.rail" class="text-uppercase text-caption">
455
+ {{ resolvedSurfaceLabel }}
456
+ </v-list-subheader>
414
457
  <ShellOutlet
415
458
  target="shell-layout:primary-menu"
416
459
  default
@@ -68,6 +68,8 @@ const resolvedIcon = computed(() =>
68
68
  :prepend-icon="resolvedIcon || undefined"
69
69
  :disabled="props.disabled"
70
70
  :exact="props.exact"
71
+ :aria-label="props.label"
72
+ v-tooltip="props.label"
71
73
  />
72
74
  </template>
73
75
 
@@ -113,6 +113,8 @@ const resolvedIcon = computed(() =>
113
113
  :prepend-icon="resolvedIcon || undefined"
114
114
  :disabled="props.disabled"
115
115
  :exact="props.exact"
116
+ :aria-label="props.label"
117
+ v-tooltip="props.label"
116
118
  />
117
119
  </template>
118
120
 
@@ -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
+ };
@@ -94,6 +94,11 @@ async function runAdaptiveShellSmokeCase({
94
94
 
95
95
  await expect(page.getByTestId("jskit-shell-bottom-nav")).toBeVisible();
96
96
  expect(await isElementVisibleInViewport(page, "jskit-shell-drawer")).toBe(false);
97
+ await page.getByTestId("jskit-shell-nav-toggle").click();
98
+ await expect(page.getByTestId("jskit-shell-drawer")).toBeVisible();
99
+ await expect(page.getByTestId("jskit-shell-drawer")).toHaveAttribute("data-presentation", "modal");
100
+ await page.keyboard.press("Escape");
101
+ expect(await isElementVisibleInViewport(page, "jskit-shell-drawer")).toBe(false);
97
102
 
98
103
  const navButtonHeights = await page.getByTestId("jskit-shell-bottom-nav").locator(".v-btn").evaluateAll((buttons) =>
99
104
  buttons.map((button) => button.getBoundingClientRect().height)
@@ -107,6 +112,14 @@ async function runAdaptiveShellSmokeCase({
107
112
  await expect.poll(() => bootstrapRequests).toBeGreaterThan(bootstrapRequestsBeforePull);
108
113
  } else {
109
114
  await expect(page.getByTestId("jskit-shell-drawer")).toBeVisible();
115
+ await page.getByTestId("jskit-shell-nav-toggle").click();
116
+ await expect(page.getByTestId("jskit-shell-drawer")).toBeVisible();
117
+ await expect(page.getByTestId("jskit-shell-drawer")).toHaveAttribute("data-presentation", "rail");
118
+ const railWidth = await page.getByTestId("jskit-shell-drawer").evaluate(
119
+ (element) => element.getBoundingClientRect().width
120
+ );
121
+ expect(railWidth).toBeGreaterThanOrEqual(79);
122
+ expect(railWidth).toBeLessThanOrEqual(81);
110
123
  }
111
124
  }
112
125
 
@@ -19,7 +19,7 @@ const drawerDefaultOpenModel = computed({
19
19
  <div>
20
20
  <h2 class="text-h6 mb-2">Navigation</h2>
21
21
  <p class="text-body-2 text-medium-emphasis mb-0">
22
- Choose the default behavior for wider screens. Phone layouts keep primary navigation in the bottom bar.
22
+ On wider screens, collapsed navigation remains available as a rail. Phone layouts close the drawer and keep primary navigation in the bottom bar.
23
23
  </p>
24
24
  </div>
25
25
 
@@ -28,7 +28,7 @@ const drawerDefaultOpenModel = computed({
28
28
  color="primary"
29
29
  inset
30
30
  hide-details="auto"
31
- label="Open drawer by default on wider screens"
31
+ label="Start with expanded navigation on wider screens"
32
32
  />
33
33
  </section>
34
34
  </template>
@@ -0,0 +1,68 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ normalizeDesktopDrawerClosedMode,
5
+ resolveShellDrawerPresentation,
6
+ resolveShellDrawerToggleLabel
7
+ } from "../src/client/support/drawerPresentation.js";
8
+
9
+ test("wide closed navigation defaults to a visible Material navigation rail", () => {
10
+ assert.deepEqual(
11
+ resolveShellDrawerPresentation({ compact: false, open: false }),
12
+ {
13
+ visible: true,
14
+ rail: true,
15
+ kind: "rail",
16
+ closedMode: "rail"
17
+ }
18
+ );
19
+ });
20
+
21
+ test("compact closed navigation dismisses instead of retaining a rail", () => {
22
+ assert.deepEqual(
23
+ resolveShellDrawerPresentation({ compact: true, open: false }),
24
+ {
25
+ visible: false,
26
+ rail: false,
27
+ kind: "modal",
28
+ closedMode: "rail"
29
+ }
30
+ );
31
+ });
32
+
33
+ test("desktopDrawerClosedMode hidden preserves an explicitly hidden wide drawer", () => {
34
+ assert.equal(normalizeDesktopDrawerClosedMode("hidden"), "hidden");
35
+ assert.deepEqual(
36
+ resolveShellDrawerPresentation({
37
+ compact: false,
38
+ open: false,
39
+ desktopClosedMode: "hidden"
40
+ }),
41
+ {
42
+ visible: false,
43
+ rail: false,
44
+ kind: "hidden",
45
+ closedMode: "hidden"
46
+ }
47
+ );
48
+ });
49
+
50
+ test("open navigation renders as a full drawer and labels its toggle responsively", () => {
51
+ assert.deepEqual(
52
+ resolveShellDrawerPresentation({ compact: false, open: true }),
53
+ {
54
+ visible: true,
55
+ rail: false,
56
+ kind: "drawer",
57
+ closedMode: "rail"
58
+ }
59
+ );
60
+ assert.equal(
61
+ resolveShellDrawerToggleLabel({ compact: false, open: true }),
62
+ "Collapse navigation drawer"
63
+ );
64
+ assert.equal(
65
+ resolveShellDrawerToggleLabel({ compact: true, open: false }),
66
+ "Open navigation menu"
67
+ );
68
+ });
@@ -151,10 +151,14 @@ test("shell-web generic link items support the expected shared route and icon be
151
151
 
152
152
  assert.match(shellMenuSource, /exact:\s*\{/);
153
153
  assert.match(shellMenuSource, /:exact="props\.exact"/);
154
+ assert.match(shellMenuSource, /:aria-label="props\.label"/);
155
+ assert.match(shellMenuSource, /v-tooltip="props\.label"/);
154
156
  assert.match(shellMenuSource, /class="shell-menu-link-item"/);
155
157
  assert.match(shellMenuSource, /min-height:\s*48px/);
156
158
  assert.match(shellSurfaceAwareSource, /exact:\s*\{/);
157
159
  assert.match(shellSurfaceAwareSource, /:exact="props\.exact"/);
160
+ assert.match(shellSurfaceAwareSource, /:aria-label="props\.label"/);
161
+ assert.match(shellSurfaceAwareSource, /v-tooltip="props\.label"/);
158
162
  assert.match(shellSurfaceAwareSource, /class="shell-menu-link-item"/);
159
163
  assert.match(shellSurfaceAwareSource, /min-height:\s*48px/);
160
164
  assert.match(shellTabSource, /icon:\s*\{/);
@@ -95,6 +95,12 @@ test("shell-web shell layout registers navigation at the app layout level", asyn
95
95
  assert.match(source, /target="shell-layout:primary-menu"[\s\S]*default/);
96
96
  assert.doesNotMatch(source, /target="shell-layout:primary-bottom-nav"[\s\S]*default/);
97
97
  assert.match(source, /data-testid="jskit-shell-drawer"/);
98
+ assert.match(source, /desktopDrawerClosedMode/);
99
+ assert.match(source, /default: "rail"/);
100
+ assert.match(source, /:rail="drawerPresentation\.rail"/);
101
+ assert.match(source, /:rail-width="80"/);
102
+ assert.match(source, /:model-value="drawerPresentation\.visible"/);
103
+ assert.match(source, /data-testid="jskit-shell-nav-toggle"/);
98
104
  assert.match(source, /data-testid="jskit-shell-bottom-nav"/);
99
105
  assert.match(source, /padding:\s*0\.75rem 1rem calc\(1rem \+ env\(safe-area-inset-bottom, 0px\)\)/);
100
106
 
@@ -288,8 +294,9 @@ test("shell-web settings general child page exposes an adaptive drawer preferenc
288
294
  assert.match(source, /generated-ui-screen generated-ui-screen--settings settings-general-screen/);
289
295
  assert.match(source, /drawerDefaultOpen/);
290
296
  assert.match(source, /setDrawerDefaultOpen/);
291
- assert.match(source, /Phone layouts keep primary navigation in the bottom bar/);
292
- assert.match(source, /Open drawer by default on wider screens/);
297
+ assert.match(source, /collapsed navigation remains available as a rail/);
298
+ assert.match(source, /Phone layouts close the drawer/);
299
+ assert.match(source, /Start with expanded navigation on wider screens/);
293
300
  assert.match(source, /min-height:\s*48px/);
294
301
  assert.doesNotMatch(source, /live in this browser only|tiny example|starter settings/);
295
302
  });