@fairfox/polly 0.73.0 → 0.74.0

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.
@@ -9,9 +9,11 @@
9
9
  * <ActionInput> and <ActionForm>, with no synthetic signal or bridging
10
10
  * `effect` required.
11
11
  *
12
- * Composes <Dropdown> for the menu. The trigger is a plain <span> (not a
13
- * nested <button>) so the Dropdown's own button is the single
14
- * interactive element. A disabled ActionSelect renders as static text.
12
+ * Composes <Dropdown> for the menu. The trigger styling is applied
13
+ * directly to Dropdown's own <button> via `triggerClassName`, so the
14
+ * visible box and the interactive element are one and the same node
15
+ * no styled <span> nested inside an unstyled <button>. A disabled
16
+ * ActionSelect renders as static text without a caret.
15
17
  */
16
18
  import type { JSX } from "preact";
17
19
  import { type PassthroughAttrs } from "./internal/passthrough.ts";
@@ -30,6 +32,8 @@ export type ActionSelectProps = PassthroughAttrs & {
30
32
  /** Trigger text shown when `value` matches no option. Default: "Select…". */
31
33
  placeholder?: string;
32
34
  disabled?: boolean;
35
+ /** Apply a comfortable minimum width to the trigger. Default: sizes to content. */
36
+ wide?: boolean;
33
37
  className?: string;
34
38
  id?: string;
35
39
  };
@@ -1,6 +1,12 @@
1
1
  /**
2
2
  * Dropdown — trigger button + popover menu using the native Popover API.
3
3
  *
4
+ * A shown popover is promoted to the browser's top layer, where CSS
5
+ * positioning resolves against the viewport rather than the `.dropdown`
6
+ * wrapper — so the menu cannot be placed with `position: absolute`
7
+ * alone. Instead it is pinned with `position: fixed` and coordinates
8
+ * measured from the trigger; see `positionMenu` below (issue #128).
9
+ *
4
10
  * The menu element gets `popover="auto"` and a unique `data-overlay-id`.
5
11
  * `closeTopOverlay()` from @fairfox/polly/actions finds the topmost
6
12
  * `[data-overlay-id]` element and dispatches `overlay:close` — the
@@ -18,6 +24,15 @@ export type DropdownProps = {
18
24
  align?: "left" | "right";
19
25
  multiSelect?: boolean;
20
26
  className?: string;
27
+ /**
28
+ * Class applied to the trigger <button> in place of Dropdown's own
29
+ * default. Lets a composing component (Select, ActionSelect) style
30
+ * the single interactive element directly, with no styled node
31
+ * nested inside the button.
32
+ */
33
+ triggerClassName?: string;
34
+ /** Disables the trigger <button>. */
35
+ triggerDisabled?: boolean;
21
36
  id?: string;
22
37
  };
23
38
  export declare function Dropdown(props: DropdownProps): JSX.Element;
@@ -20,6 +20,8 @@ export type SelectProps<T = string> = {
20
20
  placeholder?: string;
21
21
  multiSelect?: boolean;
22
22
  disabled?: boolean;
23
+ /** Apply a comfortable minimum width to the trigger. Default: sizes to content. */
24
+ wide?: boolean;
23
25
  className?: string;
24
26
  id?: string;
25
27
  };
@@ -67,10 +67,9 @@
67
67
  }
68
68
 
69
69
  .menu_HX48zA {
70
- position: absolute;
70
+ position: fixed;
71
71
  inset: unset;
72
72
  z-index: var(--polly-z-raised);
73
- margin: var(--polly-space-xs) 0 0;
74
73
  padding: var(--polly-space-xs) 0;
75
74
  border: var(--polly-border-width-default) solid var(--polly-border);
76
75
  border-radius: var(--polly-radius-md);
@@ -80,13 +79,7 @@
80
79
  overflow-y: auto;
81
80
  min-width: 160px;
82
81
  max-height: 280px;
83
- top: 100%;
84
- left: 0;
85
- }
86
-
87
- .alignRight_HX48zA {
88
- left: auto;
89
- right: 0;
82
+ margin: 0;
90
83
  }
91
84
  }
92
85
 
@@ -163,7 +156,9 @@
163
156
  }
164
157
 
