@jskit-ai/shell-web 0.1.187 → 0.1.188

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,14 +1,25 @@
1
1
  <script setup>
2
- import { computed } from "vue";
2
+ import { computed, ref } from "vue";
3
3
  import { useRoute } from "vue-router";
4
4
 
5
5
  const route = useRoute();
6
6
  const title = computed(() => String(route.meta.title || "Shell fixture"));
7
+ const answer = ref("");
8
+ const validation = ref("");
7
9
  </script>
8
10
 
9
11
  <template>
10
12
  <main tabindex="-1">
11
13
  <h1>{{ title }}</h1>
12
14
  <p>Package-owned browser fixture for the adaptive shell contract.</p>
15
+ <form @submit.prevent="validation = 'Please review this answer.'">
16
+ <label>Unsaved answer <input v-model="answer" name="answer"></label>
17
+ <details>
18
+ <summary>More details</summary>
19
+ <p>Expanded form section.</p>
20
+ </details>
21
+ <button type="submit">Validate answer</button>
22
+ <p role="status">{{ validation }}</p>
23
+ </form>
13
24
  </main>
14
25
  </template>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/shell-web",
3
- "version": "0.1.187",
3
+ "version": "0.1.188",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -31,7 +31,7 @@
31
31
  "@mdi/js": "^7.4.47"
32
32
  },
