@a4ui/core 0.37.0 → 0.37.1

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.
@@ -0,0 +1,16 @@
1
+ export interface AnchorReflowOptions {
2
+ /** The trigger the panel is anchored to. */
3
+ anchor: () => HTMLElement | undefined;
4
+ /** Re-reads the anchor's rect and repositions the panel. */
5
+ place: () => void;
6
+ /** Called only when the anchor is no longer on screen. */
7
+ close: () => void;
8
+ }
9
+ /**
10
+ * The scroll/resize handler an anchored popover should register, instead of
11
+ * `close`.
12
+ *
13
+ * Registering it for `scroll` still needs capture (`true`): the scroll may come
14
+ * from any ancestor container, and scroll events do not bubble to `window`.
15
+ */
16
+ export declare function createAnchorReflow(options: AnchorReflowOptions): () => void;
@@ -15,8 +15,8 @@ interface DateFieldProps {
15
15
  }
16
16
  /**
17
17
  * Compact hand-rolled month-grid date picker (no Kobalte primitive covers this yet).
18
- * Trigger button opens a portaled popover calendar; closes on outside click, Escape,
19
- * scroll, or resize. Speaks plain `YYYY-MM-DD` strings, never `Date`/`toISOString`.
18
+ * Trigger button opens a portaled popover calendar; closes on outside click or Escape,
19
+ * and follows the trigger when the page scrolls or resizes. Speaks plain `YYYY-MM-DD` strings, never `Date`/`toISOString`.
20
20
  *
21
21
  * @example
22
22
  * ```tsx
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a4ui/core",
3
- "version": "0.37.0",
3
+ "version": "0.37.1",
4
4
  "description": "A4ui — Spatial Glass design system & component library for SolidJS (Kobalte behavior + Tailwind glass tokens + motion).",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  // import '@a4ui/core/styles.css'
9
9
  // import { Button, Card, Modal } from '@a4ui/core'
10
10
 
11
- export const A4UI_VERSION = '0.37.0'
11
+ export const A4UI_VERSION = '0.37.1'
12
12
 
13
13
  // Helpers (src/lib) — generic, framework-level utilities.
14
14
  export { cn } from './lib/cn'
@@ -0,0 +1,57 @@
1
+ // Keeping a portaled popover glued to the trigger it belongs to.
2
+ //
3
+ // DateField, TimeField, DateRangePicker and MultiSelect all render their panel
4
+ // in a <Portal> so it escapes the stacking context of whatever card it sits in.
5
+ // At <body> it cannot be positioned relative to the trigger, so each one places
6
+ // it with `position: fixed` at the trigger's viewport rect — which is correct
7
+ // until the page scrolls and the rect goes stale.
8
+ //
9
+ // All four handled that by CLOSING on any captured scroll, and that made them
10
+ // unusable on a scrolling page: clicking a trigger below the fold scrolls it
11
+ // into view, the scroll event arrives right after the panel opens, and the
12
+ // panel shuts in the same gesture. Reported as a date picker that simply did
13
+ // not respond to clicks; measured in a browser as open-then-closed within
14
+ // 300 ms, with `mousedown` followed by `scroll:document`.
15
+ //
16
+ // A fixed panel does not need to disappear when the page moves — it needs to
17
+ // move with it. This re-places it, and closes only when the trigger has
18
+ // actually left the viewport, where following it would leave a panel hovering
19
+ // over unrelated content.
20
+
21
+ /** Vertical slack, in px, before a trigger counts as gone. Half a row: enough
22
+ * not to close while a sticky header slides over the trigger's top edge. */
23
+ const OFF_SCREEN_SLACK = 16
24
+
25
+ export interface AnchorReflowOptions {
26
+ /** The trigger the panel is anchored to. */
27
+ anchor: () => HTMLElement | undefined
28
+ /** Re-reads the anchor's rect and repositions the panel. */
29
+ place: () => void
30
+ /** Called only when the anchor is no longer on screen. */
31
+ close: () => void
32
+ }
33
+
34
+ /**
35
+ * The scroll/resize handler an anchored popover should register, instead of
36
+ * `close`.
37
+ *
38
+ * Registering it for `scroll` still needs capture (`true`): the scroll may come
39
+ * from any ancestor container, and scroll events do not bubble to `window`.
40
+ */
41
+ export function createAnchorReflow(options: AnchorReflowOptions): () => void {
42
+ return () => {
43
+ const el = options.anchor()
44
+ if (!el) {
45
+ options.close()
46
+ return
47
+ }
48
+ const rect = el.getBoundingClientRect()
49
+ const gone =
50
+ rect.bottom < -OFF_SCREEN_SLACK ||
51
+ rect.top > window.innerHeight + OFF_SCREEN_SLACK ||
52
+ rect.right < 0 ||
53
+ rect.left > window.innerWidth
54
+ if (gone) options.close()
55
+ else options.place()
56
+ }
57
+ }
@@ -9,6 +9,7 @@ import type { JSX } from 'solid-js'
9
9
  import { Show, createEffect, createMemo, createSignal, onCleanup } from 'solid-js'
