@khanacademy/wonder-blocks-core 12.4.3 → 12.5.0

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,5 +1,18 @@
1
1
  # @khanacademy/wonder-blocks-core
2
2
 
3
+ ## 12.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 63dcc94: Move `useDirectionDetection` from `wonder-blocks-modal` (internal-only) to `wonder-blocks-core` and export it publicly, so other packages can detect the nearest ancestor's writing direction (RTL/LTR) without depending on `wonder-blocks-modal`. `wonder-blocks-modal`'s own usage (`DrawerDialog`) now imports it from `wonder-blocks-core`; no change to `wonder-blocks-modal`'s public API.
8
+
9
+ ## 12.4.4
10
+
11
+ ### Patch Changes
12
+
13
+ - 40cb70f: Add `require-logical-properties-for-rtl` ESLint rule to `eslint-plugin-wonder-blocks` recommended config, and migrate all Wonder Blocks component source files to use CSS logical properties for improved RTL layout support.
14
+ - 40cb70f: Enable RTL logical-properties ESLint rule in recommended config
15
+
3
16
  ## 12.4.3
4
17
 
5
18
  ### Patch Changes
package/dist/es/index.js CHANGED
@@ -5,11 +5,11 @@ import { StyleSheet, css } from 'aphrodite';
5
5
 
6
6
  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)}}
7
7
 
8
- const isHeaderRegex=/^h[1-6]$/;const styles$1=StyleSheet.create({text:{WebkitFontSmoothing:"antialiased",MozOsxFontSmoothing:"grayscale"},header:{marginTop:0,marginBottom:0}});const Text=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})});
8
+ const isHeaderRegex=/^h[1-6]$/;const styles$1=StyleSheet.create({text:{WebkitFontSmoothing:"antialiased",MozOsxFontSmoothing:"grayscale"},header:{marginBlockStart:0,marginBlockEnd:0}});const Text=React.forwardRef(function Text({children,style,tag="span",testId,...otherProps},ref){const Tag=tag;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})});
9
9
 
10
10
  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}}});
11
11
 
12
- const styles=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}});const View=React.forwardRef(function View(props,ref){const{testId,tag="div",...restProps}=props;const commonProps={...restProps,"data-testid":testId};const StyledTag=useMemo(()=>addStyle(tag,styles.default),[tag]);return jsx(StyledTag,{...commonProps,ref:ref})});
12
+ const styles=StyleSheet.create({default:{alignItems:"stretch",borderWidth:0,borderStyle:"solid",boxSizing:"border-box",display:"flex",flexDirection:"column",margin:0,padding:0,position:"relative",zIndex:0,minBlockSize:0,minInlineSize:0}});const View=React.forwardRef(function View(props,ref){const{testId,tag="div",...restProps}=props;const commonProps={...restProps,"data-testid":testId};const StyledTag=useMemo(()=>addStyle(tag,styles.default),[tag]);return jsx(StyledTag,{...commonProps,ref:ref})});
13
13
 
14
14
  var RenderState=/*#__PURE__*/function(RenderState){RenderState["Initial"]="initial";RenderState["Standard"]="standard";return RenderState}({});var RenderStateInternal=/*#__PURE__*/function(RenderStateInternal){RenderStateInternal["Root"]="root";RenderStateInternal["Initial"]="initial";RenderStateInternal["Standard"]="standard";return RenderStateInternal}({});const RenderStateContext=React.createContext("root");RenderStateContext.displayName="RenderStateContext";
15
15
 
