@octanejs/base-ui 0.1.14 → 0.1.15
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 +4 -4
- package/src/alert-dialog.ts +2 -0
- package/src/button.ts +63 -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 +7 -0
- package/src/popover.ts +92 -22
- 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/FloatingPortalLite.ts +29 -0
- package/src/utils/createChangeEventDetails.ts +1 -0
- package/src/utils/floating/FloatingDelayGroup.ts +280 -0
- package/src/utils/floating/element.ts +36 -2
- 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/getCssDimensions.ts +28 -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/usePopupAutoResize.ts +250 -0
- package/src/utils/usePopupViewport.ts +375 -0
- package/src/utils/usePreviousValue.ts +25 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/base-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
"@floating-ui/utils": "^0.2.12",
|
|
42
42
|
"aria-hidden": "^1.2.6",
|
|
43
43
|
"tabbable": "^6.5.0",
|
|
44
|
-
"@octanejs/floating-ui": "0.1.
|
|
44
|
+
"@octanejs/floating-ui": "0.1.16"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"octane": "0.1.
|
|
47
|
+
"octane": "0.1.17"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@base-ui/react": "^1.6.0",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"react": "^19.2.7",
|
|
54
54
|
"react-dom": "^19.2.7",
|
|
55
55
|
"vitest": "^4.1.10",
|
|
56
|
-
"octane": "0.1.
|
|
56
|
+
"octane": "0.1.17"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"test": "vitest run"
|
package/src/alert-dialog.ts
CHANGED
|
@@ -40,8 +40,10 @@ export const AlertDialog = {
|
|
|
40
40
|
Portal: Dialog.Portal,
|
|
41
41
|
Backdrop: Dialog.Backdrop,
|
|
42
42
|
Popup: Dialog.Popup,
|
|
43
|
+
Viewport: Dialog.Viewport,
|
|
43
44
|
Title: Dialog.Title,
|
|
44
45
|
Description: Dialog.Description,
|
|
45
46
|
Close: Dialog.Close,
|
|
46
47
|
createHandle: createAlertDialogHandle,
|
|
48
|
+
Handle: AlertDialogHandle,
|
|
47
49
|
};
|
package/src/button.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/button/Button.tsx (v1.6.0). A button that can be
|
|
2
|
+
// used to trigger actions. Renders a `<button>` element.
|
|
3
|
+
import { S, subSlot } from './internal';
|
|
4
|
+
import { useButton } from './utils/useButton';
|
|
5
|
+
import { useRenderElement } from './utils/useRenderElement';
|
|
6
|
+
|
|
7
|
+
export interface ButtonState {
|
|
8
|
+
/** Whether the button should ignore user interaction. */
|
|
9
|
+
disabled: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ButtonProps {
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Whether the button should be focusable when disabled.
|
|
16
|
+
* @default false
|
|
17
|
+
*/
|
|
18
|
+
focusableWhenDisabled?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Whether the component renders a native `<button>` element when replacing it via the
|
|
21
|
+
* `render` prop.
|
|
22
|
+
* @default true
|
|
23
|
+
*/
|
|
24
|
+
nativeButton?: boolean;
|
|
25
|
+
className?: string | ((state: ButtonState) => string | undefined);
|
|
26
|
+
render?: import('./utils/useRenderElement').RenderProp<ButtonState>;
|
|
27
|
+
ref?: any;
|
|
28
|
+
[key: string]: any;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// octane: forwardRef → ref-as-prop.
|
|
32
|
+
export function Button(props: ButtonProps): any {
|
|
33
|
+
const slot = S('Button');
|
|
34
|
+
const {
|
|
35
|
+
render,
|
|
36
|
+
className,
|
|
37
|
+
disabled = false,
|
|
38
|
+
focusableWhenDisabled = false,
|
|
39
|
+
nativeButton = true,
|
|
40
|
+
style,
|
|
41
|
+
ref,
|
|
42
|
+
...elementProps
|
|
43
|
+
} = props;
|
|
44
|
+
|
|
45
|
+
const { getButtonProps, buttonRef } = useButton(
|
|
46
|
+
{ disabled, focusableWhenDisabled, native: nativeButton },
|
|
47
|
+
subSlot(slot, 'btn'),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const state: ButtonState = { disabled };
|
|
51
|
+
|
|
52
|
+
return useRenderElement(
|
|
53
|
+
'button',
|
|
54
|
+
{ className, render, style },
|
|
55
|
+
{ state, ref: [ref, buttonRef], props: [elementProps, getButtonProps] },
|
|
56
|
+
subSlot(slot, 're'),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export namespace Button {
|
|
61
|
+
export type Props = ButtonProps;
|
|
62
|
+
export type State = ButtonState;
|
|
63
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/csp-provider/CSPProvider.tsx (v1.6.0). Provides a
|
|
2
|
+
// default Content Security Policy configuration for Base UI components that require inline
|
|
3
|
+
// `<style>` or `<script>` tags.
|
|
4
|
+
import { createElement, useMemo } from 'octane';
|
|
5
|
+
|
|
6
|
+
import { S, subSlot } from './internal';
|
|
7
|
+
import { CSPContext } from './utils/CSPContext';
|
|
8
|
+
import type { CSPContextValue } from './utils/CSPContext';
|
|
9
|
+
|
|
10
|
+
export interface CSPProviderProps {
|
|
11
|
+
children?: any;
|
|
12
|
+
/** The nonce value to apply to inline `<style>` and `<script>` tags. */
|
|
13
|
+
nonce?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Whether inline `<style>` elements created by Base UI components should not be rendered.
|
|
16
|
+
* Instead, components must specify the CSS styles via custom class names or other methods.
|
|
17
|
+
* @default false
|
|
18
|
+
*/
|
|
19
|
+
disableStyleElements?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function CSPProvider(props: CSPProviderProps): any {
|
|
23
|
+
const slot = S('CSPProvider');
|
|
24
|
+
const { children, nonce, disableStyleElements } = props;
|
|
25
|
+
|
|
26
|
+
const contextValue: CSPContextValue = useMemo(
|
|
27
|
+
() => ({ nonce, disableStyleElements }),
|
|
28
|
+
[nonce, disableStyleElements],
|
|
29
|
+
subSlot(slot, 'ctx'),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return createElement(CSPContext.Provider, { value: contextValue, children });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export namespace CSPProvider {
|
|
36
|
+
export type Props = CSPProviderProps;
|
|
37
|
+
}
|
package/src/dialog.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import {
|
|
10
10
|
createContext,
|
|
11
11
|
createElement,
|
|
12
|
+
Fragment,
|
|
12
13
|
useContext,
|
|
13
14
|
useMemo,
|
|
14
15
|
useRef,
|
|
@@ -33,7 +34,11 @@ import { useDismiss } from './utils/floating/useDismiss';
|
|
|
33
34
|
import { useScrollLock } from './utils/useScrollLock';
|
|
34
35
|
import { useOpenChangeComplete } from './utils/useOpenChangeComplete';
|
|
35
36
|
import { useOpenMethodTriggerProps } from './utils/useOpenInteractionType';
|
|
36
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
triggerOpenStateMapping,
|
|
39
|
+
popupStateMapping,
|
|
40
|
+
CommonPopupDataAttributes,
|
|
41
|
+
} from './utils/popupStateMapping';
|
|
37
42
|
import { transitionStatusMapping } from './utils/useTransitionStatus';
|
|
38
43
|
import type { StateAttributesMapping } from './utils/getStateAttributesProps';
|
|
39
44
|
import { EMPTY_OBJECT } from './utils/empty';
|
|
@@ -404,14 +409,7 @@ function DialogInteractions(props: any): any {
|
|
|
404
409
|
subSlot(slot, 'pip'),
|
|
405
410
|
);
|
|
406
411
|
|
|
407
|
-
|
|
408
|
-
return props.children ?? null;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// Passthrough used when the interactions aren't mounted, to keep the Provider's children a stable
|
|
412
|
-
// element-descriptor shape (see the note at the `content` call site).
|
|
413
|
-
function DialogChildren(props: any): any {
|
|
414
|
-
return props.children ?? null;
|
|
412
|
+
return null;
|
|
415
413
|
}
|
|
416
414
|
|
|
417
415
|
export function useRenderDialogRoot<Payload>(
|
|
@@ -482,19 +480,19 @@ export function useRenderDialogRoot<Payload>(
|
|
|
482
480
|
const resolvedChildren =
|
|
483
481
|
typeof children === 'function' && !isChildrenBlock(children) ? children({ payload }) : children;
|
|
484
482
|
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
children: resolvedChildren,
|
|
497
|
-
|
|
483
|
+
// `DialogInteractions` is a headless SIBLING of the children, exactly as Base UI renders it, so
|
|
484
|
+
// toggling it never disturbs them.
|
|
485
|
+
const content = [
|
|
486
|
+
shouldRenderInteractions
|
|
487
|
+
? createElement(DialogInteractions, {
|
|
488
|
+
key: 'interactions',
|
|
489
|
+
store,
|
|
490
|
+
parentContext: parentDialogRootContext?.store.context,
|
|
491
|
+
isDrawer,
|
|
492
|
+
})
|
|
493
|
+
: null,
|
|
494
|
+
createElement(Fragment, { key: 'children', children: resolvedChildren }),
|
|
495
|
+
];
|
|
498
496
|
|
|
499
497
|
return createElement(IsDrawerContext.Provider, {
|
|
500
498
|
value: false,
|
|
@@ -844,6 +842,73 @@ function DialogClose(componentProps: any): any {
|
|
|
844
842
|
);
|
|
845
843
|
}
|
|
846
844
|
|
|
845
|
+
// --- Viewport ----------------------------------------------------------------
|
|
846
|
+
|
|
847
|
+
export const DialogViewportDataAttributes = {
|
|
848
|
+
open: CommonPopupDataAttributes.open,
|
|
849
|
+
closed: CommonPopupDataAttributes.closed,
|
|
850
|
+
startingStyle: CommonPopupDataAttributes.startingStyle,
|
|
851
|
+
endingStyle: CommonPopupDataAttributes.endingStyle,
|
|
852
|
+
nested: 'data-nested',
|
|
853
|
+
nestedDialogOpen: 'data-nested-dialog-open',
|
|
854
|
+
} as const;
|
|
855
|
+
|
|
856
|
+
const viewportStateAttributesMapping: StateAttributesMapping<any> = {
|
|
857
|
+
...(popupStateMapping as StateAttributesMapping<any>),
|
|
858
|
+
...(transitionStatusMapping as StateAttributesMapping<any>),
|
|
859
|
+
nested(value: boolean) {
|
|
860
|
+
return value ? { [DialogViewportDataAttributes.nested]: '' } : null;
|
|
861
|
+
},
|
|
862
|
+
nestedDialogOpen(value: boolean) {
|
|
863
|
+
return value ? { [DialogViewportDataAttributes.nestedDialogOpen]: '' } : null;
|
|
864
|
+
},
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
// A positioning container for the dialog popup that can be made scrollable.
|
|
868
|
+
function DialogViewport(componentProps: any): any {
|
|
869
|
+
const slot = S('DialogViewport');
|
|
870
|
+
const { render, className, style, children, ref, ...elementProps } = componentProps;
|
|
871
|
+
|
|
872
|
+
const keepMounted = useDialogPortalContext();
|
|
873
|
+
const { store } = useDialogRootContext() as DialogRootContextValue;
|
|
874
|
+
|
|
875
|
+
const open = store.useState('open', subSlot(slot, 'open'));
|
|
876
|
+
const nested = store.useState('nested', subSlot(slot, 'nested'));
|
|
877
|
+
const transitionStatus = store.useState('transitionStatus', subSlot(slot, 'ts'));
|
|
878
|
+
const nestedOpenDialogCount = store.useState('nestedOpenDialogCount', subSlot(slot, 'nodc'));
|
|
879
|
+
const mounted = store.useState('mounted', subSlot(slot, 'mounted'));
|
|
880
|
+
|
|
881
|
+
const setViewportElement = store.useStateSetter('viewportElement', subSlot(slot, 'sve'));
|
|
882
|
+
|
|
883
|
+
const state = {
|
|
884
|
+
open,
|
|
885
|
+
nested,
|
|
886
|
+
transitionStatus,
|
|
887
|
+
nestedDialogOpen: nestedOpenDialogCount > 0,
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
return useRenderElement(
|
|
891
|
+
'div',
|
|
892
|
+
{ render, className, style },
|
|
893
|
+
{
|
|
894
|
+
enabled: keepMounted || mounted,
|
|
895
|
+
state,
|
|
896
|
+
ref: [ref, setViewportElement],
|
|
897
|
+
stateAttributesMapping: viewportStateAttributesMapping,
|
|
898
|
+
props: [
|
|
899
|
+
{
|
|
900
|
+
role: 'presentation',
|
|
901
|
+
hidden: !mounted,
|
|
902
|
+
style: { pointerEvents: !open ? 'none' : undefined },
|
|
903
|
+
children,
|
|
904
|
+
},
|
|
905
|
+
elementProps,
|
|
906
|
+
],
|
|
907
|
+
},
|
|
908
|
+
subSlot(slot, 're'),
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
|
|
847
912
|
// --- Namespace ---------------------------------------------------------------
|
|
848
913
|
|
|
849
914
|
export const Dialog = {
|
|
@@ -852,8 +917,10 @@ export const Dialog = {
|
|
|
852
917
|
Portal: DialogPortal,
|
|
853
918
|
Backdrop: DialogBackdrop,
|
|
854
919
|
Popup: DialogPopup,
|
|
920
|
+
Viewport: DialogViewport,
|
|
855
921
|
Title: DialogTitle,
|
|
856
922
|
Description: DialogDescription,
|
|
857
923
|
Close: DialogClose,
|
|
858
924
|
createHandle: createDialogHandle,
|
|
925
|
+
Handle: DialogHandle,
|
|
859
926
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/direction-provider/DirectionProvider.tsx (v1.6.0).
|
|
2
|
+
// Enables RTL behavior for Base UI components.
|
|
3
|
+
//
|
|
4
|
+
// octane adaptation: `utils/DirectionContext` carries the `TextDirection` string DIRECTLY
|
|
5
|
+
// rather than Base UI's `{ direction }` wrapper object, so the provider needs no `useMemo`
|
|
6
|
+
// to stay referentially stable. The context value is never observable in the DOM, and every
|
|
7
|
+
// consumer reads it through `useDirection()`.
|
|
8
|
+
import { createElement } from 'octane';
|
|
9
|
+
|
|
10
|
+
import { DirectionContext, useDirection } from './utils/DirectionContext';
|
|
11
|
+
import type { TextDirection } from './utils/DirectionContext';
|
|
12
|
+
|
|
13
|
+
export type { TextDirection };
|
|
14
|
+
export { useDirection };
|
|
15
|
+
|
|
16
|
+
export interface DirectionProviderProps {
|
|
17
|
+
children?: any;
|
|
18
|
+
/**
|
|
19
|
+
* The reading direction of the text.
|
|
20
|
+
* @default 'ltr'
|
|
21
|
+
*/
|
|
22
|
+
direction?: TextDirection;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function DirectionProvider(props: DirectionProviderProps): any {
|
|
26
|
+
const { direction = 'ltr' } = props;
|
|
27
|
+
return createElement(DirectionContext.Provider, { value: direction, children: props.children });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export namespace DirectionProvider {
|
|
31
|
+
export type Props = DirectionProviderProps;
|
|
32
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
export { Separator } from './separator';
|
|
6
6
|
export { AlertDialog } from './alert-dialog';
|
|
7
7
|
export { Avatar } from './avatar';
|
|
8
|
+
export { Button } from './button';
|
|
9
|
+
export { CSPProvider } from './csp-provider';
|
|
10
|
+
export { DirectionProvider, useDirection } from './direction-provider';
|
|
11
|
+
export type { TextDirection } from './direction-provider';
|
|
12
|
+
export { useMediaQuery } from './unstable-use-media-query';
|
|
8
13
|
export { Checkbox } from './checkbox';
|
|
9
14
|
export { CheckboxGroup } from './checkbox-group';
|
|
10
15
|
export { Dialog } from './dialog';
|
|
@@ -18,6 +23,8 @@ export { Progress } from './progress';
|
|
|
18
23
|
export { Radio } from './radio';
|
|
19
24
|
export { RadioGroup } from './radio-group';
|
|
20
25
|
export { Popover } from './popover';
|
|
26
|
+
export { PreviewCard } from './preview-card';
|
|
27
|
+
export { Tooltip } from './tooltip';
|
|
21
28
|
export { Slider } from './slider';
|
|
22
29
|
export { Switch } from './switch';
|
|
23
30
|
export { Toggle } from './toggle';
|
package/src/popover.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
// Ported from .base-ui/packages/react/src/popover/ (v1.6.0): store (PopoverStore/PopoverHandle),
|
|
2
|
-
// root (context/PopoverRoot/PopoverInteractions), trigger
|
|
3
|
-
// reusing the Store + floating
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// root (context/PopoverRoot/PopoverInteractions), trigger, and the anchored open path
|
|
3
|
+
// (Portal/Positioner/Popup/Arrow/Backdrop/Title/Description/Close), reusing the Store + floating
|
|
4
|
+
// stack from Dialog. octane adaptations: forwardRef → ref-as-prop; native events; every Store
|
|
5
|
+
// hook-method + hook threads an explicit slot; `React.createContext` → octane `createContext`;
|
|
6
|
+
// `ReactDOM.flushSync` → octane `flushSync`.
|
|
6
7
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// with `useAnchorPositioning`. Interactions are only mounted when `open || mounted`, and — as with
|
|
10
|
-
// Dialog — a stable no-DOM descriptor wraps the children (octane Provider children shape-flip bug).
|
|
8
|
+
// `openOnHover` is fully wired: the trigger side runs `useHoverReferenceInteraction` (+ safePolygon)
|
|
9
|
+
// and the popup side runs `useHoverFloatingInteraction`.
|
|
11
10
|
import {
|
|
12
11
|
createContext,
|
|
13
12
|
createElement,
|
|
13
|
+
Fragment,
|
|
14
14
|
useContext,
|
|
15
15
|
useMemo,
|
|
16
16
|
useRef,
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
type UseAnchorPositioningSharedParameters,
|
|
52
52
|
} from './utils/useAnchorPositioning';
|
|
53
53
|
import { usePositioner } from './utils/usePositioner';
|
|
54
|
+
import { usePopupViewport } from './utils/usePopupViewport';
|
|
54
55
|
import { useAnchoredPopupScrollLock } from './utils/useAnchoredPopupScrollLock';
|
|
55
56
|
import { adaptiveOrigin } from './utils/adaptiveOriginMiddleware';
|
|
56
57
|
import { InternalBackdrop } from './utils/InternalBackdrop';
|
|
@@ -415,11 +416,7 @@ function PopoverInteractions(props: any): any {
|
|
|
415
416
|
subSlot(slot, 'pip'),
|
|
416
417
|
);
|
|
417
418
|
|
|
418
|
-
return
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function PopoverChildren(props: any): any {
|
|
422
|
-
return props.children ?? null;
|
|
419
|
+
return null;
|
|
423
420
|
}
|
|
424
421
|
|
|
425
422
|
// --- Root --------------------------------------------------------------------
|
|
@@ -519,12 +516,16 @@ function PopoverRootComponent<Payload>(props: any): any {
|
|
|
519
516
|
const resolvedChildren =
|
|
520
517
|
typeof children === 'function' && !isChildrenBlock(children) ? children({ payload }) : children;
|
|
521
518
|
|
|
522
|
-
//
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
519
|
+
// `PopoverInteractions` is a headless SIBLING of the children, exactly as Base UI renders it, so
|
|
520
|
+
// toggling it never disturbs them. Rendering it as a WRAPPER instead swapped the wrapper's
|
|
521
|
+
// component type on every open, which tore down and rebuilt the whole subtree — including the
|
|
522
|
+
// trigger, whose element listeners and store registration then pointed at a detached node.
|
|
523
|
+
const content = [
|
|
524
|
+
shouldRenderInteractions
|
|
525
|
+
? createElement(PopoverInteractions, { key: 'interactions', store, modal })
|
|
526
|
+
: null,
|
|
527
|
+
createElement(Fragment, { key: 'children', children: resolvedChildren }),
|
|
528
|
+
];
|
|
528
529
|
|
|
529
530
|
return createElement(PopoverRootContext.Provider, {
|
|
530
531
|
value: contextValue as PopoverRootContextValue,
|
|
@@ -680,18 +681,33 @@ function PopoverTrigger(componentProps: any): any {
|
|
|
680
681
|
subSlot(slot, 're'),
|
|
681
682
|
);
|
|
682
683
|
|
|
684
|
+
// The trigger's children shape flips between a lone element (closed) and
|
|
685
|
+
// [guard, element, guard] (open, non-modal). Base UI keeps the trigger's identity across that
|
|
686
|
+
// flip by wrapping it in a fragment keyed on the trigger id in BOTH branches — without the key
|
|
687
|
+
// the button is torn down and rebuilt on every open, which detaches the element listeners
|
|
688
|
+
// (hover open/close) and makes the positioner see a spurious trigger change.
|
|
689
|
+
const keyedElement = createElement(Fragment, { key: thisTriggerId, children: element });
|
|
690
|
+
|
|
683
691
|
if (isMountedByThisTrigger && !focusManagerModal) {
|
|
692
|
+
// Base UI renders these as static JSX fragment children; octane reconciles a returned array
|
|
693
|
+
// as a list, so the guards carry stable keys too — otherwise the unkeyed siblings shift the
|
|
694
|
+
// keyed trigger and it is rebuilt anyway.
|
|
684
695
|
return [
|
|
685
|
-
createElement(FocusGuard, { ref: preFocusGuardRef, onFocus: handlePreFocusGuardFocus }),
|
|
686
|
-
element,
|
|
687
696
|
createElement(FocusGuard, {
|
|
697
|
+
key: `${thisTriggerId}-pre-guard`,
|
|
698
|
+
ref: preFocusGuardRef,
|
|
699
|
+
onFocus: handlePreFocusGuardFocus,
|
|
700
|
+
}),
|
|
701
|
+
keyedElement,
|
|
702
|
+
createElement(FocusGuard, {
|
|
703
|
+
key: `${thisTriggerId}-post-guard`,
|
|
688
704
|
ref: store.context.triggerFocusTargetRef,
|
|
689
705
|
onFocus: handleFocusTargetFocus,
|
|
690
706
|
}),
|
|
691
707
|
];
|
|
692
708
|
}
|
|
693
709
|
|
|
694
|
-
return
|
|
710
|
+
return keyedElement;
|
|
695
711
|
}
|
|
696
712
|
|
|
697
713
|
// --- Portal ------------------------------------------------------------------
|
|
@@ -1188,6 +1204,58 @@ function PopoverClose(componentProps: any): any {
|
|
|
1188
1204
|
);
|
|
1189
1205
|
}
|
|
1190
1206
|
|
|
1207
|
+
// --- Viewport ----------------------------------------------------------------
|
|
1208
|
+
|
|
1209
|
+
export const PopoverViewportCssVars = {
|
|
1210
|
+
/**
|
|
1211
|
+
* The width of the parent popup when the previous content was rendered. Placed on the
|
|
1212
|
+
* 'previous' container so the popup's dimensions can be frozen while content animates.
|
|
1213
|
+
*/
|
|
1214
|
+
popupWidth: '--popup-width',
|
|
1215
|
+
/** The height of the parent popup when the previous content was rendered. */
|
|
1216
|
+
popupHeight: '--popup-height',
|
|
1217
|
+
} as const;
|
|
1218
|
+
|
|
1219
|
+
const viewportStateAttributesMapping: StateAttributesMapping<any> = {
|
|
1220
|
+
activationDirection: (value: string | undefined) =>
|
|
1221
|
+
value ? { 'data-activation-direction': value } : null,
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
// A viewport for displaying content transitions. Only needed when one popup can be opened by
|
|
1225
|
+
// multiple triggers, its content changes per trigger, and switching between them is animated.
|
|
1226
|
+
function PopoverViewport(componentProps: any): any {
|
|
1227
|
+
const slot = S('PopoverViewport');
|
|
1228
|
+
const { render, className, style, children, ref, ...elementProps } = componentProps;
|
|
1229
|
+
|
|
1230
|
+
const { store } = usePopoverRootContext() as PopoverRootContextValue;
|
|
1231
|
+
const { side } = usePopoverPositionerContext();
|
|
1232
|
+
|
|
1233
|
+
const instantType = store.useState('instantType', subSlot(slot, 'instant'));
|
|
1234
|
+
|
|
1235
|
+
const { children: childrenToRender, state: viewportState } = usePopupViewport(
|
|
1236
|
+
{ store, side, cssVars: PopoverViewportCssVars, children },
|
|
1237
|
+
subSlot(slot, 'vp'),
|
|
1238
|
+
);
|
|
1239
|
+
|
|
1240
|
+
const state = {
|
|
1241
|
+
activationDirection: viewportState.activationDirection,
|
|
1242
|
+
transitioning: viewportState.transitioning,
|
|
1243
|
+
instant: instantType,
|
|
1244
|
+
};
|
|
1245
|
+
|
|
1246
|
+
return useRenderElement(
|
|
1247
|
+
'div',
|
|
1248
|
+
{ render, className, style },
|
|
1249
|
+
{
|
|
1250
|
+
state,
|
|
1251
|
+
ref,
|
|
1252
|
+
props: [elementProps, { children: childrenToRender }],
|
|
1253
|
+
stateAttributesMapping: viewportStateAttributesMapping,
|
|
1254
|
+
},
|
|
1255
|
+
subSlot(slot, 're'),
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1191
1259
|
// --- Namespace ---------------------------------------------------------------
|
|
1192
1260
|
|
|
1193
1261
|
export const Popover = {
|
|
@@ -1196,10 +1264,12 @@ export const Popover = {
|
|
|
1196
1264
|
Portal: PopoverPortal,
|
|
1197
1265
|
Positioner: PopoverPositioner,
|
|
1198
1266
|
Popup: PopoverPopup,
|
|
1267
|
+
Viewport: PopoverViewport,
|
|
1199
1268
|
Arrow: PopoverArrow,
|
|
1200
1269
|
Backdrop: PopoverBackdrop,
|
|
1201
1270
|
Title: PopoverTitle,
|
|
1202
1271
|
Description: PopoverDescription,
|
|
1203
1272
|
Close: PopoverClose,
|
|
1204
1273
|
createHandle: createPopoverHandle,
|
|
1274
|
+
Handle: PopoverHandle,
|
|
1205
1275
|
};
|