@khanacademy/wonder-blocks-floating 0.0.0-PR2842-20251030173407 → 0.0.0-PR2844-20251110165628

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,8 +1,9 @@
1
1
  # @khanacademy/wonder-blocks-floating
2
2
 
3
- ## 0.0.0-PR2842-20251030173407
3
+ ## 0.0.0-PR2844-20251110165628
4
4
 
5
5
  ### Minor Changes
6
6
 
7
7
  - 7cc5c0d: Adds wonder-blocks-floating package
8
- - 669e99a: Adds Floating component with basic props (including middlewares)
8
+ - b286374: Adds Floating component with basic props (including middlewares)
9
+ - 19c49ac: Adds `portal` prop to Floating component. Includes `maybeGetPortalMountedModalHostElement` util to portal floating elements inside modals.
@@ -0,0 +1,24 @@
1
+ import * as React from "react";
2
+ type Props = {
3
+ /**
4
+ * Whether to render the floating element in a portal.
5
+ * @default true
6
+ */
7
+ portal: boolean;
8
+ /**
9
+ * The children to render in the portal.
10
+ */
11
+ children: React.JSX.Element;
12
+ /**
13
+ * The reference element to use for the portal.
14
+ */
15
+ reference?: Element | null | undefined;
16
+ };
17
+ /**
18
+ * Renders the floating element in a portal if enabled.
19
+ *
20
+ * Using a portal is recommended so it can prevent any potential clipping
21
+ * issues.
22
+ */
23
+ export declare function Portal({ portal, children, reference }: Props): React.JSX.Element;
24
+ export {};
@@ -67,13 +67,18 @@ type FloatingProps = {
67
67
  * @default true
68
68
  */
69
69
  showArrow?: boolean;
70
+ /**
71
+ * Whether to render the floating element in a portal.
72
+ * @default true
73
+ */
74
+ portal?: boolean;
70
75
  };
71
76
  /**
72
77
  * A component that uses the Floating UI library to position a floating element
73
78
  * relative to a reference element.
74
79
  *
75
80
  * Please take a look at the
76
- * [Accessibility](?path=/docs/packages-floating-floating-accessibility--docs)
81
+ * [Accessibility](?path=/docs/packages-floating-accessibility--docs)
77
82
  * section for more information.
78
83
  *
79
84
  * ## Usage
@@ -85,5 +90,5 @@ type FloatingProps = {
85
90
  * </Floating>
86
91
  * ```
87
92
  */
88
- export default function Floating({ content, children, placement, open, onOpenChange, strategy, testId, hide: hideProp, offset: offsetProp, flip: flipProp, shift: shiftProp, showArrow, }: FloatingProps): React.JSX.Element;
93
+ export default function Floating({ content, children, placement, open, onOpenChange, portal, strategy, testId, hide: hideProp, offset: offsetProp, flip: flipProp, shift: shiftProp, showArrow, }: FloatingProps): React.JSX.Element;
89
94
  export {};
package/dist/es/index.js CHANGED
@@ -1,16 +1,20 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
3
  import { useMemo } from 'react';
4
- import { FloatingArrow, useFloating, autoUpdate, offset, flip, shift, arrow, hide } from '@floating-ui/react';
4
+ import { FloatingArrow, FloatingPortal, useFloating, autoUpdate, offset, flip, shift, arrow, hide } from '@floating-ui/react';
5
5
  import { StyleSheet, css } from 'aphrodite';
6
6
  import { semanticColor, border, boxShadow } from '@khanacademy/wonder-blocks-tokens';
7
7
 