33
33
  "peerDependencies": {
34
- "@jskit-ai/kernel": "0.1.183",
34
+ "@jskit-ai/kernel": "0.1.184",
35
35
  "pinia": "^3.0.4",
36
36
  "vue": "^3.5.13",
37
37
  "vue-router": "^5.0.4",
@@ -233,16 +233,20 @@ const routeTransitionName = computed(() => {
233
233
  return "";
234
234
  });
235
235
 
236
- const routeTransitionKey = computed(() => {
237
- const routePathKey = routeTransitionName.value
238
- ? normalizeComparablePathname(route?.path || route?.fullPath || "/")
239
- : "";
240
- return resolveShellRouteTransitionKey({
241
- routePathKey,
242
- routeTransitionName: routeTransitionName.value,
243
- surfaceId: currentSurfaceId.value
244
- });
245
- });
236
+ // Animation can change with viewport geometry; pane identity changes only with
237
+ // navigation. Resizing must not recreate the routed form or lose focus.
238
+ const routeTransitionKey = ref("");
239
+ watch(
240
+ [() => normalizeComparablePathname(route?.path || route?.fullPath || "/"), currentSurfaceId],
241
+ ([routePathKey, surfaceId]) => {
242
+ routeTransitionKey.value = resolveShellRouteTransitionKey({
243
+ routePathKey,
244
+ routeTransitionName: routeTransitionName.value,
245
+ surfaceId
246
+ });
247
+ },
248
+ { immediate: true, flush: "sync" }
249
+ );
246
250
 
247
251
  const swipeNavigationEnabled = computed(() =>
248
252
  Boolean(
@@ -301,3 +301,63 @@ test("shell-web adaptive navigation passes package-owned browser contracts", {
301
301
  await stopProcess(vite);
302
302
  }
303
303
  });
304
+
305
+ test("shell-web preserves the active form across responsive changes", {
306
+ skip: RUN_BROWSER_TEST ? false : "set JSKIT_SHELL_WEB_BROWSER_INTEGRATION=1",
307
+ timeout: 90_000
308
+ }, async () => {
309
+ const vite = await startViteFixture({ fixtureRoot: FIXTURE_ROOT });
310
+ let browser;
311
+ try {
312
+ browser = await chromium.launch(createChromiumLaunchOptions());
313
+ for (const startingWidth of [1280, 390]) {
314
+ const page = await browser.newPage({ baseURL: vite.baseURL, viewport: { width: startingWidth, height: 900 } });
315
+ await page.goto("/w/acme/admin/contacts");
316
+ await page.getByRole("link", { name: "Bookings", exact: true }).click();
317
+ await expect(page).toHaveURL(/\/bookings$/u);
318
+ await expect(page.locator(".shell-route-transition__pane")).toHaveCount(1);
319
+ const answer = page.getByRole("textbox", { name: "Unsaved answer" });
320
+ await answer.fill("Keep this unsaved answer");
321
+ await page.getByText("More details", { exact: true }).click();
322
+ await page.getByRole("button", { name: "Validate answer" }).click();
323
+ await answer.focus();
324
+ const originalInput = await answer.elementHandle();
325
+ for (const viewport of [
326
+ { width: 390, height: 844 },
327
+ { width: 1024, height: 1024 },
328
+ { width: 1280, height: 900 },
329
+ { width: 1280, height: 420 },
330
+ { width: 390, height: 420 },
331
+ { width: 390, height: 844 }
332
+ ]) {
333
+ await page.setViewportSize(viewport);
334
+ await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))));
335
+ await expect(answer).toHaveValue("Keep this unsaved answer");
336
+ await expect(page.locator("details")).toHaveAttribute("open", "");
337
+ await expect(page.getByRole("status")).toHaveText("Please review this answer.");
338
+ await expect(answer).toBeFocused();
339
+ assert.equal(await originalInput.evaluate((element) => element === document.activeElement), true);
340
+ await expect(page.locator(".shell-route-transition__pane")).toHaveCount(1);
341
+ await expect(page.getByRole("heading", { level: 1 })).toHaveCount(1);
342
+ }
343
+ // Real compact navigation still replaces the pane and runs its animation.
344
+ await page.getByRole("link", { name: "Contacts", exact: true }).click();
345
+ await expect(page).toHaveURL(/\/contacts$/u);
346
+ await expect(page.locator(".shell-route-transition__pane")).toHaveCount(1);
347
+ await expect(answer).toHaveValue("");
348
+ assert.equal(await originalInput.evaluate((element) => element.isConnected), false);
349
+ // Desktop same-surface navigation continues to reuse the routed component.
350
+ await page.setViewportSize({ width: 1280, height: 900 });
351
+ await page.getByRole("link", { name: "Bookings", exact: true }).click();
352
+ await expect(page).toHaveURL(/\/bookings$/u);
353
+ await answer.fill("Desktop state");
354
+ await page.getByRole("link", { name: "Contacts", exact: true }).click();
355
+ await expect(page).toHaveURL(/\/contacts$/u);
356
+ await expect(answer).toHaveValue("Desktop state");
357
+ await page.close();
358
+ }
359
+ } finally {
360
+ await browser?.close();
361
+ await stopProcess(vite);
362
+ }
363
+ });
@@ -262,10 +262,9 @@ test("shell-web route transition keeps mobile route motion placement-driven", as
262
262
  assert.match(source, /\.shell-route-transition__pane\s*\{[\s\S]*flex:\s*1 1 auto/);
263
263
  assert.match(source, /\.shell-route-transition__pane\s*\{[\s\S]*min-height:\s*0/);
264
264
  assert.match(source, /transitionDirection\.value = nextIndex > previousIndex \? "forward" : "reverse"/);
265
- assert.match(
266
- source,
267
- /const routeTransitionKey = computed\(\(\) => \{[\s\S]*const routePathKey = routeTransitionName\.value[\s\S]*normalizeComparablePathname\(route\?\.path \|\| route\?\.fullPath \|\| "\/"\)[\s\S]*resolveShellRouteTransitionKey\(\{[\s\S]*routeTransitionName: routeTransitionName\.value,[\s\S]*surfaceId: currentSurfaceId\.value[\s\S]*\}\);[\s\S]*\}\);/
268
- );
265
+ assert.match(source, /const routeTransitionKey = ref\(""\)/);
266
+ assert.match(source, /\[\(\) => normalizeComparablePathname\(route\?\.path \|\| route\?\.fullPath \|\| "\/"\), currentSurfaceId\]/);
267
+ assert.match(source, /routeTransitionKey\.value = resolveShellRouteTransitionKey/);
269
268
  assert.match(source, /prefers-reduced-motion:\s*reduce/);
270
269
  });
271
270