@a4ui/core 0.36.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.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3418 -3383
- package/dist/lib/anchored.d.ts +16 -0
- package/dist/ui/DateField.d.ts +2 -2
- package/dist/ui/LocationPicker.d.ts +20 -0
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/lib/anchored.ts +57 -0
- package/src/ui/DateField.tsx +15 -6
- package/src/ui/DateRangePicker.tsx +13 -4
- package/src/ui/LocationPicker.tsx +31 -3
- package/src/ui/MultiSelect.tsx +13 -4
- package/src/ui/TimeField.tsx +13 -4
|
@@ -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;
|
package/dist/ui/DateField.d.ts
CHANGED
|
@@ -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
|
|
19
|
-
*
|
|
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
|
|
@@ -18,6 +18,26 @@ export interface LocationPickerProps {
|
|
|
18
18
|
labelPlaceholder?: string;
|
|
19
19
|
/** Accessible name for the "use my location" button. @default 'Use my location' */
|
|
20
20
|
geolocateLabel?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Map zoom. Also acts as a zoom trigger the same way `center` does on
|
|
23
|
+
* `InteractiveMap` — passing a new value zooms there.
|
|
24
|
+
*
|
|
25
|
+
* Exposed because 12 (a whole city) is the wrong scale for confirming a
|
|
26
|
+
* pin: a consumer that wants to zoom in after a successful geolocation had
|
|
27
|
+
* no way to do it. @default 12
|
|
28
|
+
*/
|
|
29
|
+
zoom?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Fires ONLY when the "use my location" button succeeds, in addition to
|
|
32
|
+
* `onChange`.
|
|
33
|
+
*
|
|
34
|
+
* `onChange` alone cannot tell a map click from a geolocation, so a
|
|
35
|
+
* consumer had no way to react to one and not the other. Recentring on a
|
|
36
|
+
* user already looking at their own city moves the map barely at all and
|
|
37
|
+
* leaves the pin dead centre, which reads as nothing having happened —
|
|
38
|
+
* this is the hook for saying otherwise.
|
|
39
|
+
*/
|
|
40
|
+
onGeolocate?: (v: LocationValue) => void;
|
|
21
41
|
class?: string;
|
|
22
42
|
}
|
|
23
43
|
/**
|
package/package.json
CHANGED
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.
|
|
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
|
+
}
|
package/src/ui/DateField.tsx
CHANGED
|
@@ -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
|
|
71
|
-
*
|
|
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',
|
|
123
|
-
window.addEventListener('resize',
|
|
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',
|
|
128
|
-
window.removeEventListener('resize',
|
|
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',
|
|
124
|
-
window.addEventListener('resize',
|
|
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',
|
|
129
|
-
window.removeEventListener('resize',
|
|
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.
|
|
@@ -29,10 +29,30 @@ export interface LocationPickerProps {
|
|
|
29
29
|
labelPlaceholder?: string
|
|
30
30
|
/** Accessible name for the "use my location" button. @default 'Use my location' */
|
|
31
31
|
geolocateLabel?: string
|
|
32
|
+
/**
|
|
33
|
+
* Map zoom. Also acts as a zoom trigger the same way `center` does on
|
|
34
|
+
* `InteractiveMap` — passing a new value zooms there.
|
|
35
|
+
*
|
|
36
|
+
* Exposed because 12 (a whole city) is the wrong scale for confirming a
|
|
37
|
+
* pin: a consumer that wants to zoom in after a successful geolocation had
|
|
38
|
+
* no way to do it. @default 12
|
|
39
|
+
*/
|
|
40
|
+
zoom?: number
|
|
41
|
+
/**
|
|
42
|
+
* Fires ONLY when the "use my location" button succeeds, in addition to
|
|
43
|
+
* `onChange`.
|
|
44
|
+
*
|
|
45
|
+
* `onChange` alone cannot tell a map click from a geolocation, so a
|
|
46
|
+
* consumer had no way to react to one and not the other. Recentring on a
|
|
47
|
+
* user already looking at their own city moves the map barely at all and
|
|
48
|
+
* leaves the pin dead centre, which reads as nothing having happened —
|
|
49
|
+
* this is the hook for saying otherwise.
|
|
50
|
+
*/
|
|
51
|
+
onGeolocate?: (v: LocationValue) => void
|
|
32
52
|
class?: string
|
|
33
53
|
}
|
|
34
54
|
|
|
35
|
-
const
|
|
55
|
+
const DEFAULT_MAP_ZOOM = 12
|
|
36
56
|
const MAP_HEIGHT = 240
|
|
37
57
|
|
|
38
58
|
/**
|
|
@@ -69,7 +89,15 @@ export function LocationPicker(props: LocationPickerProps): JSX.Element {
|
|
|
69
89
|
const useMyLocation = (): void => {
|
|
70
90
|
if (typeof navigator === 'undefined' || !navigator.geolocation) return
|
|
71
91
|
navigator.geolocation.getCurrentPosition(
|
|
72
|
-
(position) =>
|
|
92
|
+
(position) => {
|
|
93
|
+
const next: LocationValue = {
|
|
94
|
+
...current(),
|
|
95
|
+
lat: position.coords.latitude,
|
|
96
|
+
lng: position.coords.longitude,
|
|
97
|
+
}
|
|
98
|
+
emit(next)
|
|
99
|
+
props.onGeolocate?.(next)
|
|
100
|
+
},
|
|
73
101
|
(error) => console.error('LocationPicker: geolocation failed', error),
|
|
74
102
|
)
|
|
75
103
|
}
|
|
@@ -99,7 +127,7 @@ export function LocationPicker(props: LocationPickerProps): JSX.Element {
|
|
|
99
127
|
</div>
|
|
100
128
|
<InteractiveMap
|
|
101
129
|
center={{ lat: current().lat, lng: current().lng }}
|
|
102
|
-
zoom={
|
|
130
|
+
zoom={props.zoom ?? DEFAULT_MAP_ZOOM}
|
|
103
131
|
height={MAP_HEIGHT}
|
|
104
132
|
markers={pin()}
|
|
105
133
|
onClick={setCoord}
|
package/src/ui/MultiSelect.tsx
CHANGED
|
@@ -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',
|
|
97
|
-
window.addEventListener('resize',
|
|
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',
|
|
102
|
-
window.removeEventListener('resize',
|
|
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.
|
package/src/ui/TimeField.tsx
CHANGED
|
@@ -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',
|
|
154
|
-
window.addEventListener('resize',
|
|
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',
|
|
159
|
-
window.removeEventListener('resize',
|
|
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.
|