8
8
  function flatten(list){const result=[];if(!list){return result}else if(Array.isArray(list)){for(const item of list){result.push(...flatten(item));}}else {result.push(list);}return result}function processStyleList(style){const stylesheetStyles=[];const inlineStyles=[];if(!style){return {style:{},className:""}}const shouldInlineStyles=typeof global!=="undefined"&&global.SNAPSHOT_INLINE_APHRODITE;flatten(style).forEach(child=>{const _definition=child._definition;if(_definition!=null){if(shouldInlineStyles){const def={};for(const[key,value]of Object.entries(_definition)){def[key.replace(/-[a-z]/g,match=>match[1].toUpperCase())]=value;}inlineStyles.push(def);}else {stylesheetStyles.push(child);}}else {inlineStyles.push(child);}});const inlineStylesObject=Object.assign({},...inlineStyles);if(inlineStyles.length>0&&!shouldInlineStyles){const inlineStylesStyleSheet=StyleSheet.create({inlineStyles:inlineStylesObject});stylesheetStyles.push(inlineStylesStyleSheet.inlineStyles);}return {style:shouldInlineStyles?inlineStylesObject:{},className:css(...stylesheetStyles)}}const isHeaderRegex=/^h[1-6]$/;const styles$1=StyleSheet.create({text:{WebkitFontSmoothing:"antialiased",MozOsxFontSmoothing:"grayscale"},header:{marginTop:0,marginBottom:0}});React.forwardRef(function Text({children,style,tag:Tag="span",testId,...otherProps},ref){const isHeader=isHeaderRegex.test(Tag);const styleAttributes=processStyleList([styles$1.text,isHeader&&styles$1.header,style]);const classNames=otherProps.className?[otherProps.className,styleAttributes.className].join(" "):styleAttributes.className;return jsx(Tag,{...otherProps,style:styleAttributes.style,className:classNames,"data-testid":testId,ref:ref,children:children})});function addStyle(Component,defaultStyle){return React.forwardRef((props,ref)=>{const{className,style,...otherProps}=props;const reset=typeof Component==="string"?overrides[Component]:null;const{className:aphroditeClassName,style:inlineStyles}=processStyleList([reset,defaultStyle,style]);return jsx(Component,{...otherProps,ref:ref,className:[aphroditeClassName,className].filter(Boolean).join(" "),style:inlineStyles})})}const overrides=StyleSheet.create({button:{margin:0,"::-moz-focus-inner":{border:0}}});const styles$2=StyleSheet.create({default:{alignItems:"stretch",borderWidth:0,borderStyle:"solid",boxSizing:"border-box",display:"flex",flexDirection:"column",margin:0,padding:0,position:"relative",zIndex:0,minHeight:0,minWidth:0}});React.forwardRef(function View(props,ref){const{testId,tag="div",...restProps}=props;const commonProps={...restProps,"data-testid":testId};const StyledTag=useMemo(()=>addStyle(tag,styles$2.default),[tag]);return jsx(StyledTag,{...commonProps,ref:ref})});(function(RenderState){RenderState["Initial"]="initial";RenderState["Standard"]="standard";return RenderState})({});(function(RenderStateInternal){RenderStateInternal["Root"]="root";RenderStateInternal["Initial"]="initial";RenderStateInternal["Standard"]="standard";return RenderStateInternal})({});const RenderStateContext=React.createContext("root");RenderStateContext.displayName="RenderStateContext";
9
9
 
10
- const ARROW_SIZE_INLINE=20;const ARROW_SIZE_BLOCK=10;
10
+ const ARROW_SIZE_INLINE=20;const ARROW_SIZE_BLOCK=10;const ModalLauncherPortalAttributeName="data-modal-launcher-portal";
11
11
 
12
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})});
13
13
 
