@insymetri/styleguide 0.1.91 → 0.1.92

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.
@@ -24,6 +24,27 @@
24
24
  triggerClass?: string
25
25
  /** Strip the default surface (background, border, radius, shadow) — for content that renders its own. */
26
26
  unstyled?: boolean
27
+ /**
28
+ * Fired when a pointer interaction starts outside the popover. Call
29
+ * `event.preventDefault()` to keep the popover open. Interactions inside one of
30
+ * the styleguide's own portaled menus (IIDropdownInput, IIMenu…) are already
31
+ * kept — see `keepOpenForNestedMenus`.
32
+ */
33
+ onInteractOutside?: (event: PointerEvent) => void
34
+ /**
35
+ * Position the content against this element (or selector) instead of the
36
+ * trigger — e.g. a settings panel that should sit beside the whole card its gear
37
+ * button lives in. Clicks on the anchor don't count as outside interactions.
38
+ */
39
+ customAnchor?: HTMLElement | string | null
40
+ /**
41
+ * Whether the popover dismisses itself. Default `true`: a click outside or Escape
42
+ * closes it, as a menu should. Set `false` for a panel that owns the screen for a
43
+ * while and holds work in progress — a live call, a form mid-edit — where a stray
44
+ * click elsewhere in the app should not throw it away. It can then be closed only
45
+ * by its own trigger or by whoever owns `open`.
46
+ */
47
+ dismissable?: boolean
27
48
  }
28
49
 
29
50
  let {
@@ -38,11 +59,36 @@
38
59
  class: className,
39
60
  triggerClass,
40
61
  unstyled = false,
62
+ onInteractOutside,
63
+ customAnchor = null,
64
+ dismissable = true,
41
65
  }: Props = $props()
42
66
 
43
67
  const surfaceClasses = unstyled
44
68
  ? ''
45
69
  : 'bg-dropdown-bg border border-dropdown-border rounded-10 shadow-dropdown'
70
+
71
+ // The menu components (IIDropdownInput, IIMenu, …) portal their list to <body>,
72
+ // so to bits-ui a click on one of their options is a click OUTSIDE this popover:
73
+ // it dismissed the popover on pointerdown, unmounting the content — and the
74
+ // option along with it — before the click could land. A dropdown inside a popover
75
+ // therefore never changed its value. Every portaled menu marks its list with
76
+ // `data-menu-content`; an interaction starting there is ours, so keep the popover
77
+ // open and let the option's own click do its job.
78
+ function keepOpenForNestedMenus(event: PointerEvent) {
79
+ const target = event.target
80
+ if (!dismissable || (target instanceof Element && target.closest('[data-menu-content]'))) {
81
+ event.preventDefault()
82
+ }
83
+ onInteractOutside?.(event)
84
+ }
85
+
86
+ // bits-ui closes on Escape unless the handler defaults it away. A non-dismissable
87
+ // panel has to opt out of that too, or Escape would still throw away the very work
88
+ // the outside-click guard above is protecting.
89
+ function keepOpenOnEscape(event: KeyboardEvent) {
90
+ if (!dismissable) event.preventDefault()
91
+ }
46
92
  </script>
47
93
 
48
94
  <Popover.Root bind:open {onOpenChange}>
@@ -62,6 +108,9 @@
62
108
  {side}
63
109
  {align}
64
110
  {sideOffset}
