@khanacademy/wonder-blocks-floating 0.0.0-PR2846-20251120175615 → 0.0.0-PR2914-20260109164252
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/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# @khanacademy/wonder-blocks-floating
|
|
2
2
|
|
|
3
|
-
## 0.0.0-
|
|
3
|
+
## 0.0.0-PR2914-20260109164252
|
|
4
4
|
|
|
5
5
|
### Minor Changes
|
|
6
6
|
|
|
7
|
+
- 936901e: Adds styles prop to customize the look and feel of the floating element
|
|
7
8
|
- 7cc5c0d: Adds wonder-blocks-floating package
|
|
8
|
-
-
|
|
9
|
+
- 2c6f82c: Adds focus management support
|
|
10
|
+
- f3c5eed: Adds right-to-left support to Floating component.
|
|
9
11
|
- b286374: Adds Floating component with basic props (including middlewares)
|
|
10
12
|
- fad599f: Adds `portal` prop to Floating component. Includes `maybeGetPortalMountedModalHostElement` util to portal floating elements inside modals.
|
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { FloatingContext } from "@floating-ui/react";
|
|
3
|
-
export
|
|
3
|
+
export type ArrowStyles = {
|
|
4
|
+
/**
|
|
5
|
+
* The fill color of the arrow.
|
|
6
|
+
* @default semanticColor.core.background.base.default
|
|
7
|
+
*/
|
|
8
|
+
fill?: string;
|
|
9
|
+
/**
|
|
10
|
+
* The stroke color of the arrow.
|
|
11
|
+
* @default semanticColor.core.border.neutral.subtle
|
|
12
|
+
*/
|
|
13
|
+
stroke?: string;
|
|
14
|
+
/**
|
|
15
|
+
* The width of the stroke (border) of the arrow.
|
|
16
|
+
* @default 1
|
|
17
|
+
*
|
|
18
|
+
* NOTE: Needs to be a number value (in pixels).
|
|
19
|
+
*/
|
|
20
|
+
strokeWidth?: number;
|
|
21
|
+
/**
|
|
22
|
+
* The width of the arrow.
|
|
23
|
+
* @default 20
|
|
24
|
+
*
|
|
25
|
+
* NOTE: Needs to be a number value (in pixels).
|
|
26
|
+
*/
|
|
27
|
+
width?: number;
|
|
28
|
+
/**
|
|
29
|
+
* The height of the arrow.
|
|
30
|
+
* @default 10
|
|
31
|
+
*
|
|
32
|
+
* NOTE: Needs to be a number value (in pixels).
|
|
33
|
+
*/
|
|
34
|
+
height?: number;
|
|
35
|
+
};
|
|
36
|
+
type Props = {
|
|
37
|
+
/**
|
|
38
|
+
* The context of the floating element.
|
|
39
|
+
*/
|
|
4
40
|
context: FloatingContext;
|
|
5
|
-
|
|
41
|
+
/**
|
|
42
|
+
* The styles to use for the arrow.
|
|
43
|
+
*/
|
|
44
|
+
style?: ArrowStyles;
|
|
45
|
+
};
|
|
46
|
+
export declare const Arrow: React.ForwardRefExoticComponent<Props & React.RefAttributes<SVGSVGElement>>;
|
|
47
|
+
export {};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { Placement } from "@floating-ui/react";
|
|
3
|
+
import { StyleType } from "@khanacademy/wonder-blocks-core";
|
|
4
|
+
import { type ArrowStyles } from "./floating-arrow";
|
|
3
5
|
type FloatingProps = {
|
|
4
6
|
/**
|
|
5
7
|
* The reference (or anchored) element that is used to calculate the
|
|
@@ -31,6 +33,16 @@ type FloatingProps = {
|
|
|
31
33
|
* Callback for when the floating element is opened or closed.
|
|
32
34
|
*/
|
|
33
35
|
onOpenChange?: (open: boolean) => void;
|
|
36
|
+
/**
|
|
37
|
+
* The styles to use for the floating element.
|
|
38
|
+
*
|
|
39
|
+
* - `root`: The styles to use for the floating element.
|
|
40
|
+
* - `arrow`: The styles to use for the arrow of the floating element.
|
|
41
|
+
*/
|
|
42
|
+
styles?: {
|
|
43
|
+
root?: StyleType;
|
|
44
|
+
arrow?: ArrowStyles;
|
|
45
|
+
};
|
|
34
46
|
/**
|
|
35
47
|
* The test ID to use for the floating element.
|
|
36
48
|
*/
|
|
@@ -137,5 +149,5 @@ type Props = FloatingProps & FocusManagerProps;
|
|
|
137
149
|
* </Floating>
|
|
138
150
|
* ```
|
|
139
151
|
*/
|
|
140
|
-
export default function Floating({ content, children, placement, open, onOpenChange, portal, strategy, testId, focusManagerEnabled, initialFocusRef, dismissEnabled, hide: hideProp, offset: offsetProp, flip: flipProp, shift: shiftProp, showArrow, }: Props): React.JSX.Element;
|
|
152
|
+
export default function Floating({ content, children, placement, open, onOpenChange, portal, strategy, testId, focusManagerEnabled, initialFocusRef, dismissEnabled, hide: hideProp, offset: offsetProp, flip: flipProp, shift: shiftProp, showArrow, styles: stylesProp, }: Props): React.JSX.Element;
|
|
141
153
|
export {};
|
package/dist/es/index.js
CHANGED
|
@@ -9,12 +9,14 @@ function flatten(list){const result=[];if(!list){return result}else if(Array.isA
|
|
|
9
9
|
|
|
10
10
|
const ARROW_SIZE_INLINE=20;const ARROW_SIZE_BLOCK=10;const ModalLauncherPortalAttributeName="data-modal-launcher-portal";
|
|
11
11
|
|
|
12
|
-
const Arrow=React.forwardRef(function Arrow({context},ref){const
|
|
12
|
+
const Arrow=React.forwardRef(function Arrow({context,style},ref){const shadowStyle=context.placement.endsWith("top")?{filter:`drop-shadow(0 4px 2px ${semanticColor.core.shadow.transparent.mid})`}:undefined;return jsx(FloatingArrow,{ref:ref,context:context,fill:style?.fill??semanticColor.core.background.base.default,stroke:style?.stroke??semanticColor.core.border.neutral.subtle,strokeWidth:style?.strokeWidth??1,width:style?.width??ARROW_SIZE_INLINE,height:style?.height??ARROW_SIZE_BLOCK,style:shadowStyle})});
|
|
13
13
|
|
|
14
14
|
function maybeGetNextAncestorModalLauncherPortal(element){let candidateElement=element&&element.parentElement;while(candidateElement&&!candidateElement.hasAttribute(ModalLauncherPortalAttributeName)){candidateElement=candidateElement.parentElement;}return candidateElement}function maybeGetPortalMountedModalHostElement(element){return maybeGetNextAncestorModalLauncherPortal(element)}
|
|
15
15
|
|
|
16
16
|
function Portal({portal,children,reference}){if(portal){const modalHost=maybeGetPortalMountedModalHostElement(reference)||document.body;return jsx(FloatingPortal,{root:modalHost,children:children})}return children}
|
|
17
17
|
|
|
18
|
-
const
|
|
18
|
+
const rtlMirror=()=>{return {name:"rtlMirror",fn(state){const{placement,elements:{reference}}=state;const isRtl=reference&&!!reference.closest("[dir='rtl']");if(!isRtl||!placement.startsWith("left")&&!placement.startsWith("right")){return {}}let nextPlacement;if(placement.startsWith("left")){nextPlacement=placement.replace("left","right");}else {nextPlacement=placement.replace("right","left");}return {reset:{placement:nextPlacement}}}}};
|
|
19
|
+
|
|
20
|
+
const StyledDiv=addStyle("div");const SHIFT_PADDING=12;function Floating({content,children,placement="top",open=false,onOpenChange,portal=true,strategy="absolute",testId,focusManagerEnabled=true,initialFocusRef,dismissEnabled=false,hide:hideProp=true,offset:offsetProp=20,flip:flipProp=true,shift:shiftProp=true,showArrow=true,styles:stylesProp}){const arrowRef=React.useRef(null);const prevOpenRef=React.useRef(open??false);const{elements,refs,floatingStyles,context,middlewareData}=useFloating({open,onOpenChange,placement,strategy,whileElementsMounted:autoUpdate,middleware:[offset({mainAxis:offsetProp}),flipProp?flip():undefined,shiftProp?shift({padding:SHIFT_PADDING,crossAxis:true}):undefined,showArrow?arrow({element:arrowRef}):undefined,hideProp?hide():undefined,rtlMirror()]});const dismiss=useDismiss(context,{enabled:dismissEnabled});const{getReferenceProps,getFloatingProps}=useInteractions([dismiss]);React.useEffect(()=>{if(prevOpenRef.current!==open){onOpenChange?.(open);prevOpenRef.current=open;}},[onOpenChange,open]);const trigger=React.useMemo(()=>{return React.cloneElement(children,{ref:refs.setReference,...getReferenceProps()})},[children,refs.setReference,getReferenceProps]);return jsxs(Fragment,{children:[trigger,open&&elements.reference&&jsx(Portal,{portal:portal,reference:elements.reference,children:jsx(FloatingFocusManager,{disabled:!focusManagerEnabled,context:context,modal:false,initialFocus:initialFocusRef,closeOnFocusOut:false,visuallyHiddenDismiss:dismissEnabled,children:jsxs(StyledDiv,{"data-testid":testId,"data-placement":placement,ref:refs.setFloating,style:[styles.floating,floatingStyles,stylesProp?.root,{visibility:middlewareData.hide?.referenceHidden?"hidden":"visible"}],...getFloatingProps(),children:[content,showArrow&&jsx(Arrow,{ref:arrowRef,context:context,style:stylesProp?.arrow})]})})})]})}const styles=StyleSheet.create({floating:{background:semanticColor.core.background.base.default,border:`solid ${border.width.thin} ${semanticColor.core.border.neutral.subtle}`,borderRadius:border.radius.radius_040,maxInlineSize:472,minBlockSize:ARROW_SIZE_INLINE,boxShadow:boxShadow.mid,justifyContent:"center",outline:"none"}});
|
|
19
21
|
|
|
20
22
|
export { Floating };
|
package/dist/index.js
CHANGED
|
@@ -32,12 +32,14 @@ function flatten(list){const result=[];if(!list){return result}else if(Array.isA
|
|
|
32
32
|
|
|
33
33
|
const ARROW_SIZE_INLINE=20;const ARROW_SIZE_BLOCK=10;const ModalLauncherPortalAttributeName="data-modal-launcher-portal";
|
|
34
34
|
|
|
35
|
-
const Arrow=React__namespace.forwardRef(function Arrow({context},ref){const
|
|
35
|
+
const Arrow=React__namespace.forwardRef(function Arrow({context,style},ref){const shadowStyle=context.placement.endsWith("top")?{filter:`drop-shadow(0 4px 2px ${wonderBlocksTokens.semanticColor.core.shadow.transparent.mid})`}:undefined;return jsxRuntime.jsx(react.FloatingArrow,{ref:ref,context:context,fill:style?.fill??wonderBlocksTokens.semanticColor.core.background.base.default,stroke:style?.stroke??wonderBlocksTokens.semanticColor.core.border.neutral.subtle,strokeWidth:style?.strokeWidth??1,width:style?.width??ARROW_SIZE_INLINE,height:style?.height??ARROW_SIZE_BLOCK,style:shadowStyle})});
|
|
36
36
|
|
|
37
37
|
function maybeGetNextAncestorModalLauncherPortal(element){let candidateElement=element&&element.parentElement;while(candidateElement&&!candidateElement.hasAttribute(ModalLauncherPortalAttributeName)){candidateElement=candidateElement.parentElement;}return candidateElement}function maybeGetPortalMountedModalHostElement(element){return maybeGetNextAncestorModalLauncherPortal(element)}
|
|
38
38
|
|
|
39
39
|
function Portal({portal,children,reference}){if(portal){const modalHost=maybeGetPortalMountedModalHostElement(reference)||document.body;return jsxRuntime.jsx(react.FloatingPortal,{root:modalHost,children:children})}return children}
|
|
40
40
|
|
|
41
|
-
const
|
|
41
|
+
const rtlMirror=()=>{return {name:"rtlMirror",fn(state){const{placement,elements:{reference}}=state;const isRtl=reference&&!!reference.closest("[dir='rtl']");if(!isRtl||!placement.startsWith("left")&&!placement.startsWith("right")){return {}}let nextPlacement;if(placement.startsWith("left")){nextPlacement=placement.replace("left","right");}else {nextPlacement=placement.replace("right","left");}return {reset:{placement:nextPlacement}}}}};
|
|
42
|
+
|
|
43
|
+
const StyledDiv=addStyle("div");const SHIFT_PADDING=12;function Floating({content,children,placement="top",open=false,onOpenChange,portal=true,strategy="absolute",testId,focusManagerEnabled=true,initialFocusRef,dismissEnabled=false,hide:hideProp=true,offset:offsetProp=20,flip:flipProp=true,shift:shiftProp=true,showArrow=true,styles:stylesProp}){const arrowRef=React__namespace.useRef(null);const prevOpenRef=React__namespace.useRef(open??false);const{elements,refs,floatingStyles,context,middlewareData}=react.useFloating({open,onOpenChange,placement,strategy,whileElementsMounted:react.autoUpdate,middleware:[react.offset({mainAxis:offsetProp}),flipProp?react.flip():undefined,shiftProp?react.shift({padding:SHIFT_PADDING,crossAxis:true}):undefined,showArrow?react.arrow({element:arrowRef}):undefined,hideProp?react.hide():undefined,rtlMirror()]});const dismiss=react.useDismiss(context,{enabled:dismissEnabled});const{getReferenceProps,getFloatingProps}=react.useInteractions([dismiss]);React__namespace.useEffect(()=>{if(prevOpenRef.current!==open){onOpenChange?.(open);prevOpenRef.current=open;}},[onOpenChange,open]);const trigger=React__namespace.useMemo(()=>{return React__namespace.cloneElement(children,{ref:refs.setReference,...getReferenceProps()})},[children,refs.setReference,getReferenceProps]);return jsxRuntime.jsxs(jsxRuntime.Fragment,{children:[trigger,open&&elements.reference&&jsxRuntime.jsx(Portal,{portal:portal,reference:elements.reference,children:jsxRuntime.jsx(react.FloatingFocusManager,{disabled:!focusManagerEnabled,context:context,modal:false,initialFocus:initialFocusRef,closeOnFocusOut:false,visuallyHiddenDismiss:dismissEnabled,children:jsxRuntime.jsxs(StyledDiv,{"data-testid":testId,"data-placement":placement,ref:refs.setFloating,style:[styles.floating,floatingStyles,stylesProp?.root,{visibility:middlewareData.hide?.referenceHidden?"hidden":"visible"}],...getFloatingProps(),children:[content,showArrow&&jsxRuntime.jsx(Arrow,{ref:arrowRef,context:context,style:stylesProp?.arrow})]})})})]})}const styles=aphrodite.StyleSheet.create({floating:{background:wonderBlocksTokens.semanticColor.core.background.base.default,border:`solid ${wonderBlocksTokens.border.width.thin} ${wonderBlocksTokens.semanticColor.core.border.neutral.subtle}`,borderRadius:wonderBlocksTokens.border.radius.radius_040,maxInlineSize:472,minBlockSize:ARROW_SIZE_INLINE,boxShadow:wonderBlocksTokens.boxShadow.mid,justifyContent:"center",outline:"none"}});
|
|
42
44
|
|
|
43
45
|
exports.Floating = Floating;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Middleware } from "@floating-ui/react";
|
|
2
|
+
/**
|
|
3
|
+
* Custom middleware that mirrors the floating element placement when in RTL
|
|
4
|
+
* mode and placement is "left" or "right".
|
|
5
|
+
*
|
|
6
|
+
* This ensures the floating element appears in the correct logical position for
|
|
7
|
+
* RTL layouts.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const {placement, middlewareData} = useFloating({
|
|
12
|
+
* placement: "left",
|
|
13
|
+
* middleware: [
|
|
14
|
+
* rtlMirror()
|
|
15
|
+
* ]
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* // placement will be "right" when in RTL mode
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare const rtlMirror: () => Middleware;
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "A package that provides support for floating UI components in our UIs, including portal and focus management support.",
|
|
4
4
|
"author": "Khan Academy",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "0.0.0-
|
|
6
|
+
"version": "0.0.0-PR2914-20260109164252",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
@@ -20,15 +20,15 @@
|
|
|
20
20
|
"types": "dist/index.d.ts",
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@floating-ui/react": "^0.27.16",
|
|
23
|
-
"@khanacademy/wonder-blocks-tokens": "14.1.
|
|
23
|
+
"@khanacademy/wonder-blocks-tokens": "14.1.3"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"aphrodite": "^1.2.5",
|
|
27
27
|
"react": "18.2.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@khanacademy/
|
|
31
|
-
"@khanacademy/
|
|
30
|
+
"@khanacademy/wonder-blocks-modal": "8.5.11",
|
|
31
|
+
"@khanacademy/wb-dev-build-settings": "3.2.0"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"test": "echo \"Error: no test specified\" && exit 1"
|