@plastic-js/tsumiki 0.1.63 → 0.1.65

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
  };
@@ -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,13 +361,8 @@ var Item = (props) => {
360
361
  };
361
362
  var List = () => {
362
363
  const ctx = useSelect();
363
- let filterInputEl = null;
364
- createEffect(() => {
365
- if (ctx.open() && ctx.filter) queueMicrotask(() => filterInputEl?.focus());
366
- });
367
364
  const handleFilterClear = () => {
368
365
  ctx.query("");
369
- filterInputEl?.focus();
370
366
  };
371
367
  return jsx("div", mergeProps(() => part("list"), {
372
368
  className: listClass,
@@ -377,9 +373,6 @@ var List = () => {
377
373
  const _el3 = _el0.firstChild.nextSibling.nextSibling;
378
374
  insert(_el1, () => jsx(Icon, mergeProps({ svg: searchSvg })));
379
375
  setProp(_el1, "className", () => filterIconClass);
380
- setProp(_el2, "ref", (el) => {
381
- filterInputEl = el;
382
- });
383
376
  setProp(_el2, "className", () => `${filterInputClass} ${intentClasses[ctx.intent()] ?? intentClasses.default} ${ctx.invalid() ? errorClass : ""}`);
384
377
  setProp(_el2, "value", () => ctx.query);
385
378
  setProp(_el2, "onInput", (e) => ctx.query(e.target.value));
@@ -430,8 +423,9 @@ var Content = () => {
430
423
  return ctx.backdropStyle ?? { background: "var(--tsu-bg-overlay)" };
431
424
  },
432
425
  contentClassName: listBodyClass,
426
+ portal: !useDialogContext(),
433
427
  children: jsx(List, {})
434
428
  }));
435
429
  };
436
430
  //#endregion
437
- export { SelectTrigger, Select as default };
431
+ export { SelectTrigger, Select$1 as default };
@@ -22,6 +22,8 @@ var triggerClass = css({
22
22
  fontSize: "var(--tsu-comp-font-size-md)",
23
23
  fontFamily: "inherit",
24
24
  cursor: "pointer",
25
+ userSelect: "none",
26
+ WebkitUserSelect: "none",
25
27
  whiteSpace: "nowrap",
26
28
  "&[data-selected]": { color: "var(--tsu-accent-subtle-fg)" },
27
29
  "&[data-disabled]": {
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.63",
3
+ "version": "0.1.65",
4
4
  "description": "A UI component library for Plastic JS.",
5
5
  "license": "MIT",
6
6
  "author": "tigre",