14
- const StyledDiv=addStyle("div");const SHIFT_PADDING=12;function Floating({content,children,placement="top",open=false,onOpenChange,strategy="absolute",testId,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{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]});React.useEffect(()=>{if(prevOpenRef.current!==open){onOpenChange?.(open);prevOpenRef.current=open;}},[onOpenChange,open]);const trigger=React.useMemo(()=>{return React.cloneElement(children,{ref:refs.setReference})},[children,refs.setReference]);return jsxs(Fragment,{children:[trigger,open&&jsxs(StyledDiv,{"data-testid":testId,"data-placement":placement,ref:refs.setFloating,style:[styles.floating,floatingStyles,{visibility:middlewareData.hide?.referenceHidden?"hidden":"visible"}],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"}});
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
+
16
+ function Portal({portal,children,reference}){if(portal){const modalHost=maybeGetPortalMountedModalHostElement(reference)||document.body;return jsx(FloatingPortal,{root:modalHost,children:children})}return children}
17
+
18
+ const StyledDiv=addStyle("div");const SHIFT_PADDING=12;function Floating({content,children,placement="top",open=false,onOpenChange,portal=true,strategy="absolute",testId,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]});React.useEffect(()=>{if(prevOpenRef.current!==open){onOpenChange?.(open);prevOpenRef.current=open;}},[onOpenChange,open]);const trigger=React.useMemo(()=>{return React.cloneElement(children,{ref:refs.setReference})},[children,refs.setReference]);return jsxs(Fragment,{children:[trigger,open&&elements.reference&&jsx(Portal,{portal:portal,reference:elements.reference,children:jsxs(StyledDiv,{"data-testid":testId,"data-placement":placement,ref:refs.setFloating,style:[styles.floating,floatingStyles,{visibility:middlewareData.hide?.referenceHidden?"hidden":"visible"}],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"}});
15
19
 
16
20
  export { Floating };
package/dist/index.js CHANGED
@@ -30,10 +30,14 @@ var React__namespace = /*#__PURE__*/_interopNamespace(React);
30
30
 
31
31
  function flatten(list){const result=[];if(!list){return result}else if(Array.isArray(list)){for(const item of list){result.push(...flatten(item));}}else {result.push(list);}return result}function processStyleList(style){const stylesheetStyles=[];const inlineStyles=[];if(!style){return {style:{},className:""}}const shouldInlineStyles=typeof global!=="undefined"&&global.SNAPSHOT_INLINE_APHRODITE;flatten(style).forEach(child=>{const _definition=child._definition;if(_definition!=null){if(shouldInlineStyles){const def={};for(const[key,value]of Object.entries(_definition)){def[key.replace(/-[a-z]/g,match=>match[1].toUpperCase())]=value;}inlineStyles.push(def);}else {stylesheetStyles.push(child);}}else {inlineStyles.push(child);}});const inlineStylesObject=Object.assign({},...inlineStyles);if(inlineStyles.length>0&&!shouldInlineStyles){const inlineStylesStyleSheet=aphrodite.StyleSheet.create({inlineStyles:inlineStylesObject});stylesheetStyles.push(inlineStylesStyleSheet.inlineStyles);}return {style:shouldInlineStyles?inlineStylesObject:{},className:aphrodite.css(...stylesheetStyles)}}const isHeaderRegex=/^h[1-6]$/;const styles$1=aphrodite.StyleSheet.create({text:{WebkitFontSmoothing:"antialiased",MozOsxFontSmoothing:"grayscale"},header:{marginTop:0,marginBottom:0}});React__namespace.forwardRef(function Text({children,style,tag:Tag="span",testId,...otherProps},ref){const isHeader=isHeaderRegex.test(Tag);const styleAttributes=processStyleList([styles$1.text,isHeader&&styles$1.header,style]);const classNames=otherProps.className?[otherProps.className,styleAttributes.className].join(" "):styleAttributes.className;return jsxRuntime.jsx(Tag,{...otherProps,style:styleAttributes.style,className:classNames,"data-testid":testId,ref:ref,children:children})});function addStyle(Component,defaultStyle){return React__namespace.forwardRef((props,ref)=>{const{className,style,...otherProps}=props;const reset=typeof Component==="string"?overrides[Component]:null;const{className:aphroditeClassName,style:inlineStyles}=processStyleList([reset,defaultStyle,style]);return jsxRuntime.jsx(Component,{...otherProps,ref:ref,className:[aphroditeClassName,className].filter(Boolean).join(" "),style:inlineStyles})})}const overrides=aphrodite.StyleSheet.create({button:{margin:0,"::-moz-focus-inner":{border:0}}});const styles$2=aphrodite.StyleSheet.create({default:{alignItems:"stretch",borderWidth:0,borderStyle:"solid",boxSizing:"border-box",display:"flex",flexDirection:"column",margin:0,padding:0,position:"relative",zIndex:0,minHeight:0,minWidth:0}});React__namespace.forwardRef(function View(props,ref){const{testId,tag="div",...restProps}=props;const commonProps={...restProps,"data-testid":testId};const StyledTag=React.useMemo(()=>addStyle(tag,styles$2.default),[tag]);return jsxRuntime.jsx(StyledTag,{...commonProps,ref:ref})});(function(RenderState){RenderState["Initial"]="initial";RenderState["Standard"]="standard";return RenderState})({});(function(RenderStateInternal){RenderStateInternal["Root"]="root";RenderStateInternal["Initial"]="initial";RenderStateInternal["Standard"]="standard";return RenderStateInternal})({});const RenderStateContext=React__namespace.createContext("root");RenderStateContext.displayName="RenderStateContext";
32
32
 
33
- const ARROW_SIZE_INLINE=20;const ARROW_SIZE_BLOCK=10;
33
+ const ARROW_SIZE_INLINE=20;const ARROW_SIZE_BLOCK=10;const ModalLauncherPortalAttributeName="data-modal-launcher-portal";
34
34
 
35
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})});
36
36
 
37
- const StyledDiv=addStyle("div");const SHIFT_PADDING=12;function Floating({content,children,placement="top",open=false,onOpenChange,strategy="absolute",testId,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{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]});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})},[children,refs.setReference]);return jsxRuntime.jsxs(jsxRuntime.Fragment,{children:[trigger,open&&jsxRuntime.jsxs(StyledDiv,{"data-testid":testId,"data-placement":placement,ref:refs.setFloating,style:[styles.floating,floatingStyles,{visibility:middlewareData.hide?.referenceHidden?"hidden":"visible"}],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"}});
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
+
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
+
41
+ const StyledDiv=addStyle("div");const SHIFT_PADDING=12;function Floating({content,children,placement="top",open=false,onOpenChange,portal=true,strategy="absolute",testId,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]});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})},[children,refs.setReference]);return jsxRuntime.jsxs(jsxRuntime.Fragment,{children:[trigger,open&&elements.reference&&jsxRuntime.jsx(Portal,{portal:portal,reference:elements.reference,children:jsxRuntime.jsxs(StyledDiv,{"data-testid":testId,"data-placement":placement,ref:refs.setFloating,style:[styles.floating,floatingStyles,{visibility:middlewareData.hide?.referenceHidden?"hidden":"visible"}],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"}});
38
42
 
39
43
  exports.Floating = Floating;
@@ -1,2 +1,9 @@
1
1
  export declare const ARROW_SIZE_INLINE = 20;
2
2
  export declare const ARROW_SIZE_BLOCK = 10;
3
+ /**
4
+ * The attribute used to identify a modal launcher portal.
5
+ *
6
+ * NOTE: This is the same as the ModalLauncherPortalAttributeName in the modal
7
+ * package. Make sure to update both when making changes.
8
+ */
9
+ export declare const ModalLauncherPortalAttributeName = "data-modal-launcher-portal";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * From a given element, finds the next modal host that has been mounted in
3
+ * a modal portal.
4
+ * @param {?(Element | Text)} element The element whose ancestors are to be
5
+ * walked.
6
+ * @returns {?Element} The next portal-mounted modal host element.
7
+ * TODO(kevinb): look into getting rid of this
8
+ */
9
+ export default function maybeGetPortalMountedModalHostElement(element?: Element | Text | null): Element | null | undefined;
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-PR2842-20251030173407",
6
+ "version": "0.0.0-PR2844-20251110165628",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -27,7 +27,8 @@
27
27
  "react": "18.2.0"
28
28
  },
29
29
  "devDependencies": {
30
- "@khanacademy/wb-dev-build-settings": "3.2.0"
30
+ "@khanacademy/wb-dev-build-settings": "3.2.0",
31
+ "@khanacademy/wonder-blocks-modal": "8.5.3"
31
32
  },
32
33
  "scripts": {
33
34
  "test": "echo \"Error: no test specified\" && exit 1"