165
158
  .trigger_daofbw {
166
- display: inline-block;
159
+ display: inline-flex;
160
+ align-items: center;
161
+ gap: var(--polly-space-sm);
167
162
  padding: var(--polly-space-sm) var(--polly-space-md);
168
163
  border: var(--polly-border-width-default) solid var(--polly-border);
169
164
  border-radius: var(--polly-radius-sm);
@@ -173,15 +168,37 @@
173
168
  color: var(--polly-text);
174
169
  text-align: left;
175
170
  cursor: pointer;
171
+ max-width: 100%;
172
+ }
173
+
174
+ .trigger_daofbw:disabled, .trigger_daofbw[aria-disabled="true"] {
175
+ opacity: var(--polly-opacity-disabled);
176
+ cursor: not-allowed;
177
+ }
178
+
179
+ .triggerWide_daofbw {
180
+ min-width: 140px;
181
+ }
182
+
183
+ .triggerLabel_daofbw {
176
184
  white-space: nowrap;
177
185
  overflow: hidden;
178
186
  text-overflow: ellipsis;
179
- min-width: 140px;
187
+ flex: auto;
188
+ min-width: 0;
180
189
  }
181
190
 
182
- .trigger_daofbw:disabled {
183
- opacity: var(--polly-opacity-disabled);
184
- cursor: not-allowed;
191
+ .caret_daofbw {
192
+ color: var(--polly-text-muted);
193
+ pointer-events: none;
194
+ flex: none;
195
+ }
196
+
197
+ .caret_daofbw:before {
198
+ content: "▾";
199
+ display: block;
200
+ font-size: var(--polly-text-xs);
201
+ line-height: 1;
185
202
  }
186
203
 
187
204
  .placeholder_daofbw {
@@ -275,8 +275,7 @@ import { useEffect as useEffect2, useRef as useRef2 } from "preact/hooks";
275
275
  var Dropdown_module_default = {
276
276
  dropdown: "dropdown_HX48zA",
277
277
  trigger: "trigger_HX48zA",
278
- menu: "menu_HX48zA",
279
- alignRight: "alignRight_HX48zA"
278
+ menu: "menu_HX48zA"
280
279
  };
281
280
 
282
281
  // src/polly-ui/Layout.tsx
@@ -392,8 +391,20 @@ function Layout(props) {
392
391
  // src/polly-ui/Dropdown.tsx
393
392
  import { jsxDEV as jsxDEV3 } from "preact/jsx-dev-runtime";
394
393
  var dropdownCounter = 0;
394
+ var MENU_GAP = 4;
395
+ var VIEWPORT_PADDING = 8;
395
396
  function Dropdown(props) {
396
- const { isOpen, trigger, children, align = "left", multiSelect = false, className, id } = props;
397
+ const {
398
+ isOpen,
399
+ trigger,
400
+ children,
401
+ align = "left",
402
+ multiSelect = false,
403
+ className,
404
+ triggerClassName,
405
+ triggerDisabled = false,
406
+ id
407
+ } = props;
397
408
  const menuRef = useRef2(null);
398
409
  const triggerRef = useRef2(null);
399
410
  const idRef = useRef2(`polly-dropdown-${++dropdownCounter}`);
@@ -415,6 +426,60 @@ function Dropdown(props) {
415
426
  menu.removeEventListener("overlay:close", onOverlayClose);
416
427
  };
417
428
  }, [popoverId, isOpen]);
429
+ useEffect2(() => {
430
+ const menu = menuRef.current;
431
+ const trigger2 = triggerRef.current;
432
+ if (!menu || !trigger2)
433
+ return;
434
+ const positionMenu = () => {
435
+ const t = trigger2.getBoundingClientRect();
436
+ const prevDisplay = menu.style.display;
437
+ menu.style.maxHeight = "";
438
+ menu.style.display = "block";
439
+ const menuWidth = menu.offsetWidth;
440
+ const menuHeight = menu.offsetHeight;
441
+ menu.style.display = prevDisplay;
442
+ const viewportWidth = document.documentElement.clientWidth;
443
+ const viewportHeight = document.documentElement.clientHeight;
444
+ let left = align === "right" ? t.right - menuWidth : t.left;
445
+ const maxLeft = Math.max(VIEWPORT_PADDING, viewportWidth - menuWidth - VIEWPORT_PADDING);
446
+ left = Math.min(Math.max(left, VIEWPORT_PADDING), maxLeft);
447
+ const spaceBelow = viewportHeight - t.bottom - MENU_GAP - VIEWPORT_PADDING;
448
+ const spaceAbove = t.top - MENU_GAP - VIEWPORT_PADDING;
449
+ let top;
450
+ if (menuHeight <= spaceBelow || spaceBelow >= spaceAbove) {
451
+ top = t.bottom + MENU_GAP;
452
+ if (menuHeight > spaceBelow) {
453
+ menu.style.maxHeight = `${Math.max(spaceBelow, 0)}px`;
454
+ }
455
+ } else {
456
+ const available = Math.max(spaceAbove, 0);
457
+ menu.style.maxHeight = `${available}px`;
458
+ top = t.top - MENU_GAP - Math.min(menuHeight, available);
459
+ }
460
+ menu.style.position = "fixed";
461
+ menu.style.margin = "0";
462
+ menu.style.left = `${left}px`;
463
+ menu.style.top = `${top}px`;
464
+ };
465
+ const onBeforeToggle = (e) => {
466
+ if (e.newState === "open") {
467
+ positionMenu();
468
+ }
469
+ };
470
+ const onReposition = () => {
471
+ if (menu.matches(":popover-open"))
472
+ positionMenu();
473
+ };
474
+ menu.addEventListener("beforetoggle", onBeforeToggle);
475
+ window.addEventListener("scroll", onReposition, true);
476
+ window.addEventListener("resize", onReposition);
477
+ return () => {
478
+ menu.removeEventListener("beforetoggle", onBeforeToggle);
479
+ window.removeEventListener("scroll", onReposition, true);
480
+ window.removeEventListener("resize", onReposition);
481
+ };
482
+ }, [align]);
418
483
  useSignalEffect(() => {
419
484
  const menu = menuRef.current;
420
485
  if (!menu)
@@ -443,9 +508,6 @@ function Dropdown(props) {
443
508
  const parts = [Dropdown_module_default["dropdown"] ?? ""];
444
509
  if (className)
445
510
  parts.push(className);
446
- const menuParts = [Dropdown_module_default["menu"] ?? ""];
447
- if (align === "right")
448
- menuParts.push(Dropdown_module_default["alignRight"] ?? "");
449
511
  return /* @__PURE__ */ jsxDEV3("div", {
450
512
  id,
451
513
  class: parts.filter(Boolean).join(" "),
@@ -455,14 +517,16 @@ function Dropdown(props) {
455
517
  /* @__PURE__ */ jsxDEV3("button", {
456
518
  ref: triggerRef,
457
519
  type: "button",
458
- class: Dropdown_module_default["trigger"],
520
+ class: triggerClassName ?? Dropdown_module_default["trigger"] ?? "",
521
+ disabled: triggerDisabled,
459
522
  children: trigger
460
523
  }, undefined, false, undefined, this),
461
524
  /* @__PURE__ */ jsxDEV3("div", {
462
525
  ref: menuRef,
463
526
  id: popoverId,
464
527
  role: "listbox",
465
- class: menuParts.filter(Boolean).join(" "),
528
+ class: Dropdown_module_default["menu"] ?? "",
529
+ "data-align": align,
466
530
  popover: "auto",
467
531
  "data-overlay-id": popoverId,
468
532
  onToggle: handleToggle,
@@ -483,6 +547,9 @@ var Select_module_default = {
483
547
  select: "select_daofbw",
484
548
  label: "label_daofbw",
485
549
  trigger: "trigger_daofbw",
550
+ triggerWide: "triggerWide_daofbw",
551
+ triggerLabel: "triggerLabel_daofbw",
552
+ caret: "caret_daofbw",
486
553
  placeholder: "placeholder_daofbw",
487
554
  actions: "actions_daofbw",
488
555
  actionBtn: "actionBtn_daofbw",
@@ -492,7 +559,7 @@ var Select_module_default = {
492
559
  };
493
560
 
494
561
  // src/polly-ui/ActionSelect.tsx
495
- import { jsxDEV as jsxDEV4 } from "preact/jsx-dev-runtime";
562
+ import { jsxDEV as jsxDEV4, Fragment } from "preact/jsx-dev-runtime";
496
563
  function labelFor(options, value) {
497
564
  for (const opt of options) {
498
565
  if (opt.value === value)
@@ -509,6 +576,7 @@ function ActionSelect(props) {
509
576
  label,
510
577
  placeholder = "Select…",
511
578
  disabled = false,
579
+ wide = false,
512
580
  className,
513
581
  id
514
582
  } = props;
@@ -522,7 +590,12 @@ function ActionSelect(props) {
522
590
  return;
523
591
  dispatchAction(action, { ...actionData ?? {}, value: next });
524
592
  };
525
- const triggerClass = isEmpty ? `${Select_module_default["trigger"]} ${Select_module_default["placeholder"]}` : Select_module_default["trigger"];
593
+ const triggerParts = [Select_module_default["trigger"] ?? ""];
594
+ if (isEmpty)
595
+ triggerParts.push(Select_module_default["placeholder"] ?? "");
596
+ if (wide)
597
+ triggerParts.push(Select_module_default["triggerWide"] ?? "");
598
+ const triggerClass = triggerParts.filter(Boolean).join(" ");
526
599
  const parts = [Select_module_default["select"] ?? ""];
527
600
  if (className)
528
601
  parts.push(className);
@@ -541,13 +614,25 @@ function ActionSelect(props) {
541
614
  disabled ? /* @__PURE__ */ jsxDEV4("span", {
542
615
  class: triggerClass,
543
616
  "aria-disabled": "true",
544
- children: displayText
617
+ children: /* @__PURE__ */ jsxDEV4("span", {
618
+ class: Select_module_default["triggerLabel"],
619
+ children: displayText
620
+ }, undefined, false, undefined, this)
545
621
  }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV4(Dropdown, {
546
622
  isOpen,
547
- trigger: /* @__PURE__ */ jsxDEV4("span", {
548
- class: triggerClass,
549
- children: displayText
550
- }, undefined, false, undefined, this),
623
+ triggerClassName: triggerClass,
624
+ trigger: /* @__PURE__ */ jsxDEV4(Fragment, {
625
+ children: [
626
+ /* @__PURE__ */ jsxDEV4("span", {
627
+ class: Select_module_default["triggerLabel"],
628
+ children: displayText
629
+ }, undefined, false, undefined, this),
630
+ /* @__PURE__ */ jsxDEV4("span", {
631
+ class: Select_module_default["caret"],
632
+ "aria-hidden": "true"
633
+ }, undefined, false, undefined, this)
634
+ ]
635
+ }, undefined, true, undefined, this),
551
636
  children: options.map((opt) => {
552
637
  const isSelected = opt.value === value;
553
638
  const optClass = isSelected ? `${Select_module_default["option"]} ${Select_module_default["optionSelected"]}` : Select_module_default["option"];
@@ -1571,7 +1656,7 @@ function Host() {
1571
1656
  var ConfirmDialog = { Host, confirm };
1572
1657
  // src/polly-ui/Select.tsx
1573
1658
  import { useComputed, useSignal as useSignal2 } from "@preact/signals";
1574
- import { jsxDEV as jsxDEV14 } from "preact/jsx-dev-runtime";
1659
+ import { jsxDEV as jsxDEV14, Fragment as Fragment2 } from "preact/jsx-dev-runtime";
1575
1660
  function formatSelected(options, selected) {
1576
1661
  if (selected.size === 0)
1577
1662
  return "";
@@ -1590,6 +1675,7 @@ function Select(props) {
1590
1675
  placeholder = "Select…",
1591
1676
  multiSelect = false,
1592
1677
  disabled = false,
1678
+ wide = false,
1593
1679
  className,
1594
1680
  id
1595
1681
  } = props;
@@ -1618,13 +1704,24 @@ function Select(props) {
1618
1704
  const handleClear = () => {
1619
1705
  selected.value = new Set;
1620
1706
  };
1621
- const triggerClass = isEmpty.value ? `${Select_module_default["trigger"]} ${Select_module_default["placeholder"]}` : Select_module_default["trigger"];
1622
- const triggerButton = /* @__PURE__ */ jsxDEV14("button", {
1623
- type: "button",
1624
- class: triggerClass,
1625
- disabled,
1626
- children: displayText.value
1627
- }, undefined, false, undefined, this);
1707
+ const triggerParts = [Select_module_default["trigger"] ?? ""];
1708
+ if (isEmpty.value)
1709
+ triggerParts.push(Select_module_default["placeholder"] ?? "");
1710
+ if (wide)
1711
+ triggerParts.push(Select_module_default["triggerWide"] ?? "");
1712
+ const triggerClass = triggerParts.filter(Boolean).join(" ");
1713
+ const triggerContent = /* @__PURE__ */ jsxDEV14(Fragment2, {
1714
+ children: [
1715
+ /* @__PURE__ */ jsxDEV14("span", {
1716
+ class: Select_module_default["triggerLabel"],
1717
+ children: displayText.value
1718
+ }, undefined, false, undefined, this),
1719
+ /* @__PURE__ */ jsxDEV14("span", {
1720
+ class: Select_module_default["caret"],
1721
+ "aria-hidden": "true"
1722
+ }, undefined, false, undefined, this)
1723
+ ]
1724
+ }, undefined, true, undefined, this);
1628
1725
  const parts = [Select_module_default["select"] ?? ""];
1629
1726
  if (className)
1630
1727
  parts.push(className);
@@ -1640,7 +1737,9 @@ function Select(props) {
1640
1737
  }, undefined, false, undefined, this),
1641
1738
  /* @__PURE__ */ jsxDEV14(Dropdown, {
1642
1739
  isOpen: isOpen2,
1643
- trigger: triggerButton,
1740
+ trigger: triggerContent,
1741
+ triggerClassName: triggerClass,
1742
+ triggerDisabled: disabled,
1644
1743
  multiSelect,
1645
1744
  children: [
1646
1745
  multiSelect && /* @__PURE__ */ jsxDEV14("div", {
@@ -2100,4 +2199,4 @@ export {
2100
2199
  ActionForm
2101
2200
  };
2102
2201
 
2103
- //# debugId=7DA35338ECD4891D64756E2164756E21
2202
+ //# debugId=D646BF0B09E9BEC064756E2164756E21