@khanacademy/wonder-blocks-floating 0.0.0-PR2846-20251119173805 → 0.0.0-PR2914-20251219160806

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-PR2846-20251119173805
3
+ ## 0.0.0-PR2914-20251219160806
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
- - 0bf70fc: Adds focus management support
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 declare const Arrow: React.ForwardRefExoticComponent<{
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
- } & React.RefAttributes<SVGSVGElement>>;
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 style=context.placement.endsWith("top")?{filter:`drop-shadow(0 4px 2px ${semanticColor.core.shadow.transparent.mid})`}:undefined;return jsx(FloatingArrow,{ref:ref,context:context,fill:semanticColor.core.background.base.default,stroke:semanticColor.core.border.neutral.subtle,strokeWidth:1,width:ARROW_SIZE_INLINE,height:ARROW_SIZE_BLOCK,style:style})});
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 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}){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]});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,{visibility:middlewareData.hide?.referenceHidden?"hidden":"visible"}],...getFloatingProps(),children:[content,showArrow&&jsx(Arrow,{ref:arrowRef,context:context})]})})})]})}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"}});
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 style=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:wonderBlocksTokens.semanticColor.core.background.base.default,stroke:wonderBlocksTokens.semanticColor.core.border.neutral.subtle,strokeWidth:1,width:ARROW_SIZE_INLINE,height:ARROW_SIZE_BLOCK,style:style})});
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 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}){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]});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,{visibility:middlewareData.hide?.referenceHidden?"hidden":"visible"}],...getFloatingProps(),children:[content,showArrow&&jsxRuntime.jsx(Arrow,{ref:arrowRef,context:context})]})})})]})}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"}});
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-PR2846-20251119173805",
6
+ "version": "0.0.0-PR2914-20251219160806",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -20,14 +20,14 @@
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.2"
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/wonder-blocks-modal": "8.5.5",
30
+ "@khanacademy/wonder-blocks-modal": "8.5.10",
31
31
  "@khanacademy/wb-dev-build-settings": "3.2.0"
32
32
  },
33
33
  "scripts": {