@hzblj/kara-components 0.10.0 → 0.11.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.
Files changed (185) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/lib/module/atoms/activity-indicator/activity-indicator.android.js +6 -1
  3. package/lib/module/atoms/activity-indicator/activity-indicator.android.js.map +1 -1
  4. package/lib/module/atoms/activity-indicator/activity-indicator.ios.js +2 -0
  5. package/lib/module/atoms/activity-indicator/activity-indicator.ios.js.map +1 -1
  6. package/lib/module/atoms/avatar/avatar.js +5 -0
  7. package/lib/module/atoms/avatar/avatar.js.map +1 -1
  8. package/lib/module/atoms/button/button-tokens.js +2 -2
  9. package/lib/module/atoms/button/button-tokens.js.map +1 -1
  10. package/lib/module/atoms/button/button.android.js +6 -2
  11. package/lib/module/atoms/button/button.android.js.map +1 -1
  12. package/lib/module/atoms/button/button.ios.js +7 -2
  13. package/lib/module/atoms/button/button.ios.js.map +1 -1
  14. package/lib/module/atoms/card/card.android.js +5 -3
  15. package/lib/module/atoms/card/card.android.js.map +1 -1
  16. package/lib/module/atoms/card/card.ios.js +7 -3
  17. package/lib/module/atoms/card/card.ios.js.map +1 -1
  18. package/lib/module/atoms/checkbox/checkbox.android.js +1 -1
  19. package/lib/module/atoms/checkbox/checkbox.android.js.map +1 -1
  20. package/lib/module/atoms/fab/fab.android.js +9 -1
  21. package/lib/module/atoms/fab/fab.android.js.map +1 -1
  22. package/lib/module/atoms/fab/fab.ios.js +9 -3
  23. package/lib/module/atoms/fab/fab.ios.js.map +1 -1
  24. package/lib/module/atoms/glass-view/glass-view.android.js +1 -1
  25. package/lib/module/atoms/glass-view/glass-view.android.js.map +1 -1
  26. package/lib/module/atoms/h-stack/h-stack.ios.js +5 -3
  27. package/lib/module/atoms/h-stack/h-stack.ios.js.map +1 -1
  28. package/lib/module/atoms/icon-button/icon-button.android.js +9 -0
  29. package/lib/module/atoms/icon-button/icon-button.android.js.map +1 -1
  30. package/lib/module/atoms/icon-button/icon-button.ios.js +9 -2
  31. package/lib/module/atoms/icon-button/icon-button.ios.js.map +1 -1
  32. package/lib/module/atoms/image/image.js +4 -4
  33. package/lib/module/atoms/image/image.js.map +1 -1
  34. package/lib/module/atoms/pressable/pressable.js +26 -25
  35. package/lib/module/atoms/pressable/pressable.js.map +1 -1
  36. package/lib/module/atoms/progress/progress.android.js +7 -0
  37. package/lib/module/atoms/progress/progress.android.js.map +1 -1
  38. package/lib/module/atoms/progress/progress.ios.js +6 -1
  39. package/lib/module/atoms/progress/progress.ios.js.map +1 -1
  40. package/lib/module/atoms/segmented-control/segmented-control.android.js +1 -1
  41. package/lib/module/atoms/segmented-control/segmented-control.android.js.map +1 -1
  42. package/lib/module/atoms/segmented-control/segmented-control.ios.js +1 -1
  43. package/lib/module/atoms/segmented-control/segmented-control.ios.js.map +1 -1
  44. package/lib/module/atoms/skeleton/skeleton.js +20 -6
  45. package/lib/module/atoms/skeleton/skeleton.js.map +1 -1
  46. package/lib/module/atoms/surface/surface.android.js +5 -3
  47. package/lib/module/atoms/surface/surface.android.js.map +1 -1
  48. package/lib/module/atoms/surface/surface.ios.js +7 -3
  49. package/lib/module/atoms/surface/surface.ios.js.map +1 -1
  50. package/lib/module/atoms/switch/switch.android.js +1 -1
  51. package/lib/module/atoms/switch/switch.android.js.map +1 -1
  52. package/lib/module/atoms/text/text.android.js +5 -3
  53. package/lib/module/atoms/text/text.android.js.map +1 -1
  54. package/lib/module/atoms/text/text.ios.js +6 -4
  55. package/lib/module/atoms/text/text.ios.js.map +1 -1
  56. package/lib/module/atoms/v-stack/v-stack.ios.js +5 -3
  57. package/lib/module/atoms/v-stack/v-stack.ios.js.map +1 -1
  58. package/lib/module/atoms/z-stack/z-stack.ios.js +5 -3
  59. package/lib/module/atoms/z-stack/z-stack.ios.js.map +1 -1
  60. package/lib/module/glyph/glyph.android.js +2 -0
  61. package/lib/module/glyph/glyph.android.js.map +1 -1
  62. package/lib/module/glyph/glyph.ios.js +5 -2
  63. package/lib/module/glyph/glyph.ios.js.map +1 -1
  64. package/lib/module/molecules/collapsible/collapsible.android.js +18 -8
  65. package/lib/module/molecules/collapsible/collapsible.android.js.map +1 -1
  66. package/lib/module/molecules/labeled-content/labeled-content.js +4 -1
  67. package/lib/module/molecules/labeled-content/labeled-content.js.map +1 -1
  68. package/lib/module/molecules/link/link.js +4 -1
  69. package/lib/module/molecules/link/link.js.map +1 -1
  70. package/lib/module/molecules/list-item/list-item.js +4 -1
  71. package/lib/module/molecules/list-item/list-item.js.map +1 -1
  72. package/lib/module/molecules/navigation-bar/navigation-bar.android.js +1 -1
  73. package/lib/module/molecules/navigation-bar/navigation-bar.android.js.map +1 -1
  74. package/lib/module/molecules/navigation-bar/navigation-bar.ios.js +1 -1
  75. package/lib/module/molecules/navigation-bar/navigation-bar.ios.js.map +1 -1
  76. package/lib/module/molecules/rn-list-item/rn-list-item.js +10 -2
  77. package/lib/module/molecules/rn-list-item/rn-list-item.js.map +1 -1
  78. package/lib/module/molecules/scroll-edge-container/scroll-edge-container.android.js +3 -4
  79. package/lib/module/molecules/scroll-edge-container/scroll-edge-container.android.js.map +1 -1
  80. package/lib/module/molecules/stepper/stepper.ios.js +6 -2
  81. package/lib/module/molecules/stepper/stepper.ios.js.map +1 -1
  82. package/lib/module/motion.js +19 -0
  83. package/lib/module/motion.js.map +1 -0
  84. package/lib/module/organisms/fab-menu/fab-menu.android.js +8 -5
  85. package/lib/module/organisms/fab-menu/fab-menu.android.js.map +1 -1
  86. package/lib/module/organisms/fab-menu/fab-menu.ios.js +5 -2
  87. package/lib/module/organisms/fab-menu/fab-menu.ios.js.map +1 -1
  88. package/lib/module/organisms/snackbar/snackbar.js +142 -17
  89. package/lib/module/organisms/snackbar/snackbar.js.map +1 -1
  90. package/lib/module/organisms/swipe-actions/swipe-actions.android.js +1 -1
  91. package/lib/module/organisms/swipe-actions/swipe-actions.android.js.map +1 -1
  92. package/lib/module/use-stack-modifiers.js +10 -8
  93. package/lib/module/use-stack-modifiers.js.map +1 -1
  94. package/lib/typescript/atoms/activity-indicator/activity-indicator.types.d.ts +2 -0
  95. package/lib/typescript/atoms/avatar/avatar.types.d.ts +5 -0
  96. package/lib/typescript/atoms/button/button-tokens.d.ts +1 -1
  97. package/lib/typescript/atoms/button/button.types.d.ts +2 -0
  98. package/lib/typescript/atoms/card/card.types.d.ts +2 -2
  99. package/lib/typescript/atoms/fab/fab.types.d.ts +8 -1
  100. package/lib/typescript/atoms/h-stack/h-stack.types.d.ts +6 -3
  101. package/lib/typescript/atoms/icon-button/icon-button.types.d.ts +7 -0
  102. package/lib/typescript/atoms/image/image.d.ts +2 -2
  103. package/lib/typescript/atoms/pressable/pressable.types.d.ts +9 -9
  104. package/lib/typescript/atoms/progress/progress.types.d.ts +2 -0
  105. package/lib/typescript/atoms/skeleton/skeleton.types.d.ts +3 -2
  106. package/lib/typescript/atoms/surface/surface.types.d.ts +2 -2
  107. package/lib/typescript/atoms/text/text.types.d.ts +2 -2
  108. package/lib/typescript/atoms/v-stack/v-stack.types.d.ts +3 -3
  109. package/lib/typescript/atoms/z-stack/z-stack.types.d.ts +3 -3
  110. package/lib/typescript/glyph/glyph.types.d.ts +2 -0
  111. package/lib/typescript/molecules/labeled-content/labeled-content.types.d.ts +4 -2
  112. package/lib/typescript/molecules/link/link.types.d.ts +2 -1
  113. package/lib/typescript/molecules/list-item/list-item.types.d.ts +5 -3
  114. package/lib/typescript/molecules/stepper/stepper.ios.d.ts +1 -1
  115. package/lib/typescript/motion.d.ts +8 -0
  116. package/lib/typescript/organisms/fab-menu/fab-menu.types.d.ts +5 -0
  117. package/lib/typescript/organisms/snackbar/snackbar.types.d.ts +38 -5
  118. package/lib/typescript/use-stack-modifiers.d.ts +6 -5
  119. package/package.json +4 -4
  120. package/src/atoms/activity-indicator/activity-indicator.android.tsx +7 -1
  121. package/src/atoms/activity-indicator/activity-indicator.ios.tsx +9 -1
  122. package/src/atoms/activity-indicator/activity-indicator.types.ts +2 -0
  123. package/src/atoms/avatar/avatar.tsx +5 -1
  124. package/src/atoms/avatar/avatar.types.ts +5 -0
  125. package/src/atoms/button/button-tokens.ts +2 -2
  126. package/src/atoms/button/button.android.tsx +5 -3
  127. package/src/atoms/button/button.ios.tsx +10 -2
  128. package/src/atoms/button/button.types.ts +2 -0
  129. package/src/atoms/card/card.android.tsx +6 -4
  130. package/src/atoms/card/card.ios.tsx +7 -3
  131. package/src/atoms/card/card.types.ts +8 -7
  132. package/src/atoms/checkbox/checkbox.android.tsx +1 -1
  133. package/src/atoms/fab/fab.android.tsx +5 -3
  134. package/src/atoms/fab/fab.ios.tsx +8 -4
  135. package/src/atoms/fab/fab.types.ts +8 -1
  136. package/src/atoms/glass-view/glass-view.android.tsx +1 -1
  137. package/src/atoms/h-stack/h-stack.ios.tsx +6 -4
  138. package/src/atoms/h-stack/h-stack.types.ts +17 -13
  139. package/src/atoms/icon-button/icon-button.android.tsx +16 -3
  140. package/src/atoms/icon-button/icon-button.ios.tsx +13 -4
  141. package/src/atoms/icon-button/icon-button.types.ts +7 -0
  142. package/src/atoms/image/image.tsx +3 -3
  143. package/src/atoms/pressable/pressable.tsx +26 -18
  144. package/src/atoms/pressable/pressable.types.ts +30 -29
  145. package/src/atoms/progress/progress.android.tsx +5 -3
  146. package/src/atoms/progress/progress.ios.tsx +5 -2
  147. package/src/atoms/progress/progress.types.ts +2 -0
  148. package/src/atoms/segmented-control/segmented-control.android.tsx +1 -1
  149. package/src/atoms/segmented-control/segmented-control.ios.tsx +1 -1
  150. package/src/atoms/skeleton/skeleton.tsx +18 -4
  151. package/src/atoms/skeleton/skeleton.types.ts +3 -2
  152. package/src/atoms/surface/surface.android.tsx +6 -4
  153. package/src/atoms/surface/surface.ios.tsx +7 -3
  154. package/src/atoms/surface/surface.types.ts +10 -9
  155. package/src/atoms/switch/switch.android.tsx +1 -1
  156. package/src/atoms/text/text.android.tsx +6 -4
  157. package/src/atoms/text/text.ios.tsx +6 -4
  158. package/src/atoms/text/text.types.ts +23 -22
  159. package/src/atoms/v-stack/v-stack.ios.tsx +6 -4
  160. package/src/atoms/v-stack/v-stack.types.ts +17 -16
  161. package/src/atoms/z-stack/z-stack.ios.tsx +6 -4
  162. package/src/atoms/z-stack/z-stack.types.ts +15 -14
  163. package/src/glyph/glyph.android.tsx +14 -2
  164. package/src/glyph/glyph.ios.tsx +13 -3
  165. package/src/glyph/glyph.types.ts +2 -0
  166. package/src/molecules/collapsible/collapsible.android.tsx +27 -7
  167. package/src/molecules/labeled-content/labeled-content.tsx +12 -1
  168. package/src/molecules/labeled-content/labeled-content.types.ts +4 -2
  169. package/src/molecules/link/link.tsx +2 -1
  170. package/src/molecules/link/link.types.ts +2 -1
  171. package/src/molecules/list-item/list-item.tsx +20 -2
  172. package/src/molecules/list-item/list-item.types.ts +5 -3
  173. package/src/molecules/navigation-bar/navigation-bar.android.tsx +1 -1
  174. package/src/molecules/navigation-bar/navigation-bar.ios.tsx +1 -1
  175. package/src/molecules/rn-list-item/rn-list-item.tsx +18 -3
  176. package/src/molecules/scroll-edge-container/scroll-edge-container.android.tsx +3 -2
  177. package/src/molecules/stepper/stepper.ios.tsx +8 -2
  178. package/src/motion.ts +13 -0
  179. package/src/organisms/fab-menu/fab-menu.android.tsx +9 -5
  180. package/src/organisms/fab-menu/fab-menu.ios.tsx +6 -2
  181. package/src/organisms/fab-menu/fab-menu.types.ts +5 -0
  182. package/src/organisms/snackbar/snackbar.tsx +143 -19
  183. package/src/organisms/snackbar/snackbar.types.ts +41 -5
  184. package/src/organisms/swipe-actions/swipe-actions.android.tsx +1 -1
  185. package/src/use-stack-modifiers.ts +20 -7
