panelui-native 0.68.0 → 0.71.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.
- package/README.md +12 -2
- package/lib/module/components/accordion/index.js +28 -9
- package/lib/module/components/accordion/index.js.map +1 -1
- package/lib/module/components/button/index.js +14 -7
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/collapsible/index.js +280 -0
- package/lib/module/components/collapsible/index.js.map +1 -0
- package/lib/module/components/flow/index.js +66 -3
- package/lib/module/components/flow/index.js.map +1 -1
- package/lib/module/components/live-line-chart/index.js +103 -9
- package/lib/module/components/live-line-chart/index.js.map +1 -1
- package/lib/module/components/live-line-chart/live-line-accessibility.js +47 -0
- package/lib/module/components/live-line-chart/live-line-accessibility.js.map +1 -0
- package/lib/module/components/live-line-chart/live-line-lifecycle.js +33 -0
- package/lib/module/components/live-line-chart/live-line-lifecycle.js.map +1 -0
- package/lib/module/components/marquee/index.js +194 -0
- package/lib/module/components/marquee/index.js.map +1 -0
- package/lib/module/components/meter/index.js +38 -20
- package/lib/module/components/meter/index.js.map +1 -1
- package/lib/module/components/meter/meter-scale.js +62 -8
- package/lib/module/components/meter/meter-scale.js.map +1 -1
- package/lib/module/components/planner/index.js +133 -59
- package/lib/module/components/planner/index.js.map +1 -1
- package/lib/module/components/planner/planner-announcement.js +30 -0
- package/lib/module/components/planner/planner-announcement.js.map +1 -0
- package/lib/module/components/planner/planner-entries.js +15 -7
- package/lib/module/components/planner/planner-entries.js.map +1 -1
- package/lib/module/components/planner/planner-grid-navigation.js +33 -0
- package/lib/module/components/planner/planner-grid-navigation.js.map +1 -0
- package/lib/module/components/planner/planner-lifecycle.js +45 -0
- package/lib/module/components/planner/planner-lifecycle.js.map +1 -0
- package/lib/module/components/popover/index.js +2 -2
- package/lib/module/components/popover/index.js.map +1 -1
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/primitives/focus-restoration-store.js +52 -0
- package/lib/module/primitives/focus-restoration-store.js.map +1 -0
- package/lib/module/primitives/portal.js +36 -2
- package/lib/module/primitives/portal.js.map +1 -1
- package/lib/typescript/src/components/accordion/index.d.ts +4 -0
- package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/collapsible/index.d.ts +76 -0
- package/lib/typescript/src/components/collapsible/index.d.ts.map +1 -0
- package/lib/typescript/src/components/flow/index.d.ts +7 -1
- package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
- package/lib/typescript/src/components/live-line-chart/index.d.ts +21 -7
- package/lib/typescript/src/components/live-line-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/live-line-chart/live-line-accessibility.d.ts +22 -0
- package/lib/typescript/src/components/live-line-chart/live-line-accessibility.d.ts.map +1 -0
- package/lib/typescript/src/components/live-line-chart/live-line-lifecycle.d.ts +23 -0
- package/lib/typescript/src/components/live-line-chart/live-line-lifecycle.d.ts.map +1 -0
- package/lib/typescript/src/components/marquee/index.d.ts +73 -0
- package/lib/typescript/src/components/marquee/index.d.ts.map +1 -0
- package/lib/typescript/src/components/meter/index.d.ts +7 -4
- package/lib/typescript/src/components/meter/index.d.ts.map +1 -1
- package/lib/typescript/src/components/meter/meter-scale.d.ts +39 -0
- package/lib/typescript/src/components/meter/meter-scale.d.ts.map +1 -1
- package/lib/typescript/src/components/planner/index.d.ts.map +1 -1
- package/lib/typescript/src/components/planner/planner-announcement.d.ts +15 -0
- package/lib/typescript/src/components/planner/planner-announcement.d.ts.map +1 -0
- package/lib/typescript/src/components/planner/planner-entries.d.ts +1 -1
- package/lib/typescript/src/components/planner/planner-entries.d.ts.map +1 -1
- package/lib/typescript/src/components/planner/planner-grid-navigation.d.ts +4 -0
- package/lib/typescript/src/components/planner/planner-grid-navigation.d.ts.map +1 -0
- package/lib/typescript/src/components/planner/planner-lifecycle.d.ts +20 -0
- package/lib/typescript/src/components/planner/planner-lifecycle.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/focus-restoration-store.d.ts +24 -0
- package/lib/typescript/src/primitives/focus-restoration-store.d.ts.map +1 -0
- package/lib/typescript/src/primitives/portal.d.ts +4 -0
- package/lib/typescript/src/primitives/portal.d.ts.map +1 -1
- package/package.json +36 -3
- package/src/components/accordion/index.tsx +31 -8
- package/src/components/button/index.tsx +11 -4
- package/src/components/collapsible/index.tsx +278 -0
- package/src/components/flow/index.tsx +93 -14
- package/src/components/live-line-chart/index.tsx +130 -13
- package/src/components/live-line-chart/live-line-accessibility.ts +61 -0
- package/src/components/live-line-chart/live-line-lifecycle.ts +63 -0
- package/src/components/marquee/index.tsx +232 -0
- package/src/components/meter/index.tsx +49 -26
- package/src/components/meter/meter-scale.ts +84 -8
- package/src/components/planner/index.tsx +182 -71
- package/src/components/planner/planner-announcement.ts +37 -0
- package/src/components/planner/planner-entries.ts +16 -7
- package/src/components/planner/planner-grid-navigation.ts +45 -0
- package/src/components/planner/planner-lifecycle.ts +76 -0
- package/src/components/popover/index.tsx +3 -3
- package/src/index.ts +13 -0
- package/src/primitives/focus-restoration-store.ts +70 -0
- package/src/primitives/portal.tsx +34 -2
|
@@ -99,9 +99,13 @@ export function dayAccessibilityLabel<T extends PlannerDatedEntry>(
|
|
|
99
99
|
if (entries.length === 0) return `${dateLabel}, nothing planned`;
|
|
100
100
|
|
|
101
101
|
const named: string[] = [];
|
|
102
|
+
const seen = new Set<string>();
|
|
102
103
|
for (const entry of entries) {
|
|
103
104
|
const label = entry.category ? categoryLabels.get(entry.category) : undefined;
|
|
104
|
-
if (label && !
|
|
105
|
+
if (label && !seen.has(label)) {
|
|
106
|
+
seen.add(label);
|
|
107
|
+
named.push(label);
|
|
108
|
+
}
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
const count = `${entries.length} ${entries.length === 1 ? 'entry' : 'entries'}`;
|
|
@@ -120,18 +124,23 @@ export function dayAccessibilityLabel<T extends PlannerDatedEntry>(
|
|
|
120
124
|
* with the week the month happens to start on.
|
|
121
125
|
*/
|
|
122
126
|
export function summariseMonth<T extends PlannerDatedEntry>(
|
|
123
|
-
|
|
127
|
+
days: Map<number, T[]>,
|
|
128
|
+
grid: readonly (readonly Date[])[],
|
|
124
129
|
categories: readonly { id: string; label: string }[],
|
|
125
130
|
isInMonth: (date: Date) => boolean
|
|
126
131
|
): { total: number; categories: PlannerCountedCategory[] } {
|
|
127
132
|
const counts = new Map<string, number>();
|
|
128
133
|
let total = 0;
|
|
129
134
|
|
|
130
|
-
for (const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
for (const week of grid) {
|
|
136
|
+
for (const date of week) {
|
|
137
|
+
if (!isInMonth(date)) continue;
|
|
138
|
+
for (const entry of entriesOn(days, date)) {
|
|
139
|
+
total += 1;
|
|
140
|
+
if (entry.category) {
|
|
141
|
+
counts.set(entry.category, (counts.get(entry.category) ?? 0) + 1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
135
144
|
}
|
|
136
145
|
}
|
|
137
146
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type PlannerGridNavigationKey =
|
|
2
|
+
| 'ArrowLeft'
|
|
3
|
+
| 'ArrowRight'
|
|
4
|
+
| 'ArrowUp'
|
|
5
|
+
| 'ArrowDown'
|
|
6
|
+
| 'Home'
|
|
7
|
+
| 'End';
|
|
8
|
+
|
|
9
|
+
/** Returns the next visible cell for a bounded, row-major month grid. */
|
|
10
|
+
export function plannerGridTarget(
|
|
11
|
+
key: string,
|
|
12
|
+
index: number,
|
|
13
|
+
count: number,
|
|
14
|
+
columns = 7
|
|
15
|
+
): number | null {
|
|
16
|
+
if (!Number.isInteger(index) || index < 0 || index >= count || columns < 1) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let target: number;
|
|
21
|
+
switch (key as PlannerGridNavigationKey) {
|
|
22
|
+
case 'ArrowLeft':
|
|
23
|
+
target = index - 1;
|
|
24
|
+
break;
|
|
25
|
+
case 'ArrowRight':
|
|
26
|
+
target = index + 1;
|
|
27
|
+
break;
|
|
28
|
+
case 'ArrowUp':
|
|
29
|
+
target = index - columns;
|
|
30
|
+
break;
|
|
31
|
+
case 'ArrowDown':
|
|
32
|
+
target = index + columns;
|
|
33
|
+
break;
|
|
34
|
+
case 'Home':
|
|
35
|
+
target = index - (index % columns);
|
|
36
|
+
break;
|
|
37
|
+
case 'End':
|
|
38
|
+
target = index + (columns - 1 - (index % columns));
|
|
39
|
+
break;
|
|
40
|
+
default:
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return target >= 0 && target < count ? target : index;
|
|
45
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
interface PlannerMonthLifecycleOptions {
|
|
4
|
+
month: Date | undefined;
|
|
5
|
+
defaultMonth: Date | undefined;
|
|
6
|
+
settleMonth: (month: Date) => Date;
|
|
7
|
+
onMonthChange: ((month: Date) => void) | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Keeps controlled requests separate from the month a parent has accepted. */
|
|
11
|
+
export function usePlannerMonthLifecycle({
|
|
12
|
+
month: monthProp,
|
|
13
|
+
defaultMonth,
|
|
14
|
+
settleMonth,
|
|
15
|
+
onMonthChange,
|
|
16
|
+
}: PlannerMonthLifecycleOptions): readonly [Date, (month: Date) => void] {
|
|
17
|
+
const [internalMonth, setInternalMonth] = useState(() =>
|
|
18
|
+
settleMonth(defaultMonth ?? new Date())
|
|
19
|
+
);
|
|
20
|
+
const isControlled = monthProp !== undefined;
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
* Parents commonly create an equivalent Date on every render. Keying this
|
|
24
|
+
* derivation by its instant keeps the month object stable, so an unrelated
|
|
25
|
+
* parent render does not rebuild the grid and its month summary.
|
|
26
|
+
*/
|
|
27
|
+
const controlledMonth = useMemo(
|
|
28
|
+
() =>
|
|
29
|
+
monthProp === undefined
|
|
30
|
+
? undefined
|
|
31
|
+
: settleMonth(monthProp),
|
|
32
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
33
|
+
[monthProp?.getTime(), settleMonth]
|
|
34
|
+
);
|
|
35
|
+
const month = controlledMonth ?? internalMonth;
|
|
36
|
+
|
|
37
|
+
const setMonth = useCallback(
|
|
38
|
+
(next: Date) => {
|
|
39
|
+
const settled = settleMonth(next);
|
|
40
|
+
if (!isControlled) setInternalMonth(settled);
|
|
41
|
+
onMonthChange?.(settled);
|
|
42
|
+
},
|
|
43
|
+
[isControlled, onMonthChange, settleMonth]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return [month, setMonth] as const;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface PlannerSelectionLifecycleOptions {
|
|
50
|
+
selected: Date | null | undefined;
|
|
51
|
+
defaultSelected: Date | null;
|
|
52
|
+
onSelectedChange: ((date: Date | null) => void) | undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Commits locally only when selection is uncontrolled, but always reports a request. */
|
|
56
|
+
export function usePlannerSelectionLifecycle({
|
|
57
|
+
selected: selectedProp,
|
|
58
|
+
defaultSelected,
|
|
59
|
+
onSelectedChange,
|
|
60
|
+
}: PlannerSelectionLifecycleOptions): readonly [
|
|
61
|
+
Date | null,
|
|
62
|
+
(date: Date | null) => void,
|
|
63
|
+
] {
|
|
64
|
+
const [internalSelected, setInternalSelected] = useState<Date | null>(defaultSelected);
|
|
65
|
+
const isControlled = selectedProp !== undefined;
|
|
66
|
+
const selected = isControlled ? selectedProp : internalSelected;
|
|
67
|
+
const select = useCallback(
|
|
68
|
+
(date: Date | null) => {
|
|
69
|
+
if (!isControlled) setInternalSelected(date);
|
|
70
|
+
onSelectedChange?.(date);
|
|
71
|
+
},
|
|
72
|
+
[isControlled, onSelectedChange]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return [selected, select] as const;
|
|
76
|
+
}
|
|
@@ -59,7 +59,7 @@ import Animated, {
|
|
|
59
59
|
withTiming,
|
|
60
60
|
} from 'react-native-reanimated';
|
|
61
61
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
62
|
-
import {
|
|
62
|
+
import { FocusRestorePortal } from '../../primitives/portal';
|
|
63
63
|
import { Scrim } from '../../primitives/scrim';
|
|
64
64
|
import { useBackHandler } from '../../hooks/use-back-handler';
|
|
65
65
|
import { BottomSheet } from '../bottom-sheet';
|
|
@@ -535,7 +535,7 @@ function PopoverContent({
|
|
|
535
535
|
if (!open || !trigger) return null;
|
|
536
536
|
|
|
537
537
|
return (
|
|
538
|
-
<
|
|
538
|
+
<FocusRestorePortal>
|
|
539
539
|
{/* Portal content mounts under PortalHost, outside this provider's
|
|
540
540
|
subtree — re-provide the context so Popover.Close and Popover.Arrow
|
|
541
541
|
keep working. */}
|
|
@@ -609,7 +609,7 @@ function PopoverContent({
|
|
|
609
609
|
</Animated.View>
|
|
610
610
|
</View>
|
|
611
611
|
</PopoverContext.Provider>
|
|
612
|
-
</
|
|
612
|
+
</FocusRestorePortal>
|
|
613
613
|
);
|
|
614
614
|
}
|
|
615
615
|
|
package/src/index.ts
CHANGED
|
@@ -416,6 +416,14 @@ export {
|
|
|
416
416
|
type ChipVariant,
|
|
417
417
|
type ChipSize,
|
|
418
418
|
} from './components/chip';
|
|
419
|
+
export {
|
|
420
|
+
Collapsible,
|
|
421
|
+
type CollapsibleProps,
|
|
422
|
+
type CollapsibleTriggerProps,
|
|
423
|
+
type CollapsibleIndicatorProps,
|
|
424
|
+
type CollapsibleContentProps,
|
|
425
|
+
type CollapsibleVariant,
|
|
426
|
+
} from './components/collapsible';
|
|
419
427
|
export {
|
|
420
428
|
ColorPicker,
|
|
421
429
|
type ColorPickerProps,
|
|
@@ -728,6 +736,11 @@ export {
|
|
|
728
736
|
type MarkerIconProps,
|
|
729
737
|
type MarkerContentProps,
|
|
730
738
|
} from './components/marker';
|
|
739
|
+
export {
|
|
740
|
+
Marquee,
|
|
741
|
+
type MarqueeProps,
|
|
742
|
+
type MarqueeDirection,
|
|
743
|
+
} from './components/marquee';
|
|
731
744
|
export {
|
|
732
745
|
Menu,
|
|
733
746
|
type MenuProps,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
interface FocusDocument {
|
|
2
|
+
activeElement?: FocusTarget | null;
|
|
3
|
+
body?: FocusTarget | null;
|
|
4
|
+
documentElement?: FocusTarget | null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface FocusTarget {
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
isConnected?: boolean;
|
|
10
|
+
focus: (options?: { preventScroll?: boolean }) => void;
|
|
11
|
+
getAttribute?: (name: string) => string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type RestoreFocus = () => boolean;
|
|
15
|
+
|
|
16
|
+
interface FocusOwner {
|
|
17
|
+
owner: string;
|
|
18
|
+
released: boolean;
|
|
19
|
+
restore: RestoreFocus;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Restores nested overlays in last-opened order, even when parents close first. */
|
|
23
|
+
export class FocusRestorationStore {
|
|
24
|
+
private owners: FocusOwner[] = [];
|
|
25
|
+
|
|
26
|
+
acquire(owner: string, restore: RestoreFocus): (() => void) {
|
|
27
|
+
const entry = { owner, released: false, restore };
|
|
28
|
+
this.owners.push(entry);
|
|
29
|
+
|
|
30
|
+
return () => {
|
|
31
|
+
if (entry.released) return;
|
|
32
|
+
entry.released = true;
|
|
33
|
+
this.restoreReleasedOwners();
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private restoreReleasedOwners() {
|
|
38
|
+
const candidates: RestoreFocus[] = [];
|
|
39
|
+
while (this.owners.at(-1)?.released) {
|
|
40
|
+
const entry = this.owners.pop();
|
|
41
|
+
if (entry) candidates.push(entry.restore);
|
|
42
|
+
}
|
|
43
|
+
candidates.some((restore) => restore());
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Captures the current browser focus without evaluating DOM APIs on native. */
|
|
48
|
+
export function captureFocusRestore(
|
|
49
|
+
document = (globalThis as { document?: FocusDocument }).document
|
|
50
|
+
): RestoreFocus {
|
|
51
|
+
const target = document?.activeElement;
|
|
52
|
+
if (!target || target === document?.body || target === document?.documentElement) {
|
|
53
|
+
return () => false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return () => {
|
|
57
|
+
if (target.isConnected === false || target.disabled) return false;
|
|
58
|
+
if (target.getAttribute?.('aria-disabled') === 'true') return false;
|
|
59
|
+
try {
|
|
60
|
+
target.focus({ preventScroll: true });
|
|
61
|
+
} catch {
|
|
62
|
+
try {
|
|
63
|
+
target.focus();
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return document?.activeElement === target;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type ReactNode,
|
|
9
9
|
} from 'react';
|
|
10
10
|
import { ModalIsolationStore } from './modal-isolation-store';
|
|
11
|
+
import { captureFocusRestore, FocusRestorationStore } from './focus-restoration-store';
|
|
11
12
|
|
|
12
13
|
type PortalMap = ReadonlyMap<string, ReactNode>;
|
|
13
14
|
|
|
@@ -41,16 +42,21 @@ class PortalStore {
|
|
|
41
42
|
|
|
42
43
|
const PortalContext = createContext<PortalStore | null>(null);
|
|
43
44
|
const ModalIsolationContext = createContext<ModalIsolationStore | null>(null);
|
|
45
|
+
const FocusRestorationContext = createContext<FocusRestorationStore | null>(null);
|
|
44
46
|
|
|
45
47
|
export function PortalProvider({ children }: { children: ReactNode }) {
|
|
46
48
|
const portalStoreRef = useRef<PortalStore | null>(null);
|
|
47
49
|
const modalStoreRef = useRef<ModalIsolationStore | null>(null);
|
|
50
|
+
const focusStoreRef = useRef<FocusRestorationStore | null>(null);
|
|
48
51
|
portalStoreRef.current ??= new PortalStore();
|
|
49
52
|
modalStoreRef.current ??= new ModalIsolationStore();
|
|
53
|
+
focusStoreRef.current ??= new FocusRestorationStore();
|
|
50
54
|
return (
|
|
51
55
|
<PortalContext.Provider value={portalStoreRef.current}>
|
|
52
56
|
<ModalIsolationContext.Provider value={modalStoreRef.current}>
|
|
53
|
-
{
|
|
57
|
+
<FocusRestorationContext.Provider value={focusStoreRef.current}>
|
|
58
|
+
{children}
|
|
59
|
+
</FocusRestorationContext.Provider>
|
|
54
60
|
</ModalIsolationContext.Provider>
|
|
55
61
|
</PortalContext.Provider>
|
|
56
62
|
);
|
|
@@ -80,9 +86,27 @@ export function Portal({ children }: { children: ReactNode }) {
|
|
|
80
86
|
/** A portal that owns modal focus until it unmounts. */
|
|
81
87
|
export function ModalPortal({ children }: { children: ReactNode }) {
|
|
82
88
|
const store = useModalIsolationStore('ModalPortal');
|
|
89
|
+
const focusStore = useFocusRestorationStore('ModalPortal');
|
|
83
90
|
const owner = useId();
|
|
84
91
|
|
|
85
|
-
useEffect(() =>
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
const releaseIsolation = store.acquire(owner);
|
|
94
|
+
const releaseFocus = focusStore.acquire(owner, captureFocusRestore());
|
|
95
|
+
return () => {
|
|
96
|
+
releaseIsolation();
|
|
97
|
+
releaseFocus();
|
|
98
|
+
};
|
|
99
|
+
}, [focusStore, owner, store]);
|
|
100
|
+
|
|
101
|
+
return <Portal>{children}</Portal>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A non-isolating portal that returns browser focus to what opened it. */
|
|
105
|
+
export function FocusRestorePortal({ children }: { children: ReactNode }) {
|
|
106
|
+
const store = useFocusRestorationStore('FocusRestorePortal');
|
|
107
|
+
const owner = useId();
|
|
108
|
+
|
|
109
|
+
useEffect(() => store.acquire(owner, captureFocusRestore()), [owner, store]);
|
|
86
110
|
|
|
87
111
|
return <Portal>{children}</Portal>;
|
|
88
112
|
}
|
|
@@ -101,6 +125,14 @@ function useModalIsolationStore(component: string): ModalIsolationStore {
|
|
|
101
125
|
return store;
|
|
102
126
|
}
|
|
103
127
|
|
|
128
|
+
function useFocusRestorationStore(component: string): FocusRestorationStore {
|
|
129
|
+
const store = useContext(FocusRestorationContext);
|
|
130
|
+
if (!store) {
|
|
131
|
+
throw new Error(`${component} must be used within a <PanelUIProvider>`);
|
|
132
|
+
}
|
|
133
|
+
return store;
|
|
134
|
+
}
|
|
135
|
+
|
|
104
136
|
/** Mount point for portaled content. PanelUIProvider renders one automatically. */
|
|
105
137
|
export function PortalHost() {
|
|
106
138
|
const store = usePortalStore('PortalHost');
|