@payglocal_ui/flux-ui 0.3.2 → 0.3.3
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.cjs +1384 -1171
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +159 -1
- package/dist/index.d.ts +159 -1
- package/dist/index.js +1427 -1218
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/picker-in-dialog.test.tsx +37 -0
- package/src/__tests__/select-search.test.tsx +223 -0
- package/src/checkbox-select.tsx +121 -101
- package/src/code.tsx +21 -1
- package/src/country-select.tsx +93 -88
- package/src/date-picker.tsx +6 -2
- package/src/index.ts +5 -0
- package/src/inline-dialog.tsx +43 -38
- package/src/option-filter.ts +47 -0
- package/src/popover.tsx +14 -9
- package/src/scroll-lock.tsx +75 -0
- package/src/single-select.tsx +237 -0
- package/src/time-picker.tsx +6 -1
package/src/country-select.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
5
5
|
import { Check, ChevronDown, Search } from "lucide-react";
|
|
6
6
|
import { cn } from "./utils";
|
|
7
|
+
import { ScrollLockTakeover } from "./scroll-lock";
|
|
7
8
|
|
|
8
9
|
export interface Country {
|
|
9
10
|
code: string;
|
|
@@ -142,96 +143,100 @@ const CountrySelect = React.forwardRef<HTMLButtonElement, CountrySelectProps>(
|
|
|
142
143
|
</PopoverPrimitive.Trigger>
|
|
143
144
|
|
|
144
145
|
<PopoverPrimitive.Portal>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
"
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
{/* Search */}
|
|
155
|
-
<div className="flex items-center gap-2 border-b border-border px-3 py-2.5">
|
|
156
|
-
<Search className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true" />
|
|
157
|
-
<input
|
|
158
|
-
ref={searchRef}
|
|
159
|
-
type="text"
|
|
160
|
-
value={search}
|
|
161
|
-
onChange={(e) => setSearch(e.target.value)}
|
|
162
|
-
placeholder="Search countries…"
|
|
163
|
-
className={cn(
|
|
164
|
-
"flex-1 bg-transparent text-[14px] text-foreground placeholder:text-muted-foreground",
|
|
165
|
-
"focus-visible:outline-none"
|
|
166
|
-
)}
|
|
167
|
-
aria-label="Search countries"
|
|
168
|
-
/>
|
|
169
|
-
{search && (
|
|
170
|
-
<button
|
|
171
|
-
type="button"
|
|
172
|
-
onClick={() => setSearch("")}
|
|
173
|
-
className="shrink-0 text-muted-foreground hover:text-foreground transition-colors duration-pg-fast ease-pg-standard"
|
|
174
|
-
aria-label="Clear search"
|
|
175
|
-
>
|
|
176
|
-
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
177
|
-
<path d="M10.5 3.5L3.5 10.5M3.5 3.5L10.5 10.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
178
|
-
</svg>
|
|
179
|
-
</button>
|
|
146
|
+
{/* Takes over the scroll lock when this opens inside a Dialog or
|
|
147
|
+
Drawer, so the list can be scrolled. See `scroll-lock.tsx`. */}
|
|
148
|
+
<ScrollLockTakeover>
|
|
149
|
+
<PopoverPrimitive.Content
|
|
150
|
+
align="start"
|
|
151
|
+
sideOffset={6}
|
|
152
|
+
className={cn(
|
|
153
|
+
"z-[120] w-[var(--radix-popover-trigger-width)] min-w-[260px] rounded-xl border border-border bg-popover text-popover-foreground shadow-lg outline-none",
|
|
154
|
+
"data-[state=open]:opacity-100 data-[state=closed]:opacity-0 transition-opacity duration-150"
|
|
180
155
|
)}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
{/* List */}
|
|
184
|
-
<div
|
|
185
|
-
role="listbox"
|
|
186
|
-
aria-label="Countries"
|
|
187
|
-
className="max-h-64 overflow-y-auto p-1"
|
|
156
|
+
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
188
157
|
>
|
|
189
|
-
{
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
>
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
158
|
+
{/* Search */}
|
|
159
|
+
<div className="flex items-center gap-2 border-b border-border px-3 py-2.5">
|
|
160
|
+
<Search className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true" />
|
|
161
|
+
<input
|
|
162
|
+
ref={searchRef}
|
|
163
|
+
type="text"
|
|
164
|
+
value={search}
|
|
165
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
166
|
+
placeholder="Search countries…"
|
|
167
|
+
className={cn(
|
|
168
|
+
"flex-1 bg-transparent text-[14px] text-foreground placeholder:text-muted-foreground",
|
|
169
|
+
"focus-visible:outline-none"
|
|
170
|
+
)}
|
|
171
|
+
aria-label="Search countries"
|
|
172
|
+
/>
|
|
173
|
+
{search && (
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
176
|
+
onClick={() => setSearch("")}
|
|
177
|
+
className="shrink-0 text-muted-foreground hover:text-foreground transition-colors duration-pg-fast ease-pg-standard"
|
|
178
|
+
aria-label="Clear search"
|
|
179
|
+
>
|
|
180
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
181
|
+
<path d="M10.5 3.5L3.5 10.5M3.5 3.5L10.5 10.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
182
|
+
</svg>
|
|
183
|
+
</button>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{/* List */}
|
|
188
|
+
<div
|
|
189
|
+
role="listbox"
|
|
190
|
+
aria-label="Countries"
|
|
191
|
+
className="max-h-64 overflow-y-auto p-1"
|
|
192
|
+
>
|
|
193
|
+
{filtered.length === 0 ? (
|
|
194
|
+
<div className="py-6 text-center text-sm text-muted-foreground">
|
|
195
|
+
No countries found
|
|
196
|
+
</div>
|
|
197
|
+
) : (
|
|
198
|
+
filtered.map((country) => {
|
|
199
|
+
const isSelected = country.code === value;
|
|
200
|
+
return (
|
|
201
|
+
<button
|
|
202
|
+
key={country.code}
|
|
203
|
+
role="option"
|
|
204
|
+
aria-selected={isSelected}
|
|
205
|
+
type="button"
|
|
206
|
+
onClick={() => {
|
|
207
|
+
onValueChange?.(country.code);
|
|
208
|
+
setOpen(false);
|
|
209
|
+
}}
|
|
210
|
+
className={cn(
|
|
211
|
+
"flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm",
|
|
212
|
+
"transition-colors duration-pg-fast ease-pg-standard",
|
|
213
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
|
|
214
|
+
"hover:bg-muted",
|
|
215
|
+
isSelected && "bg-muted/60"
|
|
216
|
+
)}
|
|
217
|
+
>
|
|
218
|
+
<span className="text-base leading-none" aria-hidden="true">
|
|
219
|
+
{country.flag}
|
|
220
|
+
</span>
|
|
221
|
+
<span className="flex-1 truncate text-foreground">
|
|
222
|
+
{country.name}
|
|
223
|
+
</span>
|
|
224
|
+
<span className="shrink-0 text-xs text-muted-foreground">
|
|
225
|
+
{country.dialCode}
|
|
226
|
+
</span>
|
|
227
|
+
{isSelected && (
|
|
228
|
+
<Check
|
|
229
|
+
className="h-3.5 w-3.5 shrink-0 text-primary"
|
|
230
|
+
aria-hidden="true"
|
|
231
|
+
/>
|
|
232
|
+
)}
|
|
233
|
+
</button>
|
|
234
|
+
);
|
|
235
|
+
})
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
</PopoverPrimitive.Content>
|
|
239
|
+
</ScrollLockTakeover>
|
|
235
240
|
</PopoverPrimitive.Portal>
|
|
236
241
|
</PopoverPrimitive.Root>
|
|
237
242
|
);
|
package/src/date-picker.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { RemoveScroll } from "react-remove-scroll";
|
|
|
6
6
|
import { ChevronLeft, ChevronRight, ChevronDown, CalendarDays } from "lucide-react";
|
|
7
7
|
import { AnimatePresence, motion } from "framer-motion";
|
|
8
8
|
import { cn } from "./utils";
|
|
9
|
+
import { ScrollLockTakeover } from "./scroll-lock";
|
|
9
10
|
|
|
10
11
|
/* ─── Constants ─────────────────────────────────────────────────────────── */
|
|
11
12
|
const MONTHS = ["January","February","March","April","May","June",
|
|
@@ -655,8 +656,11 @@ export function DatePicker({ value, onChange, placeholder = "Select date", class
|
|
|
655
656
|
<ChevronDown className={cn("size-[1.125rem] shrink-0 text-muted-foreground transition-transform", open && "rotate-180")} />
|
|
656
657
|
</button>
|
|
657
658
|
|
|
658
|
-
{/* Portal
|
|
659
|
-
|
|
659
|
+
{/* Portal. Wrapped so the panel takes over the scroll lock when it opens
|
|
660
|
+
inside a Dialog or Drawer — without it the year/month lists and the
|
|
661
|
+
time columns cannot be scrolled with the wheel. See `scroll-lock.tsx`. */}
|
|
662
|
+
{mounted &&
|
|
663
|
+
createPortal(<ScrollLockTakeover>{panel}</ScrollLockTakeover>, document.body)}
|
|
660
664
|
</div>
|
|
661
665
|
);
|
|
662
666
|
}
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,11 @@ export { OtpInput } from "./otp-input";
|
|
|
20
20
|
export type { OtpInputProps } from "./otp-input";
|
|
21
21
|
export { CheckboxSelect } from "./checkbox-select";
|
|
22
22
|
export type { CheckboxSelectProps, CheckboxSelectOption } from "./checkbox-select";
|
|
23
|
+
export { SingleSelect } from "./single-select";
|
|
24
|
+
export type { SingleSelectProps, SingleSelectOption } from "./single-select";
|
|
25
|
+
export { defaultOptionFilter } from "./option-filter";
|
|
26
|
+
export { isScrollLocked, ScrollLockTakeover } from "./scroll-lock";
|
|
27
|
+
export type { OptionFilter, FilterableOption } from "./option-filter";
|
|
23
28
|
export { Textarea } from "./textarea";
|
|
24
29
|
export { Label } from "./label";
|
|
25
30
|
export { Checkbox } from "./checkbox";
|
package/src/inline-dialog.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
5
5
|
import { X } from "lucide-react";
|
|
6
6
|
import { cn } from "./utils";
|
|
7
|
+
import { ScrollLockTakeover } from "./scroll-lock";
|
|
7
8
|
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
9
10
|
// Types
|
|
@@ -103,44 +104,48 @@ const InlineDialogContent = React.forwardRef<
|
|
|
103
104
|
ref
|
|
104
105
|
) => (
|
|
105
106
|
<PopoverPrimitive.Portal>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
107
|
+
{/* Scrollable when opened from inside a Dialog or Drawer. See
|
|
108
|
+
`scroll-lock.tsx`. */}
|
|
109
|
+
<ScrollLockTakeover>
|
|
110
|
+
<PopoverPrimitive.Content
|
|
111
|
+
ref={ref}
|
|
112
|
+
side={side}
|
|
113
|
+
sideOffset={sideOffset}
|
|
114
|
+
className={cn(
|
|
115
|
+
// Base card
|
|
116
|
+
"relative z-[120] max-w-xs overflow-visible",
|
|
117
|
+
"rounded-xl border border-border bg-card p-4 text-foreground shadow-lg",
|
|
118
|
+
// Animation
|
|
119
|
+
"data-[state=open]:opacity-100 data-[state=closed]:opacity-0",
|
|
120
|
+
"transition-opacity duration-pg-fast ease-pg-standard",
|
|
121
|
+
"outline-none",
|
|
122
|
+
className
|
|
123
|
+
)}
|
|
124
|
+
{...props}
|
|
125
|
+
>
|
|
126
|
+
{/* Arrow pointer */}
|
|
127
|
+
<span className={getArrowClasses(side)} aria-hidden="true" />
|
|
128
|
+
|
|
129
|
+
{/* Close button */}
|
|
130
|
+
{!hideClose && (
|
|
131
|
+
<PopoverPrimitive.Close
|
|
132
|
+
className={cn(
|
|
133
|
+
"absolute right-2 top-2 z-10",
|
|
134
|
+
"inline-flex size-6 items-center justify-center rounded-md",
|
|
135
|
+
"text-muted-foreground hover:text-foreground",
|
|
136
|
+
"transition-colors duration-pg-fast ease-pg-standard",
|
|
137
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
|
|
138
|
+
"disabled:cursor-not-allowed disabled:opacity-50"
|
|
139
|
+
)}
|
|
140
|
+
aria-label="Close"
|
|
141
|
+
>
|
|
142
|
+
<X className="size-3.5" />
|
|
143
|
+
</PopoverPrimitive.Close>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{children}
|
|
147
|
+
</PopoverPrimitive.Content>
|
|
148
|
+
</ScrollLockTakeover>
|
|
144
149
|
</PopoverPrimitive.Portal>
|
|
145
150
|
)
|
|
146
151
|
);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared option-search behaviour for the select family.
|
|
3
|
+
*
|
|
4
|
+
* Every picker that can be searched matches the same way, so a user who learns
|
|
5
|
+
* that typing a raw code works in one dropdown can rely on it in the next.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface FilterableOption {
|
|
9
|
+
value: string;
|
|
10
|
+
label: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A custom match. Return true to keep the option in the list.
|
|
15
|
+
*
|
|
16
|
+
* The `query` arrives trimmed but otherwise untouched, so a predicate that
|
|
17
|
+
* cares about case can have it.
|
|
18
|
+
*/
|
|
19
|
+
export type OptionFilter<T extends FilterableOption = FilterableOption> = (
|
|
20
|
+
option: T,
|
|
21
|
+
query: string
|
|
22
|
+
) => boolean;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The default: case-insensitive against the label **and** the value, so "nz"
|
|
26
|
+
* finds New Zealand and a raw status code finds its prettified row. Someone who
|
|
27
|
+
* thinks in codes should not have to know the display name.
|
|
28
|
+
*
|
|
29
|
+
* This is the rule `SelectFilterChip` already applied to its list; the form
|
|
30
|
+
* fields now share it rather than each picker inventing its own.
|
|
31
|
+
*/
|
|
32
|
+
export function defaultOptionFilter<T extends FilterableOption>(option: T, query: string): boolean {
|
|
33
|
+
const q = query.toLowerCase();
|
|
34
|
+
return option.label.toLowerCase().includes(q) || option.value.toLowerCase().includes(q);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Applies `filter` (or the default) to `options`, with an empty query passing everything. */
|
|
38
|
+
export function filterOptions<T extends FilterableOption>(
|
|
39
|
+
options: T[],
|
|
40
|
+
query: string,
|
|
41
|
+
filter?: OptionFilter<T>
|
|
42
|
+
): T[] {
|
|
43
|
+
const q = query.trim();
|
|
44
|
+
if (!q) return options;
|
|
45
|
+
const match = filter ?? defaultOptionFilter;
|
|
46
|
+
return options.filter((o) => match(o, q));
|
|
47
|
+
}
|
package/src/popover.tsx
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
5
5
|
import { cn } from "./utils";
|
|
6
|
+
import { ScrollLockTakeover } from "./scroll-lock";
|
|
6
7
|
|
|
7
8
|
const Popover = PopoverPrimitive.Root;
|
|
8
9
|
const PopoverTrigger = PopoverPrimitive.Trigger;
|
|
@@ -11,12 +12,16 @@ const PopoverAnchor = PopoverPrimitive.Anchor;
|
|
|
11
12
|
const PopoverContent = React.forwardRef<
|
|
12
13
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
|
13
14
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
|
14
|
-
>(
|
|
15
|
-
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
>(({ className, align = "center", sideOffset = 6, collisionPadding = 12, ...props }, ref) => (
|
|
16
|
+
<PopoverPrimitive.Portal>
|
|
17
|
+
{/* Every popover panel is portalled to `document.body`, which puts it
|
|
18
|
+
outside a Dialog's or Drawer's scroll lock — and that lock cancels the
|
|
19
|
+
wheel for everything outside itself, so a scrollable panel silently
|
|
20
|
+
refuses to scroll. The takeover is a no-op when no lock is held, so a
|
|
21
|
+
popover on a plain page behaves exactly as before, and unlike making the
|
|
22
|
+
popover `modal` it changes nothing about focus or outside clicks. See
|
|
23
|
+
`scroll-lock.tsx`. */}
|
|
24
|
+
<ScrollLockTakeover>
|
|
20
25
|
<PopoverPrimitive.Content
|
|
21
26
|
ref={ref}
|
|
22
27
|
align={align}
|
|
@@ -32,9 +37,9 @@ const PopoverContent = React.forwardRef<
|
|
|
32
37
|
)}
|
|
33
38
|
{...props}
|
|
34
39
|
/>
|
|
35
|
-
</
|
|
36
|
-
|
|
37
|
-
);
|
|
40
|
+
</ScrollLockTakeover>
|
|
41
|
+
</PopoverPrimitive.Portal>
|
|
42
|
+
));
|
|
38
43
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
|
39
44
|
|
|
40
45
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { RemoveScroll } from "react-remove-scroll";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Scrolling inside an overlay that sits on top of another overlay.
|
|
8
|
+
*
|
|
9
|
+
* ## The defect this exists to prevent
|
|
10
|
+
*
|
|
11
|
+
* A modal `Dialog` or `Drawer` installs a `react-remove-scroll` lock. That lock
|
|
12
|
+
* listens for `wheel` and `touchmove` on `document` and, for any event whose
|
|
13
|
+
* target is **outside** the locked subtree, calls `preventDefault()` — the
|
|
14
|
+
* "outside or shard event" branch of its `SideEffect`. Radix passes only the
|
|
15
|
+
* dialog's own content as a shard.
|
|
16
|
+
*
|
|
17
|
+
* Every panel a picker opens is portalled to `document.body`, which puts it
|
|
18
|
+
* outside that subtree. So the panel renders, its list has `overflow-y-auto`,
|
|
19
|
+
* and the wheel does nothing. It is invisible in code review, because the
|
|
20
|
+
* component is correct on its own; it only misbehaves under an overlay.
|
|
21
|
+
*
|
|
22
|
+
* ## Why a lock, not an exception
|
|
23
|
+
*
|
|
24
|
+
* The same `SideEffect` returns early unless its own lock is the last one
|
|
25
|
+
* pushed. So the fix is not to poke a hole in the dialog's lock — it is for the
|
|
26
|
+
* panel to push a lock of its own while it is open, which suspends the one
|
|
27
|
+
* underneath and makes the panel's subtree the locked one. When it closes, its
|
|
28
|
+
* lock pops and the dialog's resumes.
|
|
29
|
+
*
|
|
30
|
+
* ## Why not just make the popover `modal`
|
|
31
|
+
*
|
|
32
|
+
* Radix installs a lock of its own for a `modal` popover, which would also fix
|
|
33
|
+
* the wheel — but it comes with a focus trap and with outside clicks being
|
|
34
|
+
* swallowed by the dismiss layer. Every popover already living inside a dialog
|
|
35
|
+
* would start behaving differently for the sake of a scrolling fix. So the lock
|
|
36
|
+
* is pushed directly, by {@link ScrollLockTakeover}, and modality is left
|
|
37
|
+
* alone. `PopoverContent` wraps every panel in it, which covers the pickers and
|
|
38
|
+
* anything an app composes; the hand-rolled `createPortal` panels
|
|
39
|
+
* (`DatePicker`, `TimePicker`) wrap theirs the same way.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether a `react-remove-scroll` lock is currently held — by a modal Dialog,
|
|
44
|
+
* Drawer, AlertDialog, or another picker.
|
|
45
|
+
*
|
|
46
|
+
* `data-scroll-locked` is set on `<body>` by `react-remove-scroll-bar`, which
|
|
47
|
+
* every such lock goes through, so this is the library's own signal rather
|
|
48
|
+
* than a guess about which overlay is open.
|
|
49
|
+
*/
|
|
50
|
+
export function isScrollLocked(): boolean {
|
|
51
|
+
return typeof document !== "undefined" && document.body.hasAttribute("data-scroll-locked");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The same takeover for a panel that portals itself instead of going through
|
|
56
|
+
* Radix. Renders children untouched when no lock is held, so a picker on a
|
|
57
|
+
* plain page behaves exactly as before.
|
|
58
|
+
*
|
|
59
|
+
* `removeScrollBar` is off: the page's scrollbar is already gone, and a second
|
|
60
|
+
* `RemoveScrollBar` re-measures a gap that is now zero and writes it back over
|
|
61
|
+
* the dialog's, shifting the layout while the panel is open. The consequence is
|
|
62
|
+
* that this takeover does **not** bump `data-scroll-locked`; the wrapper's
|
|
63
|
+
* `data-scroll-lock-takeover` is the marker instead, in the DOM for debugging
|
|
64
|
+
* and for tests.
|
|
65
|
+
*/
|
|
66
|
+
export function ScrollLockTakeover({ children }: { children: React.ReactNode }) {
|
|
67
|
+
// Read once, when the panel mounts — that is the moment it opens.
|
|
68
|
+
const [active] = React.useState(isScrollLocked);
|
|
69
|
+
if (!active) return <>{children}</>;
|
|
70
|
+
return (
|
|
71
|
+
<RemoveScroll removeScrollBar={false} allowPinchZoom data-scroll-lock-takeover="">
|
|
72
|
+
{children}
|
|
73
|
+
</RemoveScroll>
|
|
74
|
+
);
|
|
75
|
+
}
|