111
+ {customAnchor}
112
+ onInteractOutside={keepOpenForNestedMenus}
113
+ onEscapeKeydown={keepOpenOnEscape}
65
114
  class={cn(
66
115
  surfaceClasses,
67
116
  'z-16 animate-slide-in motion-reduce:animate-none',
@@ -21,6 +21,27 @@ type Props = {
21
21
  triggerClass?: string;
22
22
  /** Strip the default surface (background, border, radius, shadow) — for content that renders its own. */
23
23
  unstyled?: boolean;
24
+ /**
25
+ * Fired when a pointer interaction starts outside the popover. Call
26
+ * `event.preventDefault()` to keep the popover open. Interactions inside one of
27
+ * the styleguide's own portaled menus (IIDropdownInput, IIMenu…) are already
28
+ * kept — see `keepOpenForNestedMenus`.
29
+ */
30
+ onInteractOutside?: (event: PointerEvent) => void;
31
+ /**
32
+ * Position the content against this element (or selector) instead of the
33
+ * trigger — e.g. a settings panel that should sit beside the whole card its gear
34
+ * button lives in. Clicks on the anchor don't count as outside interactions.
35
+ */
36
+ customAnchor?: HTMLElement | string | null;
37
+ /**
38
+ * Whether the popover dismisses itself. Default `true`: a click outside or Escape
39
+ * closes it, as a menu should. Set `false` for a panel that owns the screen for a
40
+ * while and holds work in progress — a live call, a form mid-edit — where a stray
41
+ * click elsewhere in the app should not throw it away. It can then be closed only
42
+ * by its own trigger or by whoever owns `open`.
43
+ */
44
+ dismissable?: boolean;
24
45
  };
25
46
  declare const IIPopover: import("svelte").Component<Props, {}, "open">;
26
47
  type IIPopover = ReturnType<typeof IIPopover>;
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ import IIPopover from './IIPopover.svelte'
3
+ import {IIButton} from '../IIButton'
4
+ import {IIDropdownInput} from '../IIDropdownInput'
5
+
6
+ // Fixture for the play test: a menu-style input inside a popover. Its option
7
+ // list portals to <body>, so choosing an option used to read as an outside
8
+ // click and dismiss the popover before the choice registered.
9
+ let open = $state(false)
10
+ let device = $state('')
11
+ </script>
12
+
13
+ <IIPopover bind:open>
14
+ {#snippet trigger()}
15
+ <IIButton variant="secondary" size="sm">Audio settings</IIButton>
16
+ {/snippet}
17
+ {#snippet content()}
18
+ <div class="p-12 w-240 flex flex-col gap-8">
19
+ <p class="text-small text-secondary">Speaker</p>
20
+ <IIDropdownInput
21
+ items={[
22
+ {label: 'System Default', value: ''},
23
+ {label: 'Headset', value: 'headset'},
24
+ {label: 'Desk speakers', value: 'desk'},
25
+ ]}
26
+ bind:value={device}
27
+ class="w-full"
28
+ />
29
+ <p class="text-small text-body" data-testid="chosen">Chosen: {device || 'default'}</p>
30
+ </div>
31
+ {/snippet}
32
+ </IIPopover>
@@ -0,0 +1,3 @@
1
+ declare const IIPopoverNestedMenuProof: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type IIPopoverNestedMenuProof = ReturnType<typeof IIPopoverNestedMenuProof>;
3
+ export default IIPopoverNestedMenuProof;
@@ -0,0 +1,22 @@
1
+ <script lang="ts">
2
+ import IIPopover from './IIPopover.svelte'
3
+ import {IIButton} from '../IIButton'
4
+
5
+ // Fixture for the play test: a panel that must survive a click elsewhere in the app
6
+ // and an Escape press, and close only from its own trigger.
7
+ let open = $state(false)
8
+ </script>
9
+
10
+ <div class="flex flex-col gap-16">
11
+ <IIPopover bind:open dismissable={false}>
12
+ {#snippet trigger()}
13
+ <IIButton variant="secondary" size="sm">{open ? 'Collapse panel' : 'Open panel'}</IIButton>
14
+ {/snippet}
15
+ {#snippet content()}
16
+ <div class="p-12 w-200">
17
+ <p class="text-small text-body">Work in progress.</p>
18
+ </div>
19
+ {/snippet}
20
+ </IIPopover>
21
+ <button type="button" data-testid="elsewhere" class="text-small text-secondary">Somewhere else</button>
22
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const IIPopoverNonDismissableProof: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type IIPopoverNonDismissableProof = ReturnType<typeof IIPopoverNonDismissableProof>;
3
+ export default IIPopoverNonDismissableProof;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@insymetri/styleguide",
3
- "version": "0.1.91",
3
+ "version": "0.1.92",
4
4
  "description": "Insymetri shared UI component library built with Svelte 5",
5
5
  "type": "module",
6
6
  "scripts": {