@@ -1,4 +1,4 @@
1
- import type {ResponsiveSx} from '@hzblj/kara-sx'
1
+ import type {AccessibilityProps, ResponsiveSx} from '@hzblj/kara-sx'
2
2
  import type {ColorToken, TextVariant} from '@hzblj/kara-theme'
3
3
  import type {ReactNode} from 'react'
4
4
 
@@ -13,26 +13,27 @@ export type TextTransform = 'lowercase' | 'uppercase'
13
13
  * the native `sx` surface too: padding, a frame (`wide` lines it up by `align`), a background, and the `ios` bucket's
14
14
  * `contentTransition`, `animation` and `monospacedDigit` for a number that changes in place.
15
15
  */
16
- export type TextProps = ResponsiveSx & {
17
- /**
18
- * How its lines align. iOS reads `left` and `right` as leading and trailing, so they flip in a right-to-left
19
- * layout; Android keeps them as they are.
20
- */
21
- align?: TextAlign
22
- /** The text. */
23
- children?: ReactNode
24
- /** The text's colour. `content.primary` by default. */
25
- color?: ColorToken
26
- /** An underline or a strikethrough. */
27
- decoration?: TextDecoration
28
- /** The most lines it wraps to. iOS truncates the last with an ellipsis; Android clips it. */
29
- numberOfLines?: number
30
- /** Test identifier, forwarded to the native view. */
31
- testID?: string
32
- /** Upper- or lower-cases the text. Android applies it only when `children` is a plain string. */
33
- transform?: TextTransform
34
- /** The typography variant. `body` by default. */
35
- variant?: TextVariant
36
- }
16
+ export type TextProps = ResponsiveSx &
17
+ AccessibilityProps & {
18
+ /**
19
+ * How its lines align. iOS reads `left` and `right` as leading and trailing, so they flip in a right-to-left
20
+ * layout; Android keeps them as they are.
21
+ */
22
+ align?: TextAlign
23
+ /** The text. */
24
+ children?: ReactNode
25
+ /** The text's colour. `content.primary` by default. */
26
+ color?: ColorToken
27
+ /** An underline or a strikethrough. */
28
+ decoration?: TextDecoration
29
+ /** The most lines it wraps to. iOS truncates the last with an ellipsis; Android clips it. */
30
+ numberOfLines?: number
31
+ /** Test identifier, forwarded to the native view. */
32
+ testID?: string
33
+ /** Upper- or lower-cases the text. Android applies it only when `children` is a plain string. */
34
+ transform?: TextTransform
35
+ /** The typography variant. `body` by default. */
36
+ variant?: TextVariant
37
+ }
37
38
 