@@ -19,6 +19,8 @@ let serverSide=false;var Server = {isServerSide:()=>serverSide,setServerSide:()=
19
19
 
20
20
  const Id=({id,children})=>{const generatedId=useId();return jsx(Fragment,{children:children(id??generatedId)})};
21
21
 
22
+ function useDirectionDetection(elementRef,options={}){const{direction:explicitDirection,defaultDirection="ltr"}=options;if(explicitDirection){return explicitDirection}if(elementRef?.current){const elementWithDir=elementRef.current.closest("[dir]");if(elementWithDir){const dirValue=elementWithDir.getAttribute("dir");return dirValue==="rtl"||dirValue==="ltr"?dirValue:defaultDirection}}const documentDir=document.documentElement.getAttribute("dir")||document.body.getAttribute("dir");if(documentDir==="rtl"||documentDir==="ltr"){return documentDir}return defaultDirection}
23
+
22
24
  const useForceUpdate=()=>{const[,setUpdateState]=React.useState({});const forceUpdate=React.useCallback(()=>{setUpdateState({});},[]);return forceUpdate};
23
25
 
24
26
  const useIsMounted=()=>{const isMountedRef=React.useRef(false);const isMounted=React.useCallback(()=>isMountedRef.current,[]);React.useEffect(()=>{isMountedRef.current=true;return ()=>{isMountedRef.current=false;}},[]);return isMounted};
@@ -39,4 +41,4 @@ const keys={enter:"Enter",escape:"Escape",tab:"Tab",space:" ",up:"ArrowUp",down:
39
41
 
40
42
  const FOCUSABLE_ELEMENTS='button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';function findFocusableNodes(root){return Array.from(root.querySelectorAll(FOCUSABLE_ELEMENTS))}
41
43
 
42
- export { Id, InitialFallback, RenderState, RenderStateRoot, Server, Text, View, addStyle, findFocusableNodes, keys, useForceUpdate, useIsMounted, useLatestRef, useOnMountEffect, useOnline, usePreHydrationEffect, useRenderState };
44
+ export { Id, InitialFallback, RenderState, RenderStateRoot, Server, Text, View, addStyle, findFocusableNodes, keys, useDirectionDetection, useForceUpdate, useIsMounted, useLatestRef, useOnMountEffect, useOnline, usePreHydrationEffect, useRenderState };
@@ -0,0 +1,58 @@
1
+ import * as React from "react";
2
+ type Direction = "ltr" | "rtl";
3
+ interface DirectionDetectionOptions {
4
+ /**
5
+ * Explicit direction to use, bypassing DOM detection.
6
+ * Useful when direction is known from external sources like RequestInfo.
7
+ */
8
+ direction?: Direction;
9
+ /**
10
+ * The default direction to use if no dir attribute is found.
11
+ * Defaults to "ltr".
12
+ */
13
+ defaultDirection?: Direction;
14
+ }
15
+ /**
16
+ * Hook for detecting the text direction (LTR/RTL). Supports both element-specific
17
+ * and document-level direction detection.
18
+ *
19
+ * This hook performs DOM queries on each render and does not use state or
20
+ * observers, making it stable and predictable.
21
+ *
22
+ * @param elementRef - Optional ref to the element to start searching from.
23
+ * If provided, searches up the DOM tree from this element.
24
+ * If not provided, uses document-level detection.
25
+ * @param options - Configuration options for direction detection
26
+ * @returns The detected direction ("ltr" or "rtl")
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * // Element-specific detection - searches up DOM tree from the element
31
+ * const MyComponent = () => {
32
+ * const ref = React.useRef<HTMLDivElement>(null);
33
+ * const direction = useDirectionDetection(ref);
34
+ *
35
+ * return (
36
+ * <div ref={ref}>
37
+ * Direction is: {direction}
38
+ * </div>
39
+ * );
40
+ * };
41
+ *
42
+ * // Document-level detection - uses document.documentElement.dir
43
+ * const MyOtherComponent = () => {
44
+ * const direction = useDirectionDetection();
45
+ * return <div>Page direction is: {direction}</div>;
46
+ * };
47
+ *
48
+ * // With explicit direction (e.g., from RequestInfo)
49
+ * const MyRTLComponent = ({requestInfo}) => {
50
+ * const direction = useDirectionDetection(undefined, {
51
+ * direction: requestInfo.isRTL ? "rtl" : "ltr"
52
+ * });
53
+ * return <div>Explicit direction: {direction}</div>;
54
+ * };
55
+ * ```
56
+ */
57
+ export declare function useDirectionDetection(elementRef?: React.RefObject<HTMLElement | null>, options?: DirectionDetectionOptions): Direction;
58
+ export {};
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export { default as InitialFallback } from "./components/initial-fallback";
4
4
  export { default as addStyle } from "./util/add-style";
5
5
  export { default as Server } from "./util/server";
6
6
  export { Id } from "./components/id";
7
+ export { useDirectionDetection } from "./hooks/use-direction-detection";
7
8
  export { useForceUpdate } from "./hooks/use-force-update";
8
9
  export { useIsMounted } from "./hooks/use-is-mounted";
9
10
  export { useLatestRef } from "./hooks/use-latest-ref";
package/dist/index.js CHANGED
@@ -28,11 +28,11 @@ var React__namespace = /*#__PURE__*/_interopNamespace(React);
28
28
 
29
29
  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)}}
30
30
 
