@plastic-js/tsumiki 0.1.67 → 0.1.69
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/README.md +2 -2
- package/dist/components/Select.js +34 -3
- package/docs/api.md +1 -1
- package/package.json +2 -1
- package/docs/select-in-dialog-focus-lock.md +0 -38
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ function Example(){
|
|
|
81
81
|
| `className` | `string` | — | CSS class for the trigger button |
|
|
82
82
|
| `children` | `node` | — | Custom trigger content (replaces default label + chevron) |
|
|
83
83
|
|
|
84
|
-
> **Note on Trigger element:** The trigger renders `<div role="button" tabIndex={0}>` instead of a native `<button>` as a defense-in-depth measure against a **Chrome iOS (WebKit) focus-lock bug**.
|
|
84
|
+
> **Note on Trigger element:** The trigger renders `<div role="button" tabIndex={0}>` instead of a native `<button>` as a defense-in-depth measure against a **Chrome iOS (WebKit) focus-lock bug**. Inside a Dialog the sheet still portals to `<body>` as an independent overlay; the Dialog's focus trap is temporarily paused while the sheet is open (shared `@zag-js/focus-trap` stack) so the sheet's filter input keeps focus on iOS.
|
|
85
85
|
|
|
86
86
|
**Filter usage:**
|
|
87
87
|
|
|
@@ -95,7 +95,7 @@ function Example(){
|
|
|
95
95
|
<Select filter={(item, query, itemToLabel) => itemToLabel(item).toLowerCase().startsWith(query)} ...>
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
The search input is auto-focused
|
|
98
|
+
The search input is intentionally **not** auto-focused on open (auto-focus mid-slide leaves the field half-focused on iOS); tap to focus. It is cleared when the sheet closes. "No results" is shown when no items match the query.
|
|
99
99
|
|
|
100
100
|
---
|
|
101
101
|
|
|
@@ -3,8 +3,10 @@ import Icon from "./Icon.js";
|
|
|
3
3
|
import { Fragment, insert, jsx, mergeProps, setProp, template } from "@plastic-js/plastic/jsx-runtime";
|
|
4
4
|
import { css } from "@emotion/css";
|
|
5
5
|
import { useDialogContext } from "@plastic-js/ark";
|
|
6
|
-
import { Loop, createContext, createEffect, createSignal, mergeProps as mergeProps$2, splitProps, useContext } from "@plastic-js/plastic";
|
|
6
|
+
import { Loop, createContext, createEffect, createSignal, mergeProps as mergeProps$2, onCleanup, onMount, splitProps, useContext } from "@plastic-js/plastic";
|
|
7
|
+
import { trapFocus } from "@zag-js/focus-trap";
|
|
7
8
|
//#region src/components/Select.jsx
|
|
9
|
+
var _tmpl4 = template("<div></div>");
|
|
8
10
|
var _tmpl3 = template("<div>No results</div>");
|
|
9
11
|
var _tmpl2 = template("<button type=\"button\" aria-label=\"Clear search\"></button>");
|
|
10
12
|
var _tmpl = template("<div><span aria-hidden=\"true\"></span><input type=\"text\" placeholder=\"Search…\"><!></div>");
|
|
@@ -103,6 +105,12 @@ var triggerIndicatorClass = css({
|
|
|
103
105
|
}
|
|
104
106
|
});
|
|
105
107
|
var listBodyClass = css({ padding: 0 });
|
|
108
|
+
var pauseTrapWrapClass = css({
|
|
109
|
+
display: "flex",
|
|
110
|
+
flexDirection: "column",
|
|
111
|
+
flex: 1,
|
|
112
|
+
minHeight: 0
|
|
113
|
+
});
|
|
106
114
|
var listClass = css({
|
|
107
115
|
flex: 1,
|
|
108
116
|
minHeight: 0,
|
|
@@ -417,8 +425,31 @@ var List = () => {
|
|
|
417
425
|
}]
|
|
418
426
|
}));
|
|
419
427
|
};
|
|
428
|
+
var PauseDialogTrap = ({ children }) => {
|
|
429
|
+
let node = null;
|
|
430
|
+
let destroy = null;
|
|
431
|
+
onMount(() => {
|
|
432
|
+
if (!node) return;
|
|
433
|
+
destroy = trapFocus(() => node, {
|
|
434
|
+
initialFocus: false,
|
|
435
|
+
allowOutsideClick: true,
|
|
436
|
+
escapeDeactivates: false
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
onCleanup(() => destroy?.());
|
|
440
|
+
return () => {
|
|
441
|
+
const _el0 = _tmpl4.cloneNode(true);
|
|
442
|
+
insert(_el0, () => children);
|
|
443
|
+
setProp(_el0, "className", () => pauseTrapWrapClass);
|
|
444
|
+
setProp(_el0, "ref", (el) => {
|
|
445
|
+
node = el;
|
|
446
|
+
});
|
|
447
|
+
return _el0;
|
|
448
|
+
};
|
|
449
|
+
};
|
|
420
450
|
var Content = () => {
|
|
421
451
|
const ctx = useSelect();
|
|
452
|
+
const dialog = useDialogContext();
|
|
422
453
|
return jsx(BottomSheet, mergeProps({
|
|
423
454
|
get open() {
|
|
424
455
|
return ctx.open;
|
|
@@ -432,8 +463,8 @@ var Content = () => {
|
|
|
432
463
|
return ctx.backdropStyle ?? { background: "var(--tsu-bg-overlay)" };
|
|
433
464
|
},
|
|
434
465
|
contentClassName: listBodyClass,
|
|
435
|
-
portal:
|
|
436
|
-
children: jsx(List, {})
|
|
466
|
+
portal: true,
|
|
467
|
+
children: () => dialog ? jsx(PauseDialogTrap, mergeProps({ children: jsx(List, {}) })) : jsx(List, {})
|
|
437
468
|
}));
|
|
438
469
|
};
|
|
439
470
|
//#endregion
|
package/docs/api.md
CHANGED
|
@@ -729,7 +729,7 @@ A free-form presentation layer: a backdrop, an optional drag grabber, and a scro
|
|
|
729
729
|
| `backdropClassName` | `string` | — | Extra class for the backdrop. |
|
|
730
730
|
| `backdropStyle` | `object` | — | Extra style for the backdrop. |
|
|
731
731
|
| `contentClassName` | `string` | — | Extra class for the content area. |
|
|
732
|
-
| `portal` | `boolean` | `true` | Render the overlay into `<body>`. When `false` the backdrop + sheet render **inline
|
|
732
|
+
| `portal` | `boolean` | `true` | Render the overlay into `<body>`. When `false` the backdrop + sheet render **inline** at the call site. Prefer keeping `true`: inside a Dialog the overlay needs its own full-screen backdrop, and iOS pins `position: fixed` descendants to a touch-scroll container, so an inline sheet inside a Dialog body renders as plain dialog content. `Select` always portals and pauses the parent Dialog's focus trap while its sheet is open. |
|
|
733
733
|
| `class` | `string` | — | Extra class for the sheet. |
|
|
734
734
|
| `children` | `node` | — | **Required** body content. |
|
|
735
735
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plastic-js/tsumiki",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.69",
|
|
4
4
|
"description": "A UI component library for Plastic JS.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "tigre",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"@emotion/css": "^11.13.5",
|
|
60
|
+
"@zag-js/focus-trap": "1.40.0",
|
|
60
61
|
"@zag-js/select": "1.40.0",
|
|
61
62
|
"popmotion": "^11.0.5"
|
|
62
63
|
},
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# Select-in-Dialog filter input focus-lock on iOS
|
|
2
|
-
|
|
3
|
-
**Status:** Fixed
|
|
4
|
-
**Affects:** `Select.jsx` (nested inside `Dialog`)
|
|
5
|
-
|
|
6
|
-
## Problem
|
|
7
|
-
|
|
8
|
-
A `Select` with a filter input, nested inside a `Dialog`, could not be focused on
|
|
9
|
-
iOS Safari — tapping the search box did nothing and the keyboard never appeared.
|
|
10
|
-
|
|
11
|
-
The `Dialog` (ArkDialog `modal`) installs a focus trap that listens on
|
|
12
|
-
`document` and forces focus back into the dialog content whenever it escapes.
|
|
13
|
-
The Select's bottom sheet was portaled to `document.body`, which is **outside**
|
|
14
|
-
the dialog's focus-trap boundary. On iOS Safari the trap's `focusin` handler
|
|
15
|
-
stole focus back from the filter input on every tap, so the input never kept
|
|
16
|
-
focus long enough for the keyboard to appear.
|
|
17
|
-
|
|
18
|
-
The old `SelectMobile` component hit the exact same WebKit focus-lock bug and
|
|
19
|
-
fixed it by rendering the sheet **inline** (no `<Portal>`), keeping it inside
|
|
20
|
-
the dialog's focus-trap boundary. The current `Select` re-introduced the bug by
|
|
21
|
-
routing through `BottomSheet`, which always portaled.
|
|
22
|
-
|
|
23
|
-
## Fix
|
|
24
|
-
|
|
25
|
-
- `BottomSheet` gains a `portal` prop (default `true`, backward compatible).
|
|
26
|
-
When `false`, the backdrop + sheet render inline instead of into `<body>`.
|
|
27
|
-
- `Select.Content` reads `useDialogContext()` and passes `portal={!dialog}`:
|
|
28
|
-
inside a Dialog the sheet stays within the focus-trap boundary; standalone
|
|
29
|
-
usage still portals as before.
|
|
30
|
-
|
|
31
|
-
Because `BottomSheet`'s overlay is `position: fixed`, rendering inline still
|
|
32
|
-
covers the viewport correctly even when nested inside the dialog.
|
|
33
|
-
|
|
34
|
-
## e2e
|
|
35
|
-
|
|
36
|
-
`e2e/select-in-dialog-*` verifies the sheet stays inside the dialog content,
|
|
37
|
-
the filter input keeps focus after a tap, and closing the sheet via backdrop
|
|
38
|
-
leaves the dialog open.
|