@plastic-js/tsumiki 0.1.64 → 0.1.66
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,5 +1,5 @@
|
|
|
1
1
|
import Portal_default from "./Portal.js";
|
|
2
|
-
import { insert, jsx, mergeProps, setProp, template } from "@plastic-js/plastic/jsx-runtime";
|
|
2
|
+
import { Fragment, insert, jsx, mergeProps, setProp, template } from "@plastic-js/plastic/jsx-runtime";
|
|
3
3
|
import { css, keyframes } from "@emotion/css";
|
|
4
4
|
import { Either, createEffect, createSignal, mergeProps as mergeProps$1, onCleanup, splitProps } from "@plastic-js/plastic";
|
|
5
5
|
//#region src/components/BottomSheet.jsx
|
|
@@ -102,7 +102,8 @@ var BottomSheet = (props) => {
|
|
|
102
102
|
backdropBlur: true,
|
|
103
103
|
draggable: true,
|
|
104
104
|
lazyMount: true,
|
|
105
|
-
unmountOnExit: true
|
|
105
|
+
unmountOnExit: true,
|
|
106
|
+
portal: true
|
|
106
107
|
}, props), [
|
|
107
108
|
"open",
|
|
108
109
|
"onOpen",
|
|
@@ -116,6 +117,7 @@ var BottomSheet = (props) => {
|
|
|
116
117
|
"contentClassName",
|
|
117
118
|
"lazyMount",
|
|
118
119
|
"unmountOnExit",
|
|
120
|
+
"portal",
|
|
119
121
|
"class",
|
|
120
122
|
"children"
|
|
121
123
|
]);
|
|
@@ -193,47 +195,48 @@ var BottomSheet = (props) => {
|
|
|
193
195
|
settle(false);
|
|
194
196
|
};
|
|
195
197
|
const showGrabber = read(local.draggable);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
198
|
+
const renderOverlay = () => jsx(Fragment, { children: [(() => {
|
|
199
|
+
const _el0 = _tmpl.cloneNode(true);
|
|
200
|
+
setProp(_el0, "className", () => [
|
|
201
|
+
backdropClass,
|
|
202
|
+
local.backdropBlur && backdropBlurClass,
|
|
203
|
+
local.backdropClassName
|
|
204
|
+
].filter(Boolean).join(" "));
|
|
205
|
+
setProp(_el0, "style", () => local.backdropStyle);
|
|
206
|
+
setProp(_el0, "data-open", () => read(local.open) ? "" : void 0);
|
|
207
|
+
setProp(_el0, "onClick", () => handleBackdropClick);
|
|
208
|
+
return _el0;
|
|
209
|
+
})(), () => {
|
|
210
|
+
const _el0 = _tmpl2.cloneNode(true);
|
|
211
|
+
const _el1 = _el0.firstChild;
|
|
212
|
+
const _el2 = _el0.firstChild.nextSibling;
|
|
213
|
+
insert(_el0, () => showGrabber && (() => {
|
|
214
|
+
const _el0 = _tmpl3.cloneNode(true);
|
|
211
215
|
const _el1 = _el0.firstChild;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
setProp(_el0, "onPointerDown", () => onGrabberPointerDown);
|
|
219
|
-
setProp(_el0, "onPointerMove", () => onGrabberPointerMove);
|
|
220
|
-
setProp(_el0, "onPointerUp", () => onGrabberPointerUp);
|
|
221
|
-
setProp(_el0, "onPointerCancel", () => onGrabberPointerCancel);
|
|
222
|
-
return _el0;
|
|
223
|
-
})(), _el1);
|
|
224
|
-
insert(_el2, () => local.children);
|
|
225
|
-
setProp(_el2, "className", () => [contentClass, local.contentClassName].filter(Boolean).join(" "));
|
|
226
|
-
setProp(_el0, "ref", (el) => {
|
|
227
|
-
sheetEl = el;
|
|
228
|
-
});
|
|
229
|
-
setProp(_el0, "className", () => [
|
|
230
|
-
sheetClass,
|
|
231
|
-
dragging() && draggingClass,
|
|
232
|
-
local.class
|
|
233
|
-
].filter(Boolean).join(" "));
|
|
234
|
-
setProp(_el0, "data-open", () => read(local.open) ? "" : void 0);
|
|
216
|
+
setProp(_el1, "className", () => grabberClass);
|
|
217
|
+
setProp(_el0, "className", () => grabberAreaClass);
|
|
218
|
+
setProp(_el0, "onPointerDown", () => onGrabberPointerDown);
|
|
219
|
+
setProp(_el0, "onPointerMove", () => onGrabberPointerMove);
|
|
220
|
+
setProp(_el0, "onPointerUp", () => onGrabberPointerUp);
|
|
221
|
+
setProp(_el0, "onPointerCancel", () => onGrabberPointerCancel);
|
|
235
222
|
return _el0;
|
|
236
|
-
}
|
|
223
|
+
})(), _el1);
|
|
224
|
+
insert(_el2, () => local.children);
|
|
225
|
+
setProp(_el2, "className", () => [contentClass, local.contentClassName].filter(Boolean).join(" "));
|
|
226
|
+
setProp(_el0, "ref", (el) => {
|
|
227
|
+
sheetEl = el;
|
|
228
|
+
});
|
|
229
|
+
setProp(_el0, "className", () => [
|
|
230
|
+
sheetClass,
|
|
231
|
+
dragging() && draggingClass,
|
|
232
|
+
local.class
|
|
233
|
+
].filter(Boolean).join(" "));
|
|
234
|
+
setProp(_el0, "data-open", () => read(local.open) ? "" : void 0);
|
|
235
|
+
return _el0;
|
|
236
|
+
}] });
|
|
237
|
+
return jsx(Either, mergeProps({
|
|
238
|
+
condition: () => render(),
|
|
239
|
+
trueBranch: () => read(local.portal) ? jsx(Portal_default, mergeProps({ children: () => renderOverlay() })) : renderOverlay(),
|
|
237
240
|
falseBranch: () => null
|
|
238
241
|
}));
|
|
239
242
|
};
|
|
@@ -281,7 +281,8 @@ var DatePicker = (props = {}) => {
|
|
|
281
281
|
size: "md",
|
|
282
282
|
disabled: false,
|
|
283
283
|
intent: "default",
|
|
284
|
-
invalid: false
|
|
284
|
+
invalid: false,
|
|
285
|
+
closeOnBackdrop: true
|
|
285
286
|
}, props), [
|
|
286
287
|
"value",
|
|
287
288
|
"onValueChange",
|
|
@@ -290,7 +291,8 @@ var DatePicker = (props = {}) => {
|
|
|
290
291
|
"size",
|
|
291
292
|
"intent",
|
|
292
293
|
"invalid",
|
|
293
|
-
"className"
|
|
294
|
+
"className",
|
|
295
|
+
"closeOnBackdrop"
|
|
294
296
|
]);
|
|
295
297
|
const open = createSignal(false);
|
|
296
298
|
const resolveValue = () => {
|
|
@@ -396,7 +398,7 @@ var DatePicker = (props = {}) => {
|
|
|
396
398
|
const _el22 = _el21.firstChild;
|
|
397
399
|
const _el23 = _el21.firstChild.nextSibling;
|
|
398
400
|
setProp(_el1, "className", () => backdropClass);
|
|
399
|
-
setProp(_el1, "onClick", () => cancel);
|
|
401
|
+
setProp(_el1, "onClick", () => read(local.closeOnBackdrop) ? cancel : void 0);
|
|
400
402
|
setProp(_el4, "className", () => toolbarBtnClass);
|
|
401
403
|
setProp(_el4, "onClick", () => cancel);
|
|
402
404
|
setProp(_el5, "className", () => toolbarTitleClass);
|
|
@@ -2,7 +2,8 @@ import BottomSheet from "./BottomSheet.js";
|
|
|
2
2
|
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
|
-
import {
|
|
5
|
+
import { useDialogContext } from "@plastic-js/ark";
|
|
6
|
+
import { Loop, createContext, createEffect, createSignal, mergeProps as mergeProps$2, splitProps, useContext } from "@plastic-js/plastic";
|
|
6
7
|
//#region src/components/Select.jsx
|
|
7
8
|
var _tmpl3 = template("<div>No results</div>");
|
|
8
9
|
var _tmpl2 = template("<button type=\"button\" aria-label=\"Clear search\"></button>");
|
|
@@ -209,8 +210,8 @@ var useSelect = () => {
|
|
|
209
210
|
var read = (source) => {
|
|
210
211
|
return typeof source === "function" ? source() : source;
|
|
211
212
|
};
|
|
212
|
-
var Select = (props) => {
|
|
213
|
-
const [local, rest] = splitProps(mergeProps$
|
|
213
|
+
var Select$1 = (props) => {
|
|
214
|
+
const [local, rest] = splitProps(mergeProps$2({
|
|
214
215
|
size: "md",
|
|
215
216
|
variant: "outline",
|
|
216
217
|
intent: "default",
|
|
@@ -360,11 +361,20 @@ var Item = (props) => {
|
|
|
360
361
|
};
|
|
361
362
|
var List = () => {
|
|
362
363
|
const ctx = useSelect();
|
|
364
|
+
const heldHeight = createSignal(0);
|
|
365
|
+
const holdRef = (el) => {
|
|
366
|
+
if (!el || !ctx.filter || heldHeight()) return;
|
|
367
|
+
requestAnimationFrame(() => {
|
|
368
|
+
if (el && !heldHeight()) heldHeight(el.clientHeight);
|
|
369
|
+
});
|
|
370
|
+
};
|
|
363
371
|
const handleFilterClear = () => {
|
|
364
372
|
ctx.query("");
|
|
365
373
|
};
|
|
366
374
|
return jsx("div", mergeProps(() => part("list"), {
|
|
375
|
+
ref: holdRef,
|
|
367
376
|
className: listClass,
|
|
377
|
+
style: () => heldHeight() ? { minHeight: `${heldHeight()}px` } : void 0,
|
|
368
378
|
children: [() => ctx.filter && (() => {
|
|
369
379
|
const _el0 = _tmpl.cloneNode(true);
|
|
370
380
|
const _el1 = _el0.firstChild;
|
|
@@ -422,8 +432,9 @@ var Content = () => {
|
|
|
422
432
|
return ctx.backdropStyle ?? { background: "var(--tsu-bg-overlay)" };
|
|
423
433
|
},
|
|
424
434
|
contentClassName: listBodyClass,
|
|
435
|
+
portal: !useDialogContext(),
|
|
425
436
|
children: jsx(List, {})
|
|
426
437
|
}));
|
|
427
438
|
};
|
|
428
439
|
//#endregion
|
|
429
|
-
export { SelectTrigger, Select as default };
|
|
440
|
+
export { SelectTrigger, Select$1 as default };
|
package/docs/api.md
CHANGED
|
@@ -729,6 +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**, keeping them inside a parent Dialog's focus-trap boundary — required for a filter `input` nested in a Dialog on iOS Safari (WebKit focus-lock). The overlay is `position: fixed`, so it still covers the viewport when inline. |
|
|
732
733
|
| `class` | `string` | — | Extra class for the sheet. |
|
|
733
734
|
| `children` | `node` | — | **Required** body content. |
|
|
734
735
|
|
|
@@ -0,0 +1,38 @@
|
|
|
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.
|