31
- const isHeaderRegex=/^h[1-6]$/;const styles$1=aphrodite.StyleSheet.create({text:{WebkitFontSmoothing:"antialiased",MozOsxFontSmoothing:"grayscale"},header:{marginTop:0,marginBottom:0}});const Text=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})});
31
+ const isHeaderRegex=/^h[1-6]$/;const styles$1=aphrodite.StyleSheet.create({text:{WebkitFontSmoothing:"antialiased",MozOsxFontSmoothing:"grayscale"},header:{marginBlockStart:0,marginBlockEnd:0}});const Text=React__namespace.forwardRef(function Text({children,style,tag="span",testId,...otherProps},ref){const Tag=tag;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})});
32
32
 
33
33
  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}}});
34
34
 
35
- const styles=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}});const View=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.default),[tag]);return jsxRuntime.jsx(StyledTag,{...commonProps,ref:ref})});
35
+ const styles=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,minBlockSize:0,minInlineSize:0}});const View=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.default),[tag]);return jsxRuntime.jsx(StyledTag,{...commonProps,ref:ref})});
36
36
 
37
37
  var RenderState=/*#__PURE__*/function(RenderState){RenderState["Initial"]="initial";RenderState["Standard"]="standard";return RenderState}({});var RenderStateInternal=/*#__PURE__*/function(RenderStateInternal){RenderStateInternal["Root"]="root";RenderStateInternal["Initial"]="initial";RenderStateInternal["Standard"]="standard";return RenderStateInternal}({});const RenderStateContext=React__namespace.createContext("root");RenderStateContext.displayName="RenderStateContext";
38
38
 
@@ -42,6 +42,8 @@ let serverSide=false;var Server = {isServerSide:()=>serverSide,setServerSide:()=
42
42
 
43
43
  const Id=({id,children})=>{const generatedId=React.useId();return jsxRuntime.jsx(jsxRuntime.Fragment,{children:children(id??generatedId)})};
44
44
 
45
+ function useDirectionDetection(elementRef,options={}){const{direction:explicitDirection,defaultDirection="ltr"}=options;if(explicitDirection){return explicitDirection}if(elementRef?.current){const elementWithDir=elementRef.current.closest("[dir]");if(elementWithDir){const dirValue=elementWithDir.getAttribute("dir");return dirValue==="rtl"||dirValue==="ltr"?dirValue:defaultDirection}}const documentDir=document.documentElement.getAttribute("dir")||document.body.getAttribute("dir");if(documentDir==="rtl"||documentDir==="ltr"){return documentDir}return defaultDirection}
46
+
45
47
  const useForceUpdate=()=>{const[,setUpdateState]=React__namespace.useState({});const forceUpdate=React__namespace.useCallback(()=>{setUpdateState({});},[]);return forceUpdate};
46
48
 
47
49
  const useIsMounted=()=>{const isMountedRef=React__namespace.useRef(false);const isMounted=React__namespace.useCallback(()=>isMountedRef.current,[]);React__namespace.useEffect(()=>{isMountedRef.current=true;return ()=>{isMountedRef.current=false;}},[]);return isMounted};
@@ -72,6 +74,7 @@ exports.View = View;
72
74
  exports.addStyle = addStyle;
73
75
  exports.findFocusableNodes = findFocusableNodes;
74
76
  exports.keys = keys;
77
+ exports.useDirectionDetection = useDirectionDetection;
75
78
  exports.useForceUpdate = useForceUpdate;
76
79
  exports.useIsMounted = useIsMounted;
77
80
  exports.useLatestRef = useLatestRef;
@@ -79,7 +79,15 @@ export type TextViewSharedProps = {
79
79
  tabIndex?: number;
80
80
  id?: string;
81
81
  title?: string;
82
+ /**
83
+ * Should be ignored
84
+ * @ignore
85
+ */
82
86
  "data-modal-launcher-portal"?: boolean;
87
+ /**
88
+ * Should be ignored
89
+ * @ignore
90
+ */
83
91
  "data-placement"?: string;
84
92
  } & AriaProps & EventHandlers;
85
93
  export * from "./types.propsfor";
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "",
4
4
  "author": "Khan Academy",
5
5
  "license": "MIT",
6
- "version": "12.4.3",
6
+ "version": "12.5.0",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "devDependencies": {
32
32
  "@khanacademy/wb-dev-build-settings": "3.3.0",
33
- "@khanacademy/wonder-blocks-testing-core": "5.0.0"
33
+ "@khanacademy/wonder-blocks-testing-core": "5.2.0"
34
34
  },
35
35
  "scripts": {
36
36
  "test": "echo \"Error: no test specified\" && exit 1"