@khanacademy/wonder-blocks-link 10.3.12 → 10.4.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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @khanacademy/wonder-blocks-link@10.3.12 build:css /home/runner/work/wonder-blocks/wonder-blocks/packages/wonder-blocks-link
2
+ > @khanacademy/wonder-blocks-link@10.4.0 build:css /home/runner/work/wonder-blocks/wonder-blocks/packages/wonder-blocks-link
3
3
  > pnpm exec wonder-blocks-tokens .
4
4
 
5
5
  CSS variables generated successfully in: /home/runner/work/wonder-blocks/wonder-blocks/packages/wonder-blocks-link/dist/css
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # @khanacademy/wonder-blocks-link
2
2
 
3
+ ## 10.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8ac496e: `Clickable` and `Link` now guarantee a minimum 24x24 pointer target, satisfying WCAG 2.5.8 (Target Size, Minimum). The hit area is expanded with a transparent `::after` pseudo-element rather than by growing the element, so **visual layout is unchanged**.
8
+
9
+ The new shared style is exported as `minTargetSizeStyles` from `@khanacademy/wonder-blocks-styles`, alongside `focusStyles` and `actionStyles`, for components that compose their own style arrays.
10
+
11
+ Both components accept a new `disableMinTargetSize` prop (defaults to `false`) to opt out. Opt out where the expanded hit area would overlap an adjacent target, where a `Clickable` wraps its own interactive elements that the hit area would otherwise cover, or where the element already draws its own `::after`.
12
+
13
+ That last case matters if you pass a `style` that defines `:after` or `::after`: they are the same pseudo-element, so your rule and the hit area cascade together at equal specificity (both `!important`) and merge per-property by source order, breaking both. `Cell` hits exactly this — it draws its horizontal rule with `:after` on the `Clickable` element — so `Cell` (and `DetailCell`, `OptionItem`, `ActionItem`, which all render through it) sets `disableMinTargetSize` internally. Those are all at least 44px tall already, so no target-size guarantee is lost.
14
+
15
+ Notes:
16
+
17
+ - `Link` skips this for `inline` links regardless of the prop. WCAG 2.5.8 exempts targets whose size is constrained by the line-height of surrounding text, and expanding an inline link would steal clicks from adjacent lines.
18
+ - Because the hit area extends beyond the element's visual box, small controls placed closer than 24px apart will have overlapping hit areas, and the later-painted one wins. Either space them at least 24px apart (which satisfies WCAG 2.5.8's own spacing exception) or set `disableMinTargetSize`.
19
+ - Both components now set `position: relative` when the hit area is applied, which makes them a containing block for any absolutely-positioned descendants that previously resolved against an ancestor.
20
+ - Like the other `wonder-blocks-styles` exports, `minTargetSizeStyles.minTargetSize` is a plain object rather than an Aphrodite stylesheet, so it can be spread into a style object. Two consequences for a `style` you pass to `Clickable` or `Link`. A `style` built with `StyleSheet.create` stays its own class and merges with the hit area per-property, but is now applied _before_ it — so it can add to the `::after` (a shadow, say) yet no longer override `position: relative`. A plain-object `style` is instead merged into the same class, so a `::after` of your own **replaces** the hit area wholesale rather than merging with it. In either case, compose rather than collide by spreading the hit area into your own rule:
21
+
22
+ ```tsx
23
+ <Clickable
24
+ style={{
25
+ "::after": {
26
+ ...minTargetSizeStyles.minTargetSize["::after"],
27
+ boxShadow: boxShadow.mid,
28
+ },
29
+ }}
30
+ >
31
+ ```
32
+
33
+ - If your Jest setup inlines Aphrodite styles (`SNAPSHOT_INLINE_APHRODITE`, as Wonder Blocks' own config does), nested selectors are not applied on that path, so the `::after` hit area will not appear in snapshots.
34
+
35
+ ### Patch Changes
36
+
37
+ - Updated dependencies [8ac496e]
38
+ - @khanacademy/wonder-blocks-clickable@8.3.0
39
+ - @khanacademy/wonder-blocks-styles@0.3.0
40
+
3
41
  ## 10.3.12
4
42
 
5
43
  ### Patch Changes
@@ -21,6 +21,20 @@ type CommonProps = AriaProps & {
21
21
  * from surrounding text.
22
22
  */
23
23
  inline?: boolean;
24
+ /**
25
+ * Whether to disable the minimum 24x24 hit area applied to this link.
26
+ *
27
+ * By default a transparent `::after` pseudo-element expands the link's hit
28
+ * area to at least 24x24 to satisfy WCAG 2.5.8 (Target Size, Minimum).
29
+ * Set this to `true` where the expanded area would overlap an adjacent
30
+ * target.
31
+ *
32
+ * No change to `inline` links: WCAG 2.5.8 exempts targets whose size is
33
+ * constrained by the line-height of the surrounding text
34
+ *
35
+ * @defaultValue false
36
+ */
37
+ disableMinTargetSize?: boolean;
24
38
  /**
25
39
  * Specifies the type of relationship between the current document and the
26
40
  * linked document. Should only be used when `href` is specified. This
package/dist/es/index.js CHANGED
@@ -5,7 +5,7 @@ import { isClientSideUrl, getClickableBehavior } from '@khanacademy/wonder-block
5
5
  import { StyleSheet } from 'aphrodite';
6
6
  import { addStyle } from '@khanacademy/wonder-blocks-core';
7
7
  import { mapValuesToCssVars, border, semanticColor, font, sizing } from '@khanacademy/wonder-blocks-tokens';
8
- import { focusStyles } from '@khanacademy/wonder-blocks-styles';
8
+ import { minTargetSizeStyles, focusStyles } from '@khanacademy/wonder-blocks-styles';
9
9
  import { PhosphorIcon } from '@khanacademy/wonder-blocks-icon';
10
10
  import externalLinkIcon from '@phosphor-icons/core/bold/arrow-square-out-bold.svg';
11
11
 
@@ -13,7 +13,7 @@ var themeDefault = {root:{font:{family:"inherit",weight:"inherit"}}};
13
13
 
14
14
  var theme = mapValuesToCssVars(themeDefault,"--wb-c-link-");
15
15
 
16
- const StyledA=addStyle("a");const StyledLink=addStyle(Link$1);const LinkCore=React.forwardRef(function LinkCore(props,ref){const inRouterContext=useInRouterContext();const{children,skipClientNav,focused,hovered,href,inline=false,pressed,style,testId,waiting:_,target,startIcon,endIcon,labels,viewTransition,state,...restProps}=props;const defaultStyles=[styles.shared,styles.rest,inline&&styles.restInline,!pressed&&focused&&styles.focus];const commonProps={"data-testid":testId,style:[defaultStyles,style],target,...restProps};const linkUrl=new URL(href,window.location.origin);const isExternalLink=linkUrl.origin!==window.location.origin;const externalIcon=jsx(PhosphorIcon,{icon:externalLinkIcon,size:"small",style:[styles.endIcon,styles.centered],testId:"external-icon","aria-label":labels?.externalIconAriaLabel});let startIconElement;let endIconElement;if(startIcon){startIconElement=React.cloneElement(startIcon,{style:[styles.startIcon,styles.centered],testId:"start-icon",...startIcon.props});}if(endIcon){endIconElement=React.cloneElement(endIcon,{style:[styles.endIcon,styles.centered],testId:"end-icon",...endIcon.props});}const linkContent=jsxs(Fragment,{children:[startIcon&&startIconElement,children,endIcon?endIconElement:isExternalLink&&target==="_blank"&&externalIcon]});return inRouterContext&&!skipClientNav&&isClientSideUrl(href)?jsx(StyledLink,{...commonProps,to:href,ref:ref,viewTransition:viewTransition,state:state,children:linkContent}):jsx(StyledA,{...commonProps,href:href,ref:ref,children:linkContent})});const focusStyling={...focusStyles.focus[":focus-visible"],borderRadius:border.radius.radius_010,outlineOffset:border.width.medium};const pressStyling={color:semanticColor.link.press,textDecoration:"underline currentcolor solid",textUnderlineOffset:font.textDecoration.underlineOffset};const styles=StyleSheet.create({shared:{fontFamily:theme.root.font.family,fontWeight:theme.root.font.weight,cursor:"pointer",textDecoration:"none",outline:"none",alignItems:"center"},rest:{color:semanticColor.link.rest,":hover":{textDecoration:"underline currentcolor solid",color:semanticColor.link.hover,textUnderlineOffset:font.textDecoration.underlineOffset},":focus-visible":focusStyling,":active":pressStyling},restInline:{textDecoration:"underline currentcolor solid",textDecorationThickness:font.textDecoration.thickness,textUnderlineOffset:font.textDecoration.underlineOffset},focus:focusStyling,press:pressStyling,startIcon:{marginInlineEnd:sizing.size_040},endIcon:{marginInlineStart:sizing.size_040},centered:{verticalAlign:"-10%"}});
16
+ const StyledA=addStyle("a");const StyledLink=addStyle(Link$1);const LinkCore=React.forwardRef(function LinkCore(props,ref){const inRouterContext=useInRouterContext();const{children,skipClientNav,focused,hovered,href,inline=false,disableMinTargetSize=false,pressed,style,testId,waiting:_,target,startIcon,endIcon,labels,viewTransition,state,...restProps}=props;const defaultStyles=[styles.shared,styles.rest,inline&&styles.restInline,!inline&&!disableMinTargetSize&&minTargetSizeStyles.minTargetSize,!pressed&&focused&&styles.focus];const commonProps={"data-testid":testId,style:[defaultStyles,style],target,...restProps};const linkUrl=new URL(href,window.location.origin);const isExternalLink=linkUrl.origin!==window.location.origin;const externalIcon=jsx(PhosphorIcon,{icon:externalLinkIcon,size:"small",style:[styles.endIcon,styles.centered],testId:"external-icon","aria-label":labels?.externalIconAriaLabel});let startIconElement;let endIconElement;if(startIcon){startIconElement=React.cloneElement(startIcon,{style:[styles.startIcon,styles.centered],testId:"start-icon",...startIcon.props});}if(endIcon){endIconElement=React.cloneElement(endIcon,{style:[styles.endIcon,styles.centered],testId:"end-icon",...endIcon.props});}const linkContent=jsxs(Fragment,{children:[startIcon&&startIconElement,children,endIcon?endIconElement:isExternalLink&&target==="_blank"&&externalIcon]});return inRouterContext&&!skipClientNav&&isClientSideUrl(href)?jsx(StyledLink,{...commonProps,to:href,ref:ref,viewTransition:viewTransition,state:state,children:linkContent}):jsx(StyledA,{...commonProps,href:href,ref:ref,children:linkContent})});const focusStyling={...focusStyles.focus[":focus-visible"],borderRadius:border.radius.radius_010,outlineOffset:border.width.medium};const pressStyling={color:semanticColor.link.press,textDecoration:"underline currentcolor solid",textUnderlineOffset:font.textDecoration.underlineOffset};const styles=StyleSheet.create({shared:{fontFamily:theme.root.font.family,fontWeight:theme.root.font.weight,cursor:"pointer",textDecoration:"none",outline:"none",alignItems:"center"},rest:{color:semanticColor.link.rest,":hover":{textDecoration:"underline currentcolor solid",color:semanticColor.link.hover,textUnderlineOffset:font.textDecoration.underlineOffset},":focus-visible":focusStyling,":active":pressStyling},restInline:{textDecoration:"underline currentcolor solid",textDecorationThickness:font.textDecoration.thickness,textUnderlineOffset:font.textDecoration.underlineOffset},focus:focusStyling,press:pressStyling,startIcon:{marginInlineEnd:sizing.size_040},endIcon:{marginInlineStart:sizing.size_040},centered:{verticalAlign:"-10%"}});
17
17
 
18
18
  const Link=React.forwardRef(function Link(props,ref){const{onClick,beforeNav=undefined,safeWithNav,href,skipClientNav,children,tabIndex,onKeyDown,onKeyUp,target=undefined,inline=false,viewTransition=false,state,...sharedProps}=props;const inRouterContext=useInRouterContext();const ClickableBehavior=getClickableBehavior(href,skipClientNav,inRouterContext);if(beforeNav){return jsx(ClickableBehavior,{disabled:false,href:href,role:"link",onClick:onClick,beforeNav:beforeNav,safeWithNav:safeWithNav,onKeyDown:onKeyDown,onKeyUp:onKeyUp,viewTransition:viewTransition,state:state,children:(clickableState,{...childrenProps})=>{return jsx(LinkCore,{...sharedProps,...clickableState,...childrenProps,skipClientNav:skipClientNav,href:href,target:target,tabIndex:tabIndex,inline:inline,ref:ref,viewTransition:viewTransition,state:state,children:children})}})}else {return jsx(ClickableBehavior,{disabled:false,href:href,role:"link",onClick:onClick,safeWithNav:safeWithNav,target:target,onKeyDown:onKeyDown,onKeyUp:onKeyUp,viewTransition:viewTransition,state:state,children:(clickableState,{...childrenProps})=>{return jsx(LinkCore,{...sharedProps,...clickableState,...childrenProps,skipClientNav:skipClientNav,href:href,target:target,tabIndex:tabIndex,inline:inline,ref:ref,viewTransition:viewTransition,state:state,children:children})}})}});
19
19
 
package/dist/index.js CHANGED
@@ -38,7 +38,7 @@ var themeDefault = {root:{font:{family:"inherit",weight:"inherit"}}};
38
38
 
39
39
  var theme = wonderBlocksTokens.mapValuesToCssVars(themeDefault,"--wb-c-link-");
40
40
 
41
- const StyledA=wonderBlocksCore.addStyle("a");const StyledLink=wonderBlocksCore.addStyle(reactRouterDomV5Compat.Link);const LinkCore=React__namespace.forwardRef(function LinkCore(props,ref){const inRouterContext=reactRouterDomV5Compat.useInRouterContext();const{children,skipClientNav,focused,hovered,href,inline=false,pressed,style,testId,waiting:_,target,startIcon,endIcon,labels,viewTransition,state,...restProps}=props;const defaultStyles=[styles.shared,styles.rest,inline&&styles.restInline,!pressed&&focused&&styles.focus];const commonProps={"data-testid":testId,style:[defaultStyles,style],target,...restProps};const linkUrl=new URL(href,window.location.origin);const isExternalLink=linkUrl.origin!==window.location.origin;const externalIcon=jsxRuntime.jsx(wonderBlocksIcon.PhosphorIcon,{icon:externalLinkIcon__default["default"],size:"small",style:[styles.endIcon,styles.centered],testId:"external-icon","aria-label":labels?.externalIconAriaLabel});let startIconElement;let endIconElement;if(startIcon){startIconElement=React__namespace.cloneElement(startIcon,{style:[styles.startIcon,styles.centered],testId:"start-icon",...startIcon.props});}if(endIcon){endIconElement=React__namespace.cloneElement(endIcon,{style:[styles.endIcon,styles.centered],testId:"end-icon",...endIcon.props});}const linkContent=jsxRuntime.jsxs(jsxRuntime.Fragment,{children:[startIcon&&startIconElement,children,endIcon?endIconElement:isExternalLink&&target==="_blank"&&externalIcon]});return inRouterContext&&!skipClientNav&&wonderBlocksClickable.isClientSideUrl(href)?jsxRuntime.jsx(StyledLink,{...commonProps,to:href,ref:ref,viewTransition:viewTransition,state:state,children:linkContent}):jsxRuntime.jsx(StyledA,{...commonProps,href:href,ref:ref,children:linkContent})});const focusStyling={...wonderBlocksStyles.focusStyles.focus[":focus-visible"],borderRadius:wonderBlocksTokens.border.radius.radius_010,outlineOffset:wonderBlocksTokens.border.width.medium};const pressStyling={color:wonderBlocksTokens.semanticColor.link.press,textDecoration:"underline currentcolor solid",textUnderlineOffset:wonderBlocksTokens.font.textDecoration.underlineOffset};const styles=aphrodite.StyleSheet.create({shared:{fontFamily:theme.root.font.family,fontWeight:theme.root.font.weight,cursor:"pointer",textDecoration:"none",outline:"none",alignItems:"center"},rest:{color:wonderBlocksTokens.semanticColor.link.rest,":hover":{textDecoration:"underline currentcolor solid",color:wonderBlocksTokens.semanticColor.link.hover,textUnderlineOffset:wonderBlocksTokens.font.textDecoration.underlineOffset},":focus-visible":focusStyling,":active":pressStyling},restInline:{textDecoration:"underline currentcolor solid",textDecorationThickness:wonderBlocksTokens.font.textDecoration.thickness,textUnderlineOffset:wonderBlocksTokens.font.textDecoration.underlineOffset},focus:focusStyling,press:pressStyling,startIcon:{marginInlineEnd:wonderBlocksTokens.sizing.size_040},endIcon:{marginInlineStart:wonderBlocksTokens.sizing.size_040},centered:{verticalAlign:"-10%"}});
41
+ const StyledA=wonderBlocksCore.addStyle("a");const StyledLink=wonderBlocksCore.addStyle(reactRouterDomV5Compat.Link);const LinkCore=React__namespace.forwardRef(function LinkCore(props,ref){const inRouterContext=reactRouterDomV5Compat.useInRouterContext();const{children,skipClientNav,focused,hovered,href,inline=false,disableMinTargetSize=false,pressed,style,testId,waiting:_,target,startIcon,endIcon,labels,viewTransition,state,...restProps}=props;const defaultStyles=[styles.shared,styles.rest,inline&&styles.restInline,!inline&&!disableMinTargetSize&&wonderBlocksStyles.minTargetSizeStyles.minTargetSize,!pressed&&focused&&styles.focus];const commonProps={"data-testid":testId,style:[defaultStyles,style],target,...restProps};const linkUrl=new URL(href,window.location.origin);const isExternalLink=linkUrl.origin!==window.location.origin;const externalIcon=jsxRuntime.jsx(wonderBlocksIcon.PhosphorIcon,{icon:externalLinkIcon__default["default"],size:"small",style:[styles.endIcon,styles.centered],testId:"external-icon","aria-label":labels?.externalIconAriaLabel});let startIconElement;let endIconElement;if(startIcon){startIconElement=React__namespace.cloneElement(startIcon,{style:[styles.startIcon,styles.centered],testId:"start-icon",...startIcon.props});}if(endIcon){endIconElement=React__namespace.cloneElement(endIcon,{style:[styles.endIcon,styles.centered],testId:"end-icon",...endIcon.props});}const linkContent=jsxRuntime.jsxs(jsxRuntime.Fragment,{children:[startIcon&&startIconElement,children,endIcon?endIconElement:isExternalLink&&target==="_blank"&&externalIcon]});return inRouterContext&&!skipClientNav&&wonderBlocksClickable.isClientSideUrl(href)?jsxRuntime.jsx(StyledLink,{...commonProps,to:href,ref:ref,viewTransition:viewTransition,state:state,children:linkContent}):jsxRuntime.jsx(StyledA,{...commonProps,href:href,ref:ref,children:linkContent})});const focusStyling={...wonderBlocksStyles.focusStyles.focus[":focus-visible"],borderRadius:wonderBlocksTokens.border.radius.radius_010,outlineOffset:wonderBlocksTokens.border.width.medium};const pressStyling={color:wonderBlocksTokens.semanticColor.link.press,textDecoration:"underline currentcolor solid",textUnderlineOffset:wonderBlocksTokens.font.textDecoration.underlineOffset};const styles=aphrodite.StyleSheet.create({shared:{fontFamily:theme.root.font.family,fontWeight:theme.root.font.weight,cursor:"pointer",textDecoration:"none",outline:"none",alignItems:"center"},rest:{color:wonderBlocksTokens.semanticColor.link.rest,":hover":{textDecoration:"underline currentcolor solid",color:wonderBlocksTokens.semanticColor.link.hover,textUnderlineOffset:wonderBlocksTokens.font.textDecoration.underlineOffset},":focus-visible":focusStyling,":active":pressStyling},restInline:{textDecoration:"underline currentcolor solid",textDecorationThickness:wonderBlocksTokens.font.textDecoration.thickness,textUnderlineOffset:wonderBlocksTokens.font.textDecoration.underlineOffset},focus:focusStyling,press:pressStyling,startIcon:{marginInlineEnd:wonderBlocksTokens.sizing.size_040},endIcon:{marginInlineStart:wonderBlocksTokens.sizing.size_040},centered:{verticalAlign:"-10%"}});
42
42
 
43
43
  const Link=React__namespace.forwardRef(function Link(props,ref){const{onClick,beforeNav=undefined,safeWithNav,href,skipClientNav,children,tabIndex,onKeyDown,onKeyUp,target=undefined,inline=false,viewTransition=false,state,...sharedProps}=props;const inRouterContext=reactRouterDomV5Compat.useInRouterContext();const ClickableBehavior=wonderBlocksClickable.getClickableBehavior(href,skipClientNav,inRouterContext);if(beforeNav){return jsxRuntime.jsx(ClickableBehavior,{disabled:false,href:href,role:"link",onClick:onClick,beforeNav:beforeNav,safeWithNav:safeWithNav,onKeyDown:onKeyDown,onKeyUp:onKeyUp,viewTransition:viewTransition,state:state,children:(clickableState,{...childrenProps})=>{return jsxRuntime.jsx(LinkCore,{...sharedProps,...clickableState,...childrenProps,skipClientNav:skipClientNav,href:href,target:target,tabIndex:tabIndex,inline:inline,ref:ref,viewTransition:viewTransition,state:state,children:children})}})}else {return jsxRuntime.jsx(ClickableBehavior,{disabled:false,href:href,role:"link",onClick:onClick,safeWithNav:safeWithNav,target:target,onKeyDown:onKeyDown,onKeyUp:onKeyUp,viewTransition:viewTransition,state:state,children:(clickableState,{...childrenProps})=>{return jsxRuntime.jsx(LinkCore,{...sharedProps,...clickableState,...childrenProps,skipClientNav:skipClientNav,href:href,target:target,tabIndex:tabIndex,inline:inline,ref:ref,viewTransition:viewTransition,state:state,children:children})}})}});
44
44
 
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Link component for Wonder Blocks design system.",
4
4
  "author": "Khan Academy",
5
5
  "license": "MIT",
6
- "version": "10.3.12",
6
+ "version": "10.4.0",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -28,12 +28,12 @@
28
28
  "./styles.css": "./dist/css/vars.css"
29
29
  },
30
30
  "dependencies": {
31
- "@khanacademy/wonder-blocks-clickable": "8.2.11",
31
+ "@khanacademy/wonder-blocks-clickable": "8.3.0",
32
32
  "@khanacademy/wonder-blocks-core": "12.5.0",
33
33
  "@khanacademy/wonder-blocks-icon": "6.0.2",
34
34
  "@khanacademy/wonder-blocks-tokens": "18.0.0",
35
35
  "@khanacademy/wonder-blocks-typography": "5.0.5",
36
- "@khanacademy/wonder-blocks-styles": "0.2.54"
36
+ "@khanacademy/wonder-blocks-styles": "0.3.0"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@phosphor-icons/core": "^2.0.2",