@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
- return jsx(Either, mergeProps({
197
- condition: () => render(),
198
- trueBranch: () => jsx(Portal_default, mergeProps({ 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);
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
- const _el2 = _el0.firstChild.nextSibling;
213
- insert(_el0, () => showGrabber && (() => {
214
- const _el0 = _tmpl3.cloneNode(true);
215
- const _el1 = _el0.firstChild;
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);
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 { Loop, createContext, createEffect, createSignal, mergeProps as mergeProps$1, splitProps, useContext } from "@plastic-js/plastic";
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$1({
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plastic-js/tsumiki",
3
- "version": "0.1.64",
3
+ "version": "0.1.66",
4
4
  "description": "A UI component library for Plastic JS.",
5
5
  "license": "MIT",
6
6
  "author": "tigre",