@octanejs/base-ui 0.1.14 → 0.1.16
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/package.json +5 -5
- package/src/alert-dialog.ts +2 -0
- package/src/button.ts +63 -0
- package/src/context-menu.ts +317 -0
- package/src/csp-provider.ts +37 -0
- package/src/dialog.ts +89 -22
- package/src/direction-provider.ts +32 -0
- package/src/index.ts +10 -0
- package/src/menu.ts +3387 -0
- package/src/menubar.ts +160 -0
- package/src/popover.ts +99 -25
- package/src/preview-card.ts +812 -0
- package/src/tooltip.ts +1053 -0
- package/src/unstable-use-media-query.ts +101 -0
- package/src/utils/CSPContext.ts +21 -0
- package/src/utils/ContextMenuRootContext.ts +44 -0
- package/src/utils/FloatingPortalLite.ts +29 -0
- package/src/utils/MenubarContext.ts +38 -0
- package/src/utils/RequestQueue.ts +132 -0
- package/src/utils/TimeoutManager.ts +32 -0
- package/src/utils/composite/grid-utils.ts +437 -0
- package/src/utils/composite/gridNavigation.ts +127 -0
- package/src/utils/composite/list-utils.ts +56 -28
- package/src/utils/composite/useCompositeRoot.ts +11 -13
- package/src/utils/constants.ts +22 -0
- package/src/utils/createChangeEventDetails.ts +4 -0
- package/src/utils/findRootOwnerId.ts +19 -0
- package/src/utils/floating/FloatingDelayGroup.ts +280 -0
- package/src/utils/floating/element.ts +36 -2
- package/src/utils/floating/gridNavigation.ts +54 -0
- package/src/utils/floating/safePolygon.ts +448 -0
- package/src/utils/floating/useClientPoint.ts +276 -0
- package/src/utils/floating/useFocus.ts +248 -0
- package/src/utils/floating/useHoverFloatingInteraction.ts +301 -11
- package/src/utils/floating/useHoverInteractionSharedState.ts +139 -0
- package/src/utils/floating/useHoverReferenceInteraction.ts +507 -14
- package/src/utils/floating/useHoverShared.ts +78 -0
- package/src/utils/floating/useListNavigation.ts +1018 -0
- package/src/utils/floating/useTypeahead.ts +287 -0
- package/src/utils/getCssDimensions.ts +28 -0
- package/src/utils/getPseudoElementBounds.ts +54 -0
- package/src/utils/platform.ts +10 -0
- package/src/utils/popups/index.ts +6 -0
- package/src/utils/popups/inlineRect.ts +289 -0
- package/src/utils/useMixedToggleClickHandler.ts +77 -0
- package/src/utils/useOpenInteractionType.ts +37 -1
- package/src/utils/usePopupAutoResize.ts +250 -0
- package/src/utils/usePopupViewport.ts +375 -0
- package/src/utils/usePreviousValue.ts +25 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/unstable-use-media-query/index.ts (v1.6.0).
|
|
2
|
+
// Subscribes to a CSS media query via `window.matchMedia`, with SSR escape hatches.
|
|
3
|
+
//
|
|
4
|
+
// octane adaptation: octane ships `useSyncExternalStore` natively, so Base UI's
|
|
5
|
+
// `use-sync-external-store/shim` import is dropped; the dev-only `useDebugValue` call is
|
|
6
|
+
// dropped with the rest of the dev-warning surface.
|
|
7
|
+
//
|
|
8
|
+
// SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
|
|
9
|
+
import { useCallback, useMemo, useSyncExternalStore } from 'octane';
|
|
10
|
+
|
|
11
|
+
import { S, splitSlot, subSlot } from './internal';
|
|
12
|
+
import { addEventListener } from './utils/addEventListener';
|
|
13
|
+
|
|
14
|
+
export interface UseMediaQueryOptions {
|
|
15
|
+
/**
|
|
16
|
+
* As `window.matchMedia()` is unavailable on the server, it returns a default match
|
|
17
|
+
* during the first mount.
|
|
18
|
+
* @default false
|
|
19
|
+
*/
|
|
20
|
+
defaultMatches?: boolean | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* You can provide your own implementation of matchMedia. This can be used for handling
|
|
23
|
+
* an iframe content window.
|
|
24
|
+
*/
|
|
25
|
+
matchMedia?: typeof window.matchMedia | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* To perform the server-side hydration, the hook needs to render twice. A first time
|
|
28
|
+
* with `defaultMatches`, the value of the server, and a second time with the resolved
|
|
29
|
+
* value. This double pass rendering cycle comes with a drawback: it's slower. You can
|
|
30
|
+
* set this option to `true` if you use the returned value **only** client-side.
|
|
31
|
+
* @default false
|
|
32
|
+
*/
|
|
33
|
+
noSsr?: boolean | undefined;
|
|
34
|
+
/** Your own implementation of `matchMedia`, used when rendering server-side. */
|
|
35
|
+
ssrMatchMedia?: ((query: string) => { matches: boolean }) | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useMediaQuery(query: string, options?: UseMediaQueryOptions): boolean;
|
|
39
|
+
export function useMediaQuery(...args: any[]): boolean {
|
|
40
|
+
const [user, slotArg] = splitSlot(args);
|
|
41
|
+
const slot = slotArg ?? S('useMediaQuery');
|
|
42
|
+
|
|
43
|
+
const rawQuery = user[0] as string;
|
|
44
|
+
const options = (user[1] as UseMediaQueryOptions | undefined) ?? {};
|
|
45
|
+
|
|
46
|
+
// Wait for jsdom to support the match media feature. All the browsers Base UI supports
|
|
47
|
+
// have this built-in; this defensive check is here for simplicity.
|
|
48
|
+
const supportMatchMedia =
|
|
49
|
+
typeof window !== 'undefined' && typeof window.matchMedia !== 'undefined';
|
|
50
|
+
|
|
51
|
+
const normalizedQuery = rawQuery.replace(/^@media( ?)/m, '');
|
|
52
|
+
|
|
53
|
+
const {
|
|
54
|
+
defaultMatches = false,
|
|
55
|
+
matchMedia = supportMatchMedia ? window.matchMedia : null,
|
|
56
|
+
ssrMatchMedia = null,
|
|
57
|
+
noSsr = false,
|
|
58
|
+
} = options;
|
|
59
|
+
|
|
60
|
+
const getDefaultSnapshot = useCallback(
|
|
61
|
+
() => defaultMatches,
|
|
62
|
+
[defaultMatches],
|
|
63
|
+
subSlot(slot, 'def'),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const getServerSnapshot = useMemo(
|
|
67
|
+
() => {
|
|
68
|
+
if (noSsr && matchMedia) {
|
|
69
|
+
return () => matchMedia(normalizedQuery).matches;
|
|
70
|
+
}
|
|
71
|
+
if (ssrMatchMedia !== null) {
|
|
72
|
+
const { matches } = ssrMatchMedia(normalizedQuery);
|
|
73
|
+
return () => matches;
|
|
74
|
+
}
|
|
75
|
+
return getDefaultSnapshot;
|
|
76
|
+
},
|
|
77
|
+
[getDefaultSnapshot, normalizedQuery, ssrMatchMedia, noSsr, matchMedia],
|
|
78
|
+
subSlot(slot, 'srv'),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const [getSnapshot, subscribe] = useMemo(
|
|
82
|
+
() => {
|
|
83
|
+
if (matchMedia === null) {
|
|
84
|
+
return [getDefaultSnapshot, () => () => {}] as const;
|
|
85
|
+
}
|
|
86
|
+
const mediaQueryList = matchMedia(normalizedQuery);
|
|
87
|
+
return [
|
|
88
|
+
() => mediaQueryList.matches,
|
|
89
|
+
(notify: () => void) => addEventListener(mediaQueryList, 'change', notify),
|
|
90
|
+
] as const;
|
|
91
|
+
},
|
|
92
|
+
[getDefaultSnapshot, matchMedia, normalizedQuery],
|
|
93
|
+
subSlot(slot, 'sub'),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot, subSlot(slot, 'store'));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export namespace useMediaQuery {
|
|
100
|
+
export type Options = UseMediaQueryOptions;
|
|
101
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/internals/csp-context/CSPContext.tsx (v1.6.0).
|
|
2
|
+
// Carries the Content Security Policy configuration for components that need inline
|
|
3
|
+
// `<style>` tags — the `nonce` to stamp on them, and whether to skip them entirely.
|
|
4
|
+
import { createContext, useContext } from 'octane';
|
|
5
|
+
|
|
6
|
+
export interface CSPContextValue {
|
|
7
|
+
nonce?: string | undefined;
|
|
8
|
+
disableStyleElements?: boolean | undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const CSPContext = createContext<CSPContextValue | undefined>(undefined);
|
|
12
|
+
|
|
13
|
+
const DEFAULT_CSP_CONTEXT_VALUE: CSPContextValue = {
|
|
14
|
+
disableStyleElements: false,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function useCSPContext(...args: any[]): CSPContextValue {
|
|
18
|
+
// `useContext` is slot-exempt in octane, so the caller's slot (if any) is ignored.
|
|
19
|
+
void args;
|
|
20
|
+
return useContext(CSPContext) ?? DEFAULT_CSP_CONTEXT_VALUE;
|
|
21
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/context-menu/root/ContextMenuRootContext.ts (v1.6.0),
|
|
2
|
+
// octane-adapted (`React.createContext` → octane `createContext`; `React.RefObject` → plain ref
|
|
3
|
+
// shapes; `React.Dispatch<SetStateAction<T>>` → octane's updater signature).
|
|
4
|
+
//
|
|
5
|
+
// STAGING: only the CONTEXT lands here, ahead of the `ContextMenu` components themselves
|
|
6
|
+
// (Phase 3f stage 4). `Menu.Root`/`Menu.Trigger`/`Menu.Positioner`/`Menu.Popup` branch on
|
|
7
|
+
// `parent.type === 'context-menu'` in several places; having the context now lets those branches
|
|
8
|
+
// be transcribed verbatim instead of being stubbed out and re-added later. Until `ContextMenu.Root`
|
|
9
|
+
// lands nothing provides this context, so `useContextMenuRootContext()` always returns `undefined`.
|
|
10
|
+
import { createContext, useContext } from 'octane';
|
|
11
|
+
|
|
12
|
+
export interface ContextMenuRootContextValue {
|
|
13
|
+
anchor: { getBoundingClientRect: () => DOMRect };
|
|
14
|
+
setAnchor: (value: ContextMenuRootContextValue['anchor']) => void;
|
|
15
|
+
backdropRef: { current: HTMLDivElement | null };
|
|
16
|
+
internalBackdropRef: { current: HTMLDivElement | null };
|
|
17
|
+
actionsRef: {
|
|
18
|
+
current: {
|
|
19
|
+
setOpen: (nextOpen: boolean, eventDetails: any) => void;
|
|
20
|
+
} | null;
|
|
21
|
+
};
|
|
22
|
+
positionerRef: { current: HTMLElement | null };
|
|
23
|
+
allowMouseUpTriggerRef: { current: boolean };
|
|
24
|
+
initialCursorPointRef: { current: { x: number; y: number } | null };
|
|
25
|
+
rootId: string | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const ContextMenuRootContext = createContext<ContextMenuRootContextValue | undefined>(
|
|
29
|
+
undefined,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export function useContextMenuRootContext(optional: false): ContextMenuRootContextValue;
|
|
33
|
+
export function useContextMenuRootContext(optional?: true): ContextMenuRootContextValue | undefined;
|
|
34
|
+
export function useContextMenuRootContext(
|
|
35
|
+
optional = true,
|
|
36
|
+
): ContextMenuRootContextValue | undefined {
|
|
37
|
+
const context = useContext(ContextMenuRootContext);
|
|
38
|
+
if (context === undefined && !optional) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
'Base UI: ContextMenuRootContext is missing. ContextMenu parts must be placed within <ContextMenu.Root>.',
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return context;
|
|
44
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/utils/FloatingPortalLite.tsx (v1.6.0). `FloatingPortal`
|
|
2
|
+
// also carries the tabbable/focus-guard machinery; components that don't need it (Tooltip,
|
|
3
|
+
// PreviewCard, Toast) use this thinner variant, which only creates the portal node and renders the
|
|
4
|
+
// children into it.
|
|
5
|
+
//
|
|
6
|
+
// octane adaptations: forwardRef → ref-as-prop; `createElement` instead of JSX; `ReactDOM`'s
|
|
7
|
+
// `createPortal` → octane's.
|
|
8
|
+
import { createElement, createPortal, Fragment } from 'octane';
|
|
9
|
+
|
|
10
|
+
import { S, subSlot } from '../internal';
|
|
11
|
+
import { useFloatingPortalNode } from './floating/FloatingPortal';
|
|
12
|
+
|
|
13
|
+
export function FloatingPortalLite(componentProps: any): any {
|
|
14
|
+
const slot = S('FloatingPortalLite');
|
|
15
|
+
const { children, container, className, render, style, ref, ...elementProps } = componentProps;
|
|
16
|
+
|
|
17
|
+
const { portalNode, portalSubtree } = useFloatingPortalNode(
|
|
18
|
+
{ container, ref, componentProps: { render, className, style }, elementProps },
|
|
19
|
+
subSlot(slot, 'node'),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
if (!portalSubtree && !portalNode) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return createElement(Fragment, {
|
|
27
|
+
children: [portalSubtree, portalNode ? createPortal(children, portalNode) : null],
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/menubar/MenubarContext.ts (v1.6.0), octane-adapted
|
|
2
|
+
// (`React.createContext` → octane `createContext`; `React.RefObject` → a plain ref shape).
|
|
3
|
+
//
|
|
4
|
+
// STAGING: only the CONTEXT lands here, ahead of the `Menubar` component itself (Phase 3f stage 4).
|
|
5
|
+
// `Menu.Root`/`Menu.Trigger`/`Menu.Positioner` branch on `parent.type === 'menubar'` in several
|
|
6
|
+
// places; having the context now lets those branches be transcribed verbatim instead of being
|
|
7
|
+
// stubbed out and re-added later. Until `Menubar` lands nothing provides this context, so
|
|
8
|
+
// `useMenubarContext(true)` always returns `null`.
|
|
9
|
+
import { createContext, useContext } from 'octane';
|
|
10
|
+
|
|
11
|
+
import type { MenuRootOrientation } from '../menu';
|
|
12
|
+
|
|
13
|
+
export interface MenubarContextValue {
|
|
14
|
+
modal: boolean;
|
|
15
|
+
disabled: boolean;
|
|
16
|
+
contentElement: HTMLElement | null;
|
|
17
|
+
setContentElement: (element: HTMLElement | null) => void;
|
|
18
|
+
hasSubmenuOpen: boolean;
|
|
19
|
+
setHasSubmenuOpen: (open: boolean) => void;
|
|
20
|
+
orientation: MenuRootOrientation;
|
|
21
|
+
allowMouseUpTriggerRef: { current: boolean };
|
|
22
|
+
rootId: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const MenubarContext = createContext<MenubarContextValue | null>(null);
|
|
26
|
+
|
|
27
|
+
export function useMenubarContext(optional?: false): MenubarContextValue;
|
|
28
|
+
export function useMenubarContext(optional: true): MenubarContextValue | null;
|
|
29
|
+
export function useMenubarContext(optional?: boolean): MenubarContextValue | null {
|
|
30
|
+
const context = useContext(MenubarContext);
|
|
31
|
+
if (context === null && !optional) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
'Base UI: MenubarContext is missing. Menubar parts must be placed within <Menubar>.',
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return context;
|
|
38
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/internals/RequestQueue.ts (v1.6.0). Manages concurrent
|
|
2
|
+
// fetching with a queue: tracks each key's lifecycle through queued → pending and caps the number
|
|
3
|
+
// of in-flight requests so the server is not overwhelmed. Subclasses override `pickEntries` to
|
|
4
|
+
// change the ordering (the default is FIFO).
|
|
5
|
+
//
|
|
6
|
+
// Pure — no renderer API is involved, so this is a straight transcription.
|
|
7
|
+
export type RequestStatus = 'queued' | 'pending' | 'unknown';
|
|
8
|
+
|
|
9
|
+
export interface RequestQueueOptions<TKey> {
|
|
10
|
+
/**
|
|
11
|
+
* The function to call when a queued key is ready to be fetched.
|
|
12
|
+
*/
|
|
13
|
+
fetchFn: (key: TKey) => Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* The maximum number of concurrent requests.
|
|
16
|
+
* @default Infinity
|
|
17
|
+
*/
|
|
18
|
+
maxConcurrentRequests?: number | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Serialize a key to a string for use as a map key.
|
|
21
|
+
* Required for complex key types (e.g., objects).
|
|
22
|
+
* @default String(key)
|
|
23
|
+
*/
|
|
24
|
+
getKeyId?: ((key: TKey) => string) | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Manages concurrent fetching with a queue system.
|
|
29
|
+
* Tracks request lifecycle through queued → pending states
|
|
30
|
+
* and limits concurrency to prevent overwhelming the server.
|
|
31
|
+
*/
|
|
32
|
+
export class RequestQueue<TKey> {
|
|
33
|
+
protected pendingRequests = new Map<string, TKey>();
|
|
34
|
+
|
|
35
|
+
protected queuedRequests = new Map<string, TKey>();
|
|
36
|
+
|
|
37
|
+
protected fetchFn: (key: TKey) => Promise<void>;
|
|
38
|
+
|
|
39
|
+
protected maxConcurrentRequests: number;
|
|
40
|
+
|
|
41
|
+
protected getKeyId: (key: TKey) => string;
|
|
42
|
+
|
|
43
|
+
constructor(options: RequestQueueOptions<TKey>) {
|
|
44
|
+
this.fetchFn = options.fetchFn;
|
|
45
|
+
this.maxConcurrentRequests = options.maxConcurrentRequests ?? Infinity;
|
|
46
|
+
this.getKeyId = options.getKeyId ?? String;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns the next `count` entries to process from the queue.
|
|
51
|
+
* Default is FIFO (oldest first). Subclasses can override for different ordering.
|
|
52
|
+
*/
|
|
53
|
+
protected pickEntries(count: number): [string, TKey][] {
|
|
54
|
+
const result: [string, TKey][] = [];
|
|
55
|
+
const iterator = this.queuedRequests.entries();
|
|
56
|
+
for (let i = 0; i < count; i += 1) {
|
|
57
|
+
const { value } = iterator.next() as IteratorYieldResult<[string, TKey]>;
|
|
58
|
+
result.push(value);
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
protected processQueue = async () => {
|
|
64
|
+
if (this.queuedRequests.size === 0 || this.pendingRequests.size >= this.maxConcurrentRequests) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const loopLength = Math.min(
|
|
69
|
+
this.maxConcurrentRequests - this.pendingRequests.size,
|
|
70
|
+
this.queuedRequests.size,
|
|
71
|
+
);
|
|
72
|
+
if (loopLength === 0) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const fetchPromises: Promise<void>[] = [];
|
|
77
|
+
|
|
78
|
+
for (const [keyId, key] of this.pickEntries(loopLength)) {
|
|
79
|
+
this.queuedRequests.delete(keyId);
|
|
80
|
+
this.pendingRequests.set(keyId, key);
|
|
81
|
+
|
|
82
|
+
fetchPromises.push(
|
|
83
|
+
this.fetchFn(key).catch(() => {
|
|
84
|
+
this.pendingRequests.delete(keyId);
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await Promise.all(fetchPromises);
|
|
90
|
+
if (this.queuedRequests.size > 0) {
|
|
91
|
+
await this.processQueue();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
public queue = async (keys: TKey[]) => {
|
|
96
|
+
for (const key of keys) {
|
|
97
|
+
const keyId = this.getKeyId(key);
|
|
98
|
+
if (!this.pendingRequests.has(keyId)) {
|
|
99
|
+
this.queuedRequests.set(keyId, key);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
await this.processQueue();
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
public setRequestSettled = async (key: TKey) => {
|
|
106
|
+
const keyId = this.getKeyId(key);
|
|
107
|
+
this.pendingRequests.delete(keyId);
|
|
108
|
+
await this.processQueue();
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
public clear = () => {
|
|
112
|
+
this.queuedRequests.clear();
|
|
113
|
+
this.pendingRequests.clear();
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
public clearPendingRequest = async (key: TKey) => {
|
|
117
|
+
const keyId = this.getKeyId(key);
|
|
118
|
+
this.pendingRequests.delete(keyId);
|
|
119
|
+
await this.processQueue();
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
public getRequestStatus = (key: TKey): RequestStatus => {
|
|
123
|
+
const keyId = this.getKeyId(key);
|
|
124
|
+
if (this.pendingRequests.has(keyId)) {
|
|
125
|
+
return 'pending';
|
|
126
|
+
}
|
|
127
|
+
if (this.queuedRequests.has(keyId)) {
|
|
128
|
+
return 'queued';
|
|
129
|
+
}
|
|
130
|
+
return 'unknown';
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/internals/TimeoutManager.ts (v1.6.0). Several keyed
|
|
2
|
+
// timeouts behind one handle, so a caller can start/replace/cancel each independently and drop them
|
|
3
|
+
// all on teardown. Distinct from `utils/useTimeout`'s `Timeout`, which owns exactly one.
|
|
4
|
+
//
|
|
5
|
+
// Pure — no renderer API is involved, so this is a straight transcription.
|
|
6
|
+
export class TimeoutManager {
|
|
7
|
+
private ids: Map<string, number> = new Map();
|
|
8
|
+
|
|
9
|
+
/** Starts `fn` after `delay`, replacing any timeout already running under `key`. */
|
|
10
|
+
start = (key: string, delay: number, fn: () => void): void => {
|
|
11
|
+
this.clear(key);
|
|
12
|
+
const id = setTimeout(() => {
|
|
13
|
+
this.ids.delete(key);
|
|
14
|
+
fn();
|
|
15
|
+
}, delay) as unknown as number;
|
|
16
|
+
|
|
17
|
+
this.ids.set(key, id);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
clear = (key: string): void => {
|
|
21
|
+
const id = this.ids.get(key);
|
|
22
|
+
if (id != null) {
|
|
23
|
+
clearTimeout(id);
|
|
24
|
+
this.ids.delete(key);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
clearAll = (): void => {
|
|
29
|
+
this.ids.forEach(clearTimeout);
|
|
30
|
+
this.ids.clear();
|
|
31
|
+
};
|
|
32
|
+
}
|