10
10
  import { Portal } from 'solid-js/web'
11
11
 
12
+ import { createAnchorReflow } from '../lib/anchored'
12
13
  import { cn } from '../lib/cn'
13
14
  import { CalendarCore } from './internal/CalendarCore'
14
15
 
@@ -67,8 +68,8 @@ interface DateFieldProps {
67
68
 
68
69
  /**
69
70
  * Compact hand-rolled month-grid date picker (no Kobalte primitive covers this yet).
70
- * Trigger button opens a portaled popover calendar; closes on outside click, Escape,
71
- * scroll, or resize. Speaks plain `YYYY-MM-DD` strings, never `Date`/`toISOString`.
71
+ * Trigger button opens a portaled popover calendar; closes on outside click or Escape,
72
+ * and follows the trigger when the page scrolls or resizes. Speaks plain `YYYY-MM-DD` strings, never `Date`/`toISOString`.
72
73
  *
73
74
  * @example
74
75
  * ```tsx
@@ -115,17 +116,25 @@ export function DateField(props: DateFieldProps): JSX.Element {
115
116
 
116
117
  // Wire/tear-down the global listeners strictly around the open state. Scroll/
117
118
  // resize close the popover (its fixed position would otherwise drift).
119
+ // Follow the trigger rather than closing. The panel is `fixed` at the
120
+ // trigger's rect, so a scroll makes that rect stale — it does not make the
121
+ // panel unwanted. Closing on any captured scroll made this unusable on a
122
+ // scrolling page: clicking a trigger below the fold scrolls it into view,
123
+ // and that scroll arrived right after the panel opened and shut it in the
124
+ // same gesture. See `createAnchorReflow`.
125
+ const reflow = createAnchorReflow({ anchor: () => btnRef, place, close })
126
+
118
127
  const toggleListeners = (isOpen: boolean) => {
119
128
  if (isOpen) {
120
129
  document.addEventListener('mousedown', onDocPointer)
121
130
  document.addEventListener('keydown', onKey)
122
- window.addEventListener('scroll', close, true)
123
- window.addEventListener('resize', close)
131
+ window.addEventListener('scroll', reflow, true)
132
+ window.addEventListener('resize', reflow)
124
133
  } else {
125
134
  document.removeEventListener('mousedown', onDocPointer)
126
135
  document.removeEventListener('keydown', onKey)
127
- window.removeEventListener('scroll', close, true)
128
- window.removeEventListener('resize', close)
136
+ window.removeEventListener('scroll', reflow, true)
137
+ window.removeEventListener('resize', reflow)
129
138
  }
130
139
  }
131
140
  // Reactively attach when open flips; always clean up on unmount.
@@ -9,6 +9,7 @@ import type { JSX } from 'solid-js'
9
9
  import { createEffect, createMemo, createSignal, onCleanup, Show } from 'solid-js'
10
10
  import { Portal } from 'solid-js/web'
11
11
 
12
+ import { createAnchorReflow } from '../lib/anchored'
12
13
  import { cn } from '../lib/cn'
13
14
  import { CalendarCore } from './internal/CalendarCore'
14
15
 
@@ -116,17 +117,25 @@ export function DateRangePicker(props: DateRangePickerProps): JSX.Element {
116
117
 
117
118
  // Wire/tear-down the global listeners strictly around the open state. Scroll/
118
119
  // resize close the popover (its fixed position would otherwise drift).
120
+ // Follow the trigger rather than closing. The panel is `fixed` at the
121
+ // trigger's rect, so a scroll makes that rect stale — it does not make the
122
+ // panel unwanted. Closing on any captured scroll made this unusable on a
123
+ // scrolling page: clicking a trigger below the fold scrolls it into view,
124
+ // and that scroll arrived right after the panel opened and shut it in the
125
+ // same gesture. See `createAnchorReflow`.
126
+ const reflow = createAnchorReflow({ anchor: () => btnRef, place, close })
127
+
119
128
  const toggleListeners = (isOpen: boolean) => {
120
129
  if (isOpen) {
121
130
  document.addEventListener('mousedown', onDocPointer)
122
131
  document.addEventListener('keydown', onKey)
123
- window.addEventListener('scroll', close, true)
124
- window.addEventListener('resize', close)
132
+ window.addEventListener('scroll', reflow, true)
133
+ window.addEventListener('resize', reflow)
125
134
  } else {
126
135
  document.removeEventListener('mousedown', onDocPointer)
127
136
  document.removeEventListener('keydown', onKey)
128
- window.removeEventListener('scroll', close, true)
129
- window.removeEventListener('resize', close)
137
+ window.removeEventListener('scroll', reflow, true)
138
+ window.removeEventListener('resize', reflow)
130
139
  }
131
140
  }
132
141
  // Reactively attach when open flips; always clean up on unmount.
@@ -8,6 +8,7 @@ import type { JSX } from 'solid-js'
8
8
  import { For, Show, createEffect, createMemo, createSignal, onCleanup } from 'solid-js'
9
9
  import { Portal } from 'solid-js/web'
10
10
 
11
+ import { createAnchorReflow } from '../lib/anchored'
11
12
  import { cn } from '../lib/cn'
12
13
 
13
14
  /** One selectable option: a stable `value` and its display `label`. */
@@ -89,17 +90,25 @@ export function MultiSelect(props: MultiSelectProps): JSX.Element {
89
90
 
90
91
  // Wire/tear-down the global listeners strictly around the open state. Scroll/
91
92
  // resize close the popover (its fixed position would otherwise drift).
93
+ // Follow the trigger rather than closing. The panel is `fixed` at the
94
+ // trigger's rect, so a scroll makes that rect stale — it does not make the
95
+ // panel unwanted. Closing on any captured scroll made this unusable on a
96
+ // scrolling page: clicking a trigger below the fold scrolls it into view,
97
+ // and that scroll arrived right after the panel opened and shut it in the
98
+ // same gesture. See `createAnchorReflow`.
99
+ const reflow = createAnchorReflow({ anchor: () => btnRef, place, close })
100
+
92
101
  const toggleListeners = (isOpen: boolean) => {
93
102
  if (isOpen) {
94
103
  document.addEventListener('mousedown', onDocPointer)
95
104
  document.addEventListener('keydown', onKey)
96
- window.addEventListener('scroll', close, true)
97
- window.addEventListener('resize', close)
105
+ window.addEventListener('scroll', reflow, true)
106
+ window.addEventListener('resize', reflow)
98
107
  } else {
99
108
  document.removeEventListener('mousedown', onDocPointer)
100
109
  document.removeEventListener('keydown', onKey)
101
- window.removeEventListener('scroll', close, true)
102
- window.removeEventListener('resize', close)
110
+ window.removeEventListener('scroll', reflow, true)
111
+ window.removeEventListener('resize', reflow)
103
112
  }
104
113
  }
105
114
  // Reactively attach when open flips; always clean up on unmount.
@@ -8,6 +8,7 @@ import type { JSX } from 'solid-js'
8
8
  import { For, Show, createEffect, createMemo, createSignal, onCleanup } from 'solid-js'
9
9
  import { Portal } from 'solid-js/web'
10
10
 
11
+ import { createAnchorReflow } from '../lib/anchored'
11
12
  import { cn } from '../lib/cn'
12
13
 
13
14
  type AmPm = 'AM' | 'PM'
@@ -146,17 +147,25 @@ export function TimeField(props: TimeFieldProps): JSX.Element {
146
147
 
147
148
  // Wire/tear-down the global listeners strictly around the open state. Scroll/
148
149
  // resize close the popover (its fixed position would otherwise drift).
150
+ // Follow the trigger rather than closing. The panel is `fixed` at the
151
+ // trigger's rect, so a scroll makes that rect stale — it does not make the
152
+ // panel unwanted. Closing on any captured scroll made this unusable on a
153
+ // scrolling page: clicking a trigger below the fold scrolls it into view,
154
+ // and that scroll arrived right after the panel opened and shut it in the
155
+ // same gesture. See `createAnchorReflow`.
156
+ const reflow = createAnchorReflow({ anchor: () => btnRef, place, close })
157
+
149
158
  const toggleListeners = (isOpen: boolean) => {
150
159
  if (isOpen) {
151
160
  document.addEventListener('mousedown', onDocPointer)
152
161
  document.addEventListener('keydown', onKey)
153
- window.addEventListener('scroll', close, true)
154
- window.addEventListener('resize', close)
162
+ window.addEventListener('scroll', reflow, true)
163
+ window.addEventListener('resize', reflow)
155
164
  } else {
156
165
  document.removeEventListener('mousedown', onDocPointer)
157
166
  document.removeEventListener('keydown', onKey)
158
- window.removeEventListener('scroll', close, true)
159
- window.removeEventListener('resize', close)
167
+ window.removeEventListener('scroll', reflow, true)
168
+ window.removeEventListener('resize', reflow)
160
169
  }
161
170
  }
162
171
  // Reactively attach when open flips; always clean up on unmount.