38
39
  export type TextComponent = (props: TextProps) => ReactNode
@@ -1,6 +1,6 @@
1
1
  import {VStack as SwiftUIVStack} from '@expo/ui/swift-ui'
2
2
  import {onTapGesture} from '@expo/ui/swift-ui/modifiers'
3
- import {type FrameAlignment, useSxModifiers} from '@hzblj/kara-sx'
3
+ import {type FrameAlignment, splitAccessibility, useAccessibilityModifiers, useSxModifiers} from '@hzblj/kara-sx'
4
4
  import {spacing} from '@hzblj/kara-theme'
5
5
  import {useMemo} from 'react'
6
6
  import {useHostGuard} from '../../host-context'
@@ -18,13 +18,15 @@ export const VStack: VStackComponent = ({
18
18
  onPress,
19
19
  spacing: gap,
20
20
  testID,
21
- ...sx
21
+ ...props
22
22
  }: VStackProps) => {
23
23
  useHostGuard('VStack')
24
+ const [accessibility, sx] = splitAccessibility(props)
24
25
  const sxModifiers = useSxModifiers(sx, ALIGNMENT[align])
26
+ const accessibilityModifiers = useAccessibilityModifiers(accessibility, onPress !== undefined)
25
27
  const modifiers = useMemo(
26
- () => (onPress ? [...sxModifiers, onTapGesture(onPress)] : sxModifiers),
27
- [onPress, sxModifiers]
28
+ () => [...sxModifiers, ...(onPress ? [onTapGesture(onPress)] : []), ...accessibilityModifiers],
29
+ [accessibilityModifiers, onPress, sxModifiers]
28
30
  )
29
31
 
30
32
  return (
@@ -1,24 +1,25 @@
1
- import type {ResponsiveSx} from '@hzblj/kara-sx'
1
+ import type {AccessibilityProps, ResponsiveSx} from '@hzblj/kara-sx'
2
2
  import type {SpaceSize} from '@hzblj/kara-theme'
3
3
  import type {ReactNode} from 'react'
4
4
 
5
5
  export type VStackAlign = 'center' | 'end' | 'start'
6
6
 
7
7
  /** Stacks its children top to bottom: SwiftUI `VStack` / Compose `Column`. */
8
- export type VStackProps = ResponsiveSx & {
9
- /** The children's horizontal alignment. `start` by default. */
10
- align?: VStackAlign
11
- /** Laid out top to bottom, `spacing` apart. */
12
- children?: ReactNode
13
- /**
14
- * Makes the whole stack pressable. iOS adds a tap gesture with no press feedback; Android a `clickable` whose ripple
15
- * fills the stack's shape.
16
- */
17
- onPress?: () => void
18
- /** The space between children. */
19
- spacing?: SpaceSize
20
- /** Test identifier, forwarded to the native view. */
21
- testID?: string
22
- }
8
+ export type VStackProps = ResponsiveSx &
9
+ AccessibilityProps & {
10
+ /** The children's horizontal alignment. `start` by default. */
11
+ align?: VStackAlign
12
+ /** Laid out top to bottom, `spacing` apart. */
13
+ children?: ReactNode
14
+ /**
15
+ * Makes the whole stack pressable. iOS adds a tap gesture with no press feedback; Android a `clickable` whose ripple
16
+ * fills the stack's shape. Screen readers then read the stack as one button.
17
+ */
18
+ onPress?: () => void
19
+ /** The space between children. */
20
+ spacing?: SpaceSize
21
+ /** Test identifier, forwarded to the native view. */
22
+ testID?: string
23
+ }
23
24
 
24
25
  export type VStackComponent = (props: VStackProps) => ReactNode
@@ -1,6 +1,6 @@
1
1
  import {ZStack as SwiftUIZStack} from '@expo/ui/swift-ui'
2
2
  import {onTapGesture} from '@expo/ui/swift-ui/modifiers'
3
- import {type FrameAlignment, useSxModifiers} from '@hzblj/kara-sx'
3
+ import {type FrameAlignment, splitAccessibility, useAccessibilityModifiers, useSxModifiers} from '@hzblj/kara-sx'
4
4
  import {useMemo} from 'react'
5
5
  import {useHostGuard} from '../../host-context'
6
6
  import type {ZStackAlign, ZStackComponent, ZStackProps} from './z-stack.types'
@@ -11,12 +11,14 @@ const ALIGNMENT = {
11
11
  top: 'top',
12
12
  } as const satisfies Record<ZStackAlign, FrameAlignment>
13
13
 
14
- export const ZStack: ZStackComponent = ({align = 'center', children, onPress, testID, ...sx}: ZStackProps) => {
14
+ export const ZStack: ZStackComponent = ({align = 'center', children, onPress, testID, ...props}: ZStackProps) => {
15
15
  useHostGuard('ZStack')
16
+ const [accessibility, sx] = splitAccessibility(props)
16
17
  const sxModifiers = useSxModifiers(sx, ALIGNMENT[align])
18
+ const accessibilityModifiers = useAccessibilityModifiers(accessibility, onPress !== undefined)
17
19
  const modifiers = useMemo(
18
- () => (onPress ? [...sxModifiers, onTapGesture(onPress)] : sxModifiers),
19
- [onPress, sxModifiers]
20
+ () => [...sxModifiers, ...(onPress ? [onTapGesture(onPress)] : []), ...accessibilityModifiers],
21
+ [accessibilityModifiers, onPress, sxModifiers]
20
22
  )
21
23
 
22
24
  return (
@@ -1,21 +1,22 @@
1
- import type {ResponsiveSx} from '@hzblj/kara-sx'
1
+ import type {AccessibilityProps, ResponsiveSx} from '@hzblj/kara-sx'
2
2
  import type {ReactNode} from 'react'
3
3
 
4
4
  export type ZStackAlign = 'bottom' | 'center' | 'top'
5
5
 
6
6
  /** Layers its children on one another: SwiftUI `ZStack` / Compose `Box`. */
7
- export type ZStackProps = ResponsiveSx & {
8
- /** Where the children sit vertically; they are always centred horizontally. `center` by default. */
9
- align?: ZStackAlign
10
- /** Drawn in order, the last on top. */
11
- children?: ReactNode
12
- /**
13
- * Makes the whole stack pressable. iOS adds a tap gesture with no press feedback; Android a `clickable` whose ripple
14
- * fills the stack's shape.
15
- */
16
- onPress?: () => void
17
- /** Test identifier, forwarded to the native view. */
18
- testID?: string
19
- }
7
+ export type ZStackProps = ResponsiveSx &
8
+ AccessibilityProps & {
9
+ /** Where the children sit vertically; they are always centred horizontally. `center` by default. */
10
+ align?: ZStackAlign
11
+ /** Drawn in order, the last on top. */
12
+ children?: ReactNode
13
+ /**
14
+ * Makes the whole stack pressable. iOS adds a tap gesture with no press feedback; Android a `clickable` whose ripple
15
+ * fills the stack's shape. Screen readers then read the stack as one button.
16
+ */
17
+ onPress?: () => void
18
+ /** Test identifier, forwarded to the native view. */
19
+ testID?: string
20
+ }
20
21
 
21
22
  export type ZStackComponent = (props: ZStackProps) => ReactNode
@@ -5,8 +5,20 @@ import {spacing, useAppTheme} from '@hzblj/kara-theme'
5
5
  import type {GlyphComponent, GlyphProps} from './glyph.types'
6
6
 
7
7
  /** The Material Symbol `@hzblj/kara-native` ships as a drawable — inside a Compose host. */
8
- export const Glyph: GlyphComponent = ({color = 'content.primary', name, size = 's24'}: GlyphProps) => {
8
+ export const Glyph: GlyphComponent = ({
9
+ accessibilityLabel,
10
+ color = 'content.primary',
11
+ name,
12
+ size = 's24',
13
+ }: GlyphProps) => {
9
14
  const theme = useAppTheme()
10
15
 
11
- return <DrawableIcon iconColor={theme.color(color)} iconSize={spacing[size]} resourceName={GLYPHS[name].android} />
16
+ return (
17
+ <DrawableIcon
18
+ accessibilityLabel={accessibilityLabel}
19
+ iconColor={theme.color(color)}
20
+ iconSize={spacing[size]}
21
+ resourceName={GLYPHS[name].android}
22
+ />
23
+ )
12
24
  }
@@ -1,15 +1,25 @@
1
1
  import {Image} from '@expo/ui/swift-ui'
2
- import {accessibilityHidden} from '@expo/ui/swift-ui/modifiers'
2
+ import {accessibilityHidden, accessibilityLabel as labelModifier} from '@expo/ui/swift-ui/modifiers'
3
3
  import {GLYPHS} from '@hzblj/kara-native'
4
4
  import {spacing, useAppTheme} from '@hzblj/kara-theme'
5
+ import {useMemo} from 'react'
5
6
 
6
7
  import type {GlyphComponent, GlyphProps} from './glyph.types'
7
8
 
8
9
  const DECORATIVE = [accessibilityHidden(true)]
9
10
 
10
11
  /** The SF Symbol the system already has — inside a SwiftUI host. */
11
- export const Glyph: GlyphComponent = ({color = 'content.primary', name, size = 's24'}: GlyphProps) => {
12
+ export const Glyph: GlyphComponent = ({
13
+ accessibilityLabel,
14
+ color = 'content.primary',
15
+ name,
16
+ size = 's24',
17
+ }: GlyphProps) => {
12
18
  const theme = useAppTheme()
19
+ const modifiers = useMemo(
20
+ () => (accessibilityLabel ? [labelModifier(accessibilityLabel)] : DECORATIVE),
21
+ [accessibilityLabel]
22
+ )
13
23
 
14
- return <Image color={theme.color(color)} modifiers={DECORATIVE} size={spacing[size]} systemName={GLYPHS[name].ios} />
24
+ return <Image color={theme.color(color)} modifiers={modifiers} size={spacing[size]} systemName={GLYPHS[name].ios} />
15
25
  }
@@ -7,6 +7,8 @@ import type {ReactNode} from 'react'
7
7
  * their own icons with `Icon` / `RnIcon`.
8
8
  */
9
9
  export type GlyphProps = {
10
+ /** What a screen reader says. Unset, the glyph is decorative and hidden from it. */
11
+ accessibilityLabel?: string
10
12
  color?: ColorToken
11
13
  name: GlyphName
12
14
  size?: SpaceSize
@@ -15,19 +15,25 @@ import {
15
15
  graphicsLayer,
16
16
  tween,
17
17
  } from '@expo/ui/jetpack-compose/modifiers'
18
- import {duration, useAppTheme} from '@hzblj/kara-theme'
18
+ import {useAccessibilityModifiers} from '@hzblj/kara-sx'
19
+ import {duration, useAppTheme, useReducedMotion} from '@hzblj/kara-theme'
19
20
  import {useMemo} from 'react'
20
21
  import {Glyph} from '../../glyph'
21
22
  import {useHostGuard} from '../../host-context'
22
23
  import type {CollapsibleComponent, CollapsibleProps} from './collapsible.types'
23
24
 
24
- /** Material's standard easing, at the length a disclosure takes. */
25
- const TURN = tween({durationMillis: duration.d200, easing: 'fastOutSlowIn'})
25
+ /** Material's standard easing, at the length a chevron turning takes. */
26
+ const TURN = tween({durationMillis: duration.fast, easing: 'fastOutSlowIn'})
27
+ const SNAP = tween({durationMillis: duration.none})
26
28
 
27
29
  /** Down from the row, fading in — Compose's default grows from a corner, sideways as well as down. */
28
30
  const ENTER = EnterTransition.expandVertically().plus(EnterTransition.fadeIn())
29
31
  const EXIT = ExitTransition.shrinkVertically().plus(ExitTransition.fadeOut())
30
32
 
33
+ /** With motion reduced (Compose follows the device's own setting, this `KaraProvider reduceMotion`): no growing. */
34
+ const FADE_IN = EnterTransition.fadeIn()
35
+ const FADE_OUT = ExitTransition.fadeOut()
36
+
31
37
  const WIDE = [fillMaxWidth()]
32
38
 
33
39
  /** Material's least touch target. */
@@ -41,11 +47,21 @@ const ROW_HEIGHT = 48
41
47
  export const Collapsible: CollapsibleComponent = ({children, expanded, label, onExpandedChange}: CollapsibleProps) => {
42
48
  useHostGuard('Collapsible')
43
49
  const theme = useAppTheme()
50
+ const reduced = useReducedMotion()
51
+ const accessibilityModifiers = useAccessibilityModifiers({}, true)
44
52
  const modifiers = useMemo(
45
- () => [fillMaxWidth(), defaultMinSize({minHeight: ROW_HEIGHT}), clickable(() => onExpandedChange(!expanded))],
46
- [expanded, onExpandedChange]
53
+ () => [
54
+ fillMaxWidth(),
55
+ defaultMinSize({minHeight: ROW_HEIGHT}),
56
+ clickable(() => onExpandedChange(!expanded)),
57
+ ...accessibilityModifiers,
58
+ ],
59
+ [accessibilityModifiers, expanded, onExpandedChange]
60
+ )
61
+ const turn = useMemo(
62
+ () => [graphicsLayer({rotationZ: animated(expanded ? 90 : 0, reduced ? SNAP : TURN)})],
63
+ [expanded, reduced]
47
64
  )
48
- const turn = useMemo(() => [graphicsLayer({rotationZ: animated(expanded ? 90 : 0, TURN)})], [expanded])
49
65
 
50
66
  return (
51
67
  <Column modifiers={WIDE}>
@@ -57,7 +73,11 @@ export const Collapsible: CollapsibleComponent = ({children, expanded, label, on
57
73
  <Glyph name="chevronRight" size="s20" />
58
74
  </Box>
59
75
  </Row>
60
- <AnimatedVisibility enterTransition={ENTER} exitTransition={EXIT} visible={expanded}>
76
+ <AnimatedVisibility
77
+ enterTransition={reduced ? FADE_IN : ENTER}
78
+ exitTransition={reduced ? FADE_OUT : EXIT}
79
+ visible={expanded}
80
+ >
61
81
  {children}
62
82
  </AnimatedVisibility>
63
83
  </Column>
@@ -19,8 +19,19 @@ export const LabeledContent: LabeledContentComponent = ({
19
19
  px,
20
20
  testID,
21
21
  value,
22
+ ...accessibility
22
23
  }: LabeledContentProps) => (
23
- <HStack align="center" onPress={onPress} px={px} py="s8" spacing="s12" testID={testID} wide>
24
+ <HStack
25
+ accessible={children === undefined}
26
+ {...accessibility}
27
+ align="center"
28
+ onPress={onPress}
29
+ px={px}
30
+ py="s8"
31
+ spacing="s12"
32
+ testID={testID}
33
+ wide
34
+ >
24
35
  {icon ? <Icon color="content.secondary" name={icon} size="s20" /> : null}
25
36
  <Text variant="body">{label}</Text>
26
37
  <Spacer />
@@ -1,12 +1,14 @@
1
+ import type {AccessibilityProps} from '@hzblj/kara-sx'
1
2
  import type {SpaceSize} from '@hzblj/kara-theme'
2
3
  import type {ReactNode} from 'react'
3
4
  import type {IconName} from '../../atoms/icon'
4
5
 
5
6
  /**
6
7
  * A label on the leading edge and its value on the trailing edge: the settings/detail row.
7
- * Pass `children` instead of `value` when the trailing side is a control rather than text.
8
+ * Pass `children` instead of `value` when the trailing side is a control rather than text. With a `value`, screen
9
+ * readers read the row as one element ("Version, 2.4"); with a control, the control stays its own.
8
10
  */
9
- export type LabeledContentProps = {
11
+ export type LabeledContentProps = AccessibilityProps & {
10
12
  /** The trailing side when it is a control, in place of `value`. */
11
13
  children?: ReactNode
12
14
  /** One of the app's icons, before the label, in the secondary colour. */
@@ -12,13 +12,14 @@ export const Link: LinkComponent = ({
12
12
  testID,
13
13
  url,
14
14
  variant = 'body',
15
+ ...accessibility
15
16
  }: LinkProps) => {
16
17
  const open = () => {
17
18
  Linking.openURL(url).catch(() => undefined)
18
19
  }
19
20
 
20
21
  return (
21
- <HStack align="center" onPress={open} spacing="s8" testID={testID}>
22
+ <HStack accessibilityRole="link" {...accessibility} align="center" onPress={open} spacing="s8" testID={testID}>
22
23
  {icon ? <Icon color={color} name={icon} size="s20" /> : null}
23
24
  <Text color={color} variant={variant}>
24
25
  {label}
@@ -1,10 +1,11 @@
1
+ import type {AccessibilityProps} from '@hzblj/kara-sx'
1
2
  import type {ColorToken, TextVariant} from '@hzblj/kara-theme'
2
3
  import type {ReactNode} from 'react'
3
4
 
4
5
  import type {IconName} from '../../atoms/icon'
5
6
 
6
7
  /** Opens an external URL in the platform browser or the app that claims the scheme. */
7
- export type LinkProps = {
8
+ export type LinkProps = AccessibilityProps & {
8
9
  /** Colours the label and the icon. `content.accent` by default. */
9
10
  color?: ColorToken
10
11
  /** One of the app's icons, before the label. */
@@ -14,8 +14,26 @@ import type {ListItemComponent, ListItemProps} from './list-item.types'
14
14
  */
15
15
  const TEXT_FILLS_ROW = {weight: 1}
16
16
 
17
- export const ListItem: ListItemComponent = ({icon, onPress, px, subtitle, testID, title}: ListItemProps) => (
18
- <HStack align="center" onPress={onPress} px={px} py="s8" spacing="s12" testID={testID} wide>
17
+ export const ListItem: ListItemComponent = ({
18
+ icon,
19
+ onPress,
20
+ px,
21
+ subtitle,
22
+ testID,
23
+ title,
24
+ ...accessibility
25
+ }: ListItemProps) => (
26
+ <HStack
27
+ accessible
28
+ {...accessibility}
29
+ align="center"
30
+ onPress={onPress}
31
+ px={px}
32
+ py="s8"
33
+ spacing="s12"
34
+ testID={testID}
35
+ wide
36
+ >
19
37
  {icon ? <Icon color="content.secondary" name={icon} size="s20" /> : null}
20
38
  <VStack android={TEXT_FILLS_ROW} spacing="s2">
21
39
  <Text variant="body">{title}</Text>
@@ -1,13 +1,15 @@
1
+ import type {AccessibilityProps} from '@hzblj/kara-sx'
1
2
  import type {SpaceSize} from '@hzblj/kara-theme'
2
3
  import type {ReactNode} from 'react'
3
4
 
4
5
  import type {IconName} from '../../atoms/icon'
5
6
 
6
7
  /**
7
- * A row with an optional icon, a title and subtitle, and a chevron when it can be pressed. A native atom: it renders
8
- * inside `<Screen>` or `<Island>`; `RnListItem` is the same row without a host.
8
+ * A row with an optional icon, a title and subtitle, and a chevron when it can be pressed. Screen readers read it as one
9
+ * element (a button when it is pressable). A native atom: it renders inside `<Screen>` or `<Island>`; `RnListItem` is
10
+ * the same row without a host.
9
11
  */
10
- export type ListItemProps = {
12
+ export type ListItemProps = AccessibilityProps & {
11
13
  /** One of the app's icons, before the title, in the secondary colour. */
12
14
  icon?: IconName
13
15
  /** Makes the row pressable and adds a chevron on the trailing edge. */
@@ -27,7 +27,7 @@ export const NavigationBar: NavigationBarComponent = ({
27
27
  }: NavigationBarProps) => {
28
28
  const theme = useAppTheme()
29
29
  const font = theme.typography.headline
30
- const color = theme.color(inverted ? 'content.on.bg' : 'content.primary')
30
+ const color = theme.color(inverted ? 'content.on.accent' : 'content.primary')
31
31
 
32
32
  const nativeLeadingItems = useMemo(() => leadingItems?.map(toNativeItem), [leadingItems])
33
33
  const nativeTrailingItems = useMemo(() => trailingItems?.map(toNativeItem), [trailingItems])
@@ -38,7 +38,7 @@ export const NavigationBar: NavigationBarComponent = ({
38
38
  const theme = useAppTheme()
39
39
  const barRef = useRef<NavigationBarRef>(null)
40
40
  const font = theme.typography.headline
41
- const color = theme.color(inverted ? 'content.on.bg' : 'content.primary')
41
+ const color = theme.color(inverted ? 'content.on.accent' : 'content.primary')
42
42
 
43
43
  useImperativeHandle(
44
44
  search?.ref,
@@ -1,3 +1,4 @@
1
+ import {accessibilityProps} from '@hzblj/kara-sx'
1
2
  import {useAppTheme} from '@hzblj/kara-theme'
2
3
  import {useMemo} from 'react'
3
4
  import {Platform, Pressable, type PressableStateCallbackType} from 'react-native'
@@ -8,8 +9,18 @@ import {RnText} from '../../atoms/rn-text'
8
9
  import {RnGlyph} from '../../glyph'
9
10
  import type {RnListItemComponent, RnListItemProps} from './rn-list-item.types'
10
11
 
11
- export const RnListItem: RnListItemComponent = ({icon, onPress, px, subtitle, testID, title}: RnListItemProps) => {
12
+ export const RnListItem: RnListItemComponent = ({
13
+ icon,
14
+ onPress,
15
+ px,
16
+ subtitle,
17
+ testID,
18
+ title,
19
+ ...accessibility
20
+ }: RnListItemProps) => {
12
21
  const theme = useAppTheme()
22
+ // One element for a screen reader, as `ListItem` is: a button when it can be pressed.
23
+ const a11y = accessibilityProps({accessible: true, ...accessibility}, onPress ? 'button' : undefined)
13
24
  const pressedColor = theme.color('fill.tertiary')
14
25
 
15
26
  const ripple = useMemo(() => ({color: pressedColor}), [pressedColor])
@@ -37,11 +48,15 @@ export const RnListItem: RnListItemComponent = ({icon, onPress, px, subtitle, te
37
48
  )
38
49
 
39
50
  if (!onPress) {
40
- return <Box testID={testID}>{row}</Box>
51
+ return (
52
+ <Box {...a11y} testID={testID}>
53
+ {row}
54
+ </Box>
55
+ )
41
56
  }
42
57
 
43
58
  return (
44
- <Pressable accessibilityRole="button" android_ripple={ripple} onPress={onPress} style={highlight} testID={testID}>
59
+ <Pressable {...a11y} android_ripple={ripple} onPress={onPress} style={highlight} testID={testID}>
45
60
  {row}
46
61
  </Pressable>
47
62
  )
@@ -1,15 +1,16 @@
1
- import {duration, opacity} from '@hzblj/kara-theme'
1
+ import {opacity} from '@hzblj/kara-theme'
2
2
  import {cloneElement, type ReactNode, useCallback, useRef} from 'react'
3
3
  import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent} from 'react-native'
4
4
  import {type SharedValue, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'
5
5
 
6
6
  import {ABox, Box} from '../../atoms/box'
7
+ import {timing} from '../../motion'
7
8
  import type {ScrollEdgeContainerComponent, ScrollEdgeContainerProps} from './scroll-edge-container.types'
8
9
 
9
10
  /** Less than a pixel scrolled is not scrolled — a fling settles a fraction away from the edge. */
10
11
  const EDGE_SLOP = 0.5
11
12
 
12
- const FILL = {duration: duration.d200}
13
+ const FILL = timing('quick', 'out')
13
14
 
14
15
  type Bar = {children: ReactNode; filled: SharedValue<number>}
15
16
 
@@ -1,5 +1,6 @@
1
1
  import {Stepper as SwiftUIStepper} from '@expo/ui/swift-ui'
2
2
  import {labelsHidden} from '@expo/ui/swift-ui/modifiers'
3
+ import {useReducedMotion} from '@hzblj/kara-theme'
3
4
  import {useRef} from 'react'
4
5
 
5
6
  import {HStack} from '../../atoms/h-stack'
@@ -14,10 +15,11 @@ const LABEL_HIDDEN = [labelsHidden()]
14
15
  /**
15
16
  * SwiftUI's `Stepper` at the end of a row, the label leading and the value just before the control — the way iOS's own
16
17
  * settings show a stepped value. The value rolls to the next one (`numericText`), up or down with the change, in
17
- * monospaced digits so the row does not jump.
18
+ * monospaced digits so the row does not jump. With motion reduced it cross-fades instead.
18
19
  */
19
20
  export const Stepper: StepperComponent = ({label, max, min, onValueChange, step = 1, value}: StepperProps) => {
20
21
  useHostGuard('Stepper')
22
+ const reduced = useReducedMotion()
21
23
  const previous = useRef(value)
22
24
  const countsDown = value < previous.current
23
25
  previous.current = value
@@ -28,7 +30,11 @@ export const Stepper: StepperComponent = ({label, max, min, onValueChange, step
28
30
  <Spacer />
29
31
  <Text
30
32
  color="content.secondary"
31
- ios={{animation: {value}, contentTransition: {countsDown, type: 'numericText'}, monospacedDigit: true}}
33
+ ios={{
34
+ animation: {value},
35
+ contentTransition: reduced ? {type: 'opacity'} : {countsDown, type: 'numericText'},
36
+ monospacedDigit: true,
37
+ }}
32
38
  >
33
39
  {String(value)}
34
40
  </Text>
package/src/motion.ts ADDED
@@ -0,0 +1,13 @@
1
+ import {type DurationSize, duration, type EasingName, easing} from '@hzblj/kara-theme'
2
+ import {Easing, ReduceMotion, type WithTimingConfig} from 'react-native-reanimated'
3
+
4
+ /**
5
+ * A Reanimated timing from the motion tokens, `smoothOut` unless told otherwise. `ReduceMotion.Never`, because a
6
+ * component has applied `useReducedMotion` before it animates: what is left (a fade, a dim) is meant to run, and
7
+ * Reanimated's own check reads the setting once at launch and never hears of `KaraProvider reduceMotion`.
8
+ */
9
+ export const timing = (size: DurationSize, curve: EasingName = 'smoothOut'): WithTimingConfig => {
10
+ const [x1, y1, x2, y2] = easing[curve]
11
+
12
+ return {duration: duration[size], easing: Easing.bezier(x1, y1, x2, y2), reduceMotion: ReduceMotion.Never}
13
+ }
@@ -25,16 +25,16 @@ const FabMenuRow = ({action, onSelect}: FabMenuRowProps) => {
25
25
  return (
26
26
  <FloatingActionButtonMenuItem
27
27
  containerColor={theme.color('fill.accent.primary')}
28
- contentColor={theme.color('content.on.bg')}
28
+ contentColor={theme.color('content.on.accent')}
29
29
  onClick={onClick}
30
30
  >
31
31
  {action.icon ? (
32
32
  <FloatingActionButtonMenuItem.Icon>
33
- <Icon color="content.on.bg" name={action.icon} />
33
+ <Icon color="content.on.accent" name={action.icon} />
34
34
  </FloatingActionButtonMenuItem.Icon>
35
35
  ) : null}
36
36
  <FloatingActionButtonMenuItem.Text>
37
- <ComposeText color={theme.colorString('content.on.bg')} style={theme.typography.labelMedium}>
37
+ <ComposeText color={theme.colorString('content.on.accent')} style={theme.typography.labelMedium}>
38
38
  {action.label}
39
39
  </ComposeText>
40
40
  </FloatingActionButtonMenuItem.Text>
@@ -47,7 +47,7 @@ const FabMenuRow = ({action, onSelect}: FabMenuRowProps) => {
47
47
  * content — rather than a row of its own. The pills take the button's colours, so the menu reads as the button
48
48
  * unfolding rather than a second surface.
49
49
  */
50
- export const FabMenu: FabMenuComponent = ({actions, icon}: FabMenuProps) => {
50
+ export const FabMenu: FabMenuComponent = ({accessibilityLabel, actions, icon}: FabMenuProps) => {
51
51
  useHostGuard('FabMenu')
52
52
 
53
53
  const theme = useAppTheme()
@@ -67,7 +67,11 @@ export const FabMenu: FabMenuComponent = ({actions, icon}: FabMenuProps) => {
67
67
  onCheckedChange={setExpanded}
68
68
  >
69
69
  <ToggleFloatingActionButton.Icon>
70
- {icon ? <Icon color="content.on.bg" name={icon} /> : <Glyph color="content.on.bg" name="plus" />}
70
+ {icon ? (
71
+ <Icon accessibilityLabel={accessibilityLabel} color="content.on.accent" name={icon} />
72
+ ) : (
73
+ <Glyph accessibilityLabel={accessibilityLabel} color="content.on.accent" name="plus" />
74
+ )}
71
75
  </ToggleFloatingActionButton.Icon>
72
76
  </ToggleFloatingActionButton>
73
77
  </FloatingActionButtonMenu.Button>
@@ -10,7 +10,7 @@ import type {FabMenuComponent, FabMenuProps} from './fab-menu.types'
10
10
  /** Matches `Fab`, so a screen can switch between the two without the button changing shape. */
11
11
  const FAB_SIZE = 56
12
12
 
13
- export const FabMenu: FabMenuComponent = ({actions, icon, testID}: FabMenuProps) => {
13
+ export const FabMenu: FabMenuComponent = ({accessibilityLabel, actions, icon, testID}: FabMenuProps) => {
14
14
  useHostGuard('FabMenu')
15
15
 
16
16
  return (
@@ -25,7 +25,11 @@ export const FabMenu: FabMenuComponent = ({actions, icon, testID}: FabMenuProps)
25
25
  shadow="btn.accent"
26
26
  w={FAB_SIZE}
27
27
  >
28
- {icon ? <Icon color="content.on.bg" name={icon} /> : <Glyph color="content.on.bg" name="plus" />}
28
+ {icon ? (
29
+ <Icon accessibilityLabel={accessibilityLabel} color="content.on.accent" name={icon} />
30
+ ) : (
31
+ <Glyph accessibilityLabel={accessibilityLabel} color="content.on.accent" name="plus" />
32
+ )}
29
33
  </VStack>
30
34
  }
31
35
  testID={testID}