@hzblj/kara-components 0.10.0 → 0.12.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 (214) hide show
  1. package/CHANGELOG.md +55 -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/box/box.js +3 -0
  9. package/lib/module/atoms/box/box.js.map +1 -1
  10. package/lib/module/atoms/box/index.js +0 -1
  11. package/lib/module/atoms/box/index.js.map +1 -1
  12. package/lib/module/atoms/button/button-tokens.js +2 -2
  13. package/lib/module/atoms/button/button-tokens.js.map +1 -1
  14. package/lib/module/atoms/button/button.android.js +6 -2
  15. package/lib/module/atoms/button/button.android.js.map +1 -1
  16. package/lib/module/atoms/button/button.ios.js +7 -2
  17. package/lib/module/atoms/button/button.ios.js.map +1 -1
  18. package/lib/module/atoms/card/card.android.js +5 -3
  19. package/lib/module/atoms/card/card.android.js.map +1 -1
  20. package/lib/module/atoms/card/card.ios.js +7 -3
  21. package/lib/module/atoms/card/card.ios.js.map +1 -1
  22. package/lib/module/atoms/checkbox/checkbox.android.js +1 -1
  23. package/lib/module/atoms/checkbox/checkbox.android.js.map +1 -1
  24. package/lib/module/atoms/fab/fab.android.js +9 -1
  25. package/lib/module/atoms/fab/fab.android.js.map +1 -1
  26. package/lib/module/atoms/fab/fab.ios.js +9 -3
  27. package/lib/module/atoms/fab/fab.ios.js.map +1 -1
  28. package/lib/module/atoms/glass-view/glass-view.android.js +1 -1
  29. package/lib/module/atoms/glass-view/glass-view.android.js.map +1 -1
  30. package/lib/module/atoms/h-stack/h-stack.ios.js +5 -3
  31. package/lib/module/atoms/h-stack/h-stack.ios.js.map +1 -1
  32. package/lib/module/atoms/icon-button/icon-button.android.js +9 -0
  33. package/lib/module/atoms/icon-button/icon-button.android.js.map +1 -1
  34. package/lib/module/atoms/icon-button/icon-button.ios.js +9 -2
  35. package/lib/module/atoms/icon-button/icon-button.ios.js.map +1 -1
  36. package/lib/module/atoms/image/image.js +4 -4
  37. package/lib/module/atoms/image/image.js.map +1 -1
  38. package/lib/module/atoms/index.js +1 -0
  39. package/lib/module/atoms/index.js.map +1 -1
  40. package/lib/module/atoms/pressable/pressable.js +75 -27
  41. package/lib/module/atoms/pressable/pressable.js.map +1 -1
  42. package/lib/module/atoms/progress/progress.android.js +7 -0
  43. package/lib/module/atoms/progress/progress.android.js.map +1 -1
  44. package/lib/module/atoms/progress/progress.ios.js +6 -1
  45. package/lib/module/atoms/progress/progress.ios.js.map +1 -1
  46. package/lib/module/atoms/rn-text/rn-text.js +66 -26
  47. package/lib/module/atoms/rn-text/rn-text.js.map +1 -1
  48. package/lib/module/atoms/segmented-control/segmented-control.android.js +1 -1
  49. package/lib/module/atoms/segmented-control/segmented-control.android.js.map +1 -1
  50. package/lib/module/atoms/segmented-control/segmented-control.ios.js +1 -1
  51. package/lib/module/atoms/segmented-control/segmented-control.ios.js.map +1 -1
  52. package/lib/module/atoms/skeleton/skeleton.js +22 -8
  53. package/lib/module/atoms/skeleton/skeleton.js.map +1 -1
  54. package/lib/module/atoms/stagger/index.js +5 -0
  55. package/lib/module/atoms/stagger/index.js.map +1 -0
  56. package/lib/module/atoms/stagger/stagger.js +102 -0
  57. package/lib/module/atoms/stagger/stagger.js.map +1 -0
  58. package/lib/module/atoms/stagger/stagger.types.js +4 -0
  59. package/lib/module/atoms/stagger/stagger.types.js.map +1 -0
  60. package/lib/module/atoms/surface/surface.android.js +5 -3
  61. package/lib/module/atoms/surface/surface.android.js.map +1 -1
  62. package/lib/module/atoms/surface/surface.ios.js +7 -3
  63. package/lib/module/atoms/surface/surface.ios.js.map +1 -1
  64. package/lib/module/atoms/switch/switch.android.js +1 -1
  65. package/lib/module/atoms/switch/switch.android.js.map +1 -1
  66. package/lib/module/atoms/text/text.android.js +5 -3
  67. package/lib/module/atoms/text/text.android.js.map +1 -1
  68. package/lib/module/atoms/text/text.ios.js +6 -4
  69. package/lib/module/atoms/text/text.ios.js.map +1 -1
  70. package/lib/module/atoms/v-stack/v-stack.ios.js +5 -3
  71. package/lib/module/atoms/v-stack/v-stack.ios.js.map +1 -1
  72. package/lib/module/atoms/z-stack/z-stack.ios.js +5 -3
  73. package/lib/module/atoms/z-stack/z-stack.ios.js.map +1 -1
  74. package/lib/module/glyph/glyph.android.js +2 -0
  75. package/lib/module/glyph/glyph.android.js.map +1 -1
  76. package/lib/module/glyph/glyph.ios.js +5 -2
  77. package/lib/module/glyph/glyph.ios.js.map +1 -1
  78. package/lib/module/molecules/collapsible/collapsible.android.js +18 -8
  79. package/lib/module/molecules/collapsible/collapsible.android.js.map +1 -1
  80. package/lib/module/molecules/labeled-content/labeled-content.js +4 -1
  81. package/lib/module/molecules/labeled-content/labeled-content.js.map +1 -1
  82. package/lib/module/molecules/link/link.js +4 -1
  83. package/lib/module/molecules/link/link.js.map +1 -1
  84. package/lib/module/molecules/list-item/list-item.js +4 -1
  85. package/lib/module/molecules/list-item/list-item.js.map +1 -1
  86. package/lib/module/molecules/navigation-bar/navigation-bar.android.js +1 -1
  87. package/lib/module/molecules/navigation-bar/navigation-bar.android.js.map +1 -1
  88. package/lib/module/molecules/navigation-bar/navigation-bar.ios.js +1 -1
  89. package/lib/module/molecules/navigation-bar/navigation-bar.ios.js.map +1 -1
  90. package/lib/module/molecules/rn-list-item/rn-list-item.js +10 -2
  91. package/lib/module/molecules/rn-list-item/rn-list-item.js.map +1 -1
  92. package/lib/module/molecules/scroll-edge-container/scroll-edge-container.android.js +5 -6
  93. package/lib/module/molecules/scroll-edge-container/scroll-edge-container.android.js.map +1 -1
  94. package/lib/module/molecules/stepper/stepper.ios.js +6 -2
  95. package/lib/module/molecules/stepper/stepper.ios.js.map +1 -1
  96. package/lib/module/organisms/fab-menu/fab-menu.android.js +8 -5
  97. package/lib/module/organisms/fab-menu/fab-menu.android.js.map +1 -1
  98. package/lib/module/organisms/fab-menu/fab-menu.ios.js +5 -2
  99. package/lib/module/organisms/fab-menu/fab-menu.ios.js.map +1 -1
  100. package/lib/module/organisms/snackbar/snackbar.js +155 -18
  101. package/lib/module/organisms/snackbar/snackbar.js.map +1 -1
  102. package/lib/module/organisms/swipe-actions/swipe-actions.android.js +1 -1
  103. package/lib/module/organisms/swipe-actions/swipe-actions.android.js.map +1 -1
  104. package/lib/module/use-stack-modifiers.js +10 -8
  105. package/lib/module/use-stack-modifiers.js.map +1 -1
  106. package/lib/typescript/atoms/activity-indicator/activity-indicator.types.d.ts +2 -0
  107. package/lib/typescript/atoms/avatar/avatar.types.d.ts +5 -0
  108. package/lib/typescript/atoms/box/box.d.ts +3 -0
  109. package/lib/typescript/atoms/box/index.d.ts +0 -1
  110. package/lib/typescript/atoms/button/button-tokens.d.ts +1 -1
  111. package/lib/typescript/atoms/button/button.types.d.ts +2 -0
  112. package/lib/typescript/atoms/card/card.types.d.ts +2 -2
  113. package/lib/typescript/atoms/fab/fab.types.d.ts +8 -1
  114. package/lib/typescript/atoms/h-stack/h-stack.types.d.ts +6 -3
  115. package/lib/typescript/atoms/icon-button/icon-button.types.d.ts +7 -0
  116. package/lib/typescript/atoms/image/image.d.ts +2 -2
  117. package/lib/typescript/atoms/index.d.ts +1 -0
  118. package/lib/typescript/atoms/pressable/pressable.types.d.ts +10 -9
  119. package/lib/typescript/atoms/progress/progress.types.d.ts +2 -0
  120. package/lib/typescript/atoms/rn-text/rn-text.types.d.ts +7 -4
  121. package/lib/typescript/atoms/skeleton/skeleton.types.d.ts +3 -2
  122. package/lib/typescript/atoms/stagger/index.d.ts +2 -0
  123. package/lib/typescript/atoms/stagger/stagger.d.ts +2 -0
  124. package/lib/typescript/atoms/stagger/stagger.types.d.ts +32 -0
  125. package/lib/typescript/atoms/surface/surface.types.d.ts +2 -2
  126. package/lib/typescript/atoms/text/text.types.d.ts +2 -2
  127. package/lib/typescript/atoms/v-stack/v-stack.types.d.ts +3 -3
  128. package/lib/typescript/atoms/z-stack/z-stack.types.d.ts +3 -3
  129. package/lib/typescript/glyph/glyph.types.d.ts +2 -0
  130. package/lib/typescript/molecules/labeled-content/labeled-content.types.d.ts +4 -2
  131. package/lib/typescript/molecules/link/link.types.d.ts +2 -1
  132. package/lib/typescript/molecules/list-item/list-item.types.d.ts +5 -3
  133. package/lib/typescript/molecules/stepper/stepper.ios.d.ts +1 -1
  134. package/lib/typescript/organisms/fab-menu/fab-menu.types.d.ts +5 -0
  135. package/lib/typescript/organisms/snackbar/snackbar.types.d.ts +35 -4
  136. package/lib/typescript/use-stack-modifiers.d.ts +6 -5
  137. package/package.json +4 -4
  138. package/src/atoms/activity-indicator/activity-indicator.android.tsx +7 -1
  139. package/src/atoms/activity-indicator/activity-indicator.ios.tsx +9 -1
  140. package/src/atoms/activity-indicator/activity-indicator.types.ts +2 -0
  141. package/src/atoms/avatar/avatar.tsx +5 -1
  142. package/src/atoms/avatar/avatar.types.ts +5 -0
  143. package/src/atoms/box/box.tsx +3 -0
  144. package/src/atoms/box/index.ts +0 -1
  145. package/src/atoms/button/button-tokens.ts +2 -2
  146. package/src/atoms/button/button.android.tsx +5 -3
  147. package/src/atoms/button/button.ios.tsx +10 -2
  148. package/src/atoms/button/button.types.ts +2 -0
  149. package/src/atoms/card/card.android.tsx +6 -4
  150. package/src/atoms/card/card.ios.tsx +7 -3
  151. package/src/atoms/card/card.types.ts +8 -7
  152. package/src/atoms/checkbox/checkbox.android.tsx +1 -1
  153. package/src/atoms/fab/fab.android.tsx +5 -3
  154. package/src/atoms/fab/fab.ios.tsx +8 -4
  155. package/src/atoms/fab/fab.types.ts +8 -1
  156. package/src/atoms/glass-view/glass-view.android.tsx +1 -1
  157. package/src/atoms/h-stack/h-stack.ios.tsx +6 -4
  158. package/src/atoms/h-stack/h-stack.types.ts +17 -13
  159. package/src/atoms/icon-button/icon-button.android.tsx +16 -3
  160. package/src/atoms/icon-button/icon-button.ios.tsx +13 -4
  161. package/src/atoms/icon-button/icon-button.types.ts +7 -0
  162. package/src/atoms/image/image.tsx +3 -3
  163. package/src/atoms/index.ts +1 -0
  164. package/src/atoms/pressable/pressable.tsx +164 -100
  165. package/src/atoms/pressable/pressable.types.ts +32 -29
  166. package/src/atoms/progress/progress.android.tsx +5 -3
  167. package/src/atoms/progress/progress.ios.tsx +5 -2
  168. package/src/atoms/progress/progress.types.ts +2 -0
  169. package/src/atoms/rn-text/rn-text.tsx +101 -32
  170. package/src/atoms/rn-text/rn-text.types.ts +7 -3
  171. package/src/atoms/segmented-control/segmented-control.android.tsx +1 -1
  172. package/src/atoms/segmented-control/segmented-control.ios.tsx +1 -1
  173. package/src/atoms/skeleton/skeleton.tsx +20 -6
  174. package/src/atoms/skeleton/skeleton.types.ts +3 -2
  175. package/src/atoms/stagger/index.ts +2 -0
  176. package/src/atoms/stagger/stagger.tsx +98 -0
  177. package/src/atoms/stagger/stagger.types.ts +35 -0
  178. package/src/atoms/surface/surface.android.tsx +6 -4
  179. package/src/atoms/surface/surface.ios.tsx +7 -3
  180. package/src/atoms/surface/surface.types.ts +10 -9
  181. package/src/atoms/switch/switch.android.tsx +1 -1
  182. package/src/atoms/text/text.android.tsx +6 -4
  183. package/src/atoms/text/text.ios.tsx +6 -4
  184. package/src/atoms/text/text.types.ts +23 -22
  185. package/src/atoms/v-stack/v-stack.ios.tsx +6 -4
  186. package/src/atoms/v-stack/v-stack.types.ts +17 -16
  187. package/src/atoms/z-stack/z-stack.ios.tsx +6 -4
  188. package/src/atoms/z-stack/z-stack.types.ts +15 -14
  189. package/src/glyph/glyph.android.tsx +14 -2
  190. package/src/glyph/glyph.ios.tsx +13 -3
  191. package/src/glyph/glyph.types.ts +2 -0
  192. package/src/molecules/collapsible/collapsible.android.tsx +27 -7
  193. package/src/molecules/labeled-content/labeled-content.tsx +12 -1
  194. package/src/molecules/labeled-content/labeled-content.types.ts +4 -2
  195. package/src/molecules/link/link.tsx +2 -1
  196. package/src/molecules/link/link.types.ts +2 -1
  197. package/src/molecules/list-item/list-item.tsx +20 -2
  198. package/src/molecules/list-item/list-item.types.ts +5 -3
  199. package/src/molecules/navigation-bar/navigation-bar.android.tsx +1 -1
  200. package/src/molecules/navigation-bar/navigation-bar.ios.tsx +1 -1
  201. package/src/molecules/rn-list-item/rn-list-item.tsx +18 -3
  202. package/src/molecules/scroll-edge-container/scroll-edge-container.android.tsx +5 -4
  203. package/src/molecules/stepper/stepper.ios.tsx +8 -2
  204. package/src/organisms/fab-menu/fab-menu.android.tsx +9 -5
  205. package/src/organisms/fab-menu/fab-menu.ios.tsx +6 -2
  206. package/src/organisms/fab-menu/fab-menu.types.ts +5 -0
  207. package/src/organisms/snackbar/snackbar.tsx +125 -21
  208. package/src/organisms/snackbar/snackbar.types.ts +38 -4
  209. package/src/organisms/swipe-actions/swipe-actions.android.tsx +1 -1
  210. package/src/use-stack-modifiers.ts +20 -7
  211. package/lib/module/atoms/box/a-box.js +0 -6
  212. package/lib/module/atoms/box/a-box.js.map +0 -1
  213. package/lib/typescript/atoms/box/a-box.d.ts +0 -49
  214. package/src/atoms/box/a-box.ts +0 -5
@@ -3,8 +3,9 @@ import type {RadiiSize} from '@hzblj/kara-theme'
3
3
  import type {ReactNode} from 'react'
4
4
 
5
5
  /**
6
- * A loading placeholder that pulses in `fill.quaternary`. It is a React Native view, so it needs no host. Sized with
7
- * the `sx` names: `w`, `h`, `borderRadius`.
6
+ * A loading placeholder that pulses in `fill.quaternary`, and holds still with motion reduced. Hidden from screen
7
+ * readers: what is loading is for the screen to say. It is a React Native view, so it needs no host. Sized with the
8
+ * `sx` names: `w`, `h`, `borderRadius`.
8
9
  */
9
10
  export type SkeletonProps = {
10
11
  /** The corner radius. `small` by default. */
@@ -0,0 +1,2 @@
1
+ export * from './stagger'
2
+ export * from './stagger.types'
@@ -0,0 +1,98 @@
1
+ import {isMotionComponent, type MotionPresetConfig, type SxEntryExit, type SxLayout} from '@hzblj/kara-sx'
2
+ import {duration} from '@hzblj/kara-theme'
3
+ import {Children, cloneElement, isValidElement, type Key, type ReactElement, useLayoutEffect, useRef} from 'react'
4
+
5
+ import {Box} from '../box'
6
+ import type {StaggerComponent, StaggerMotion, StaggerProps} from './stagger.types'
7
+
8
+ type MotionChildProps = {entering?: SxEntryExit; exiting?: SxEntryExit; layout?: SxLayout}
9
+
10
+ const asConfig = (motion: StaggerMotion): MotionPresetConfig => (typeof motion === 'string' ? {preset: motion} : motion)
11
+
12
+ /**
13
+ * The stagger's preset, or the child's own with the delay added: a child that names its look keeps it and only takes
14
+ * its turn. Reanimated's own builder is the child's, untouched, since adding a delay to one mutates it.
15
+ */
16
+ const withDelay = (own: SxEntryExit | undefined, fallback: MotionPresetConfig, delay: number): SxEntryExit => {
17
+ if (own === undefined) {
18
+ return {...fallback, delay: (fallback.delay ?? 0) + delay}
19
+ }
20
+
21
+ if (typeof own === 'string') {
22
+ return {delay, preset: own}
23
+ }
24
+
25
+ return typeof own === 'object' && own !== null && 'preset' in own ? {...own, delay: (own.delay ?? 0) + delay} : own
26
+ }
27
+
28
+ export const Stagger: StaggerComponent = ({
29
+ children,
30
+ entering = 'fadeUp',
31
+ exiting,
32
+ from = 'start',
33
+ interval = 'stagger',
34
+ layout,
35
+ testID,
36
+ ...sx
37
+ }: StaggerProps) => {
38
+ // The children that have arrived already. Only the ones new in this render take turns, so a list that grows by a
39
+ // page staggers the page, and an item added on its own arrives at once instead of after everything above it.
40
+ const arrived = useRef(new Set<Key>())
41
+ const items = Children.toArray(children)
42
+ const turns = new Map<Key, number>()
43
+
44
+ for (const item of items) {
45
+ if (isValidElement(item) && item.key !== null && !arrived.current.has(item.key)) {
46
+ turns.set(item.key, turns.size)
47
+ }
48
+ }
49
+
50
+ // Marked once committed: a render React throws away must not use up a child's turn.
51
+ useLayoutEffect(() => {
52
+ for (const key of turns.keys()) {
53
+ arrived.current.add(key)
54
+ }
55
+ })
56
+
57
+ const step = duration[interval]
58
+ const enter = asConfig(entering)
59
+ const leave = exiting === undefined ? undefined : asConfig(exiting)
60
+
61
+ const delayOf = (key: Key | null) => {
62
+ const turn = key === null ? undefined : turns.get(key)
63
+
64
+ if (turn === undefined) {
65
+ return 0
66
+ }
67
+
68
+ return (from === 'start' ? turn : turns.size - 1 - turn) * step
69
+ }
70
+
71
+ return (
72
+ // With `layout` the container moves and resizes with its children, so what holds it can follow.
73
+ <Box layout={layout} testID={testID} {...sx}>
74
+ {items.map(item => {
75
+ if (!isValidElement<MotionChildProps>(item)) {
76
+ return item
77
+ }
78
+
79
+ const delay = delayOf(item.key)
80
+
81
+ // A component that takes the motion props gets them directly, with no view added around it.
82
+ if (isMotionComponent(item.type)) {
83
+ return cloneElement(item as ReactElement<MotionChildProps>, {
84
+ entering: withDelay(item.props.entering, enter, delay),
85
+ exiting: item.props.exiting ?? leave,
86
+ layout: item.props.layout ?? layout,
87
+ })
88
+ }
89
+
90
+ return (
91
+ <Box entering={withDelay(undefined, enter, delay)} exiting={leave} key={item.key} layout={layout}>
92
+ {item}
93
+ </Box>
94
+ )
95
+ })}
96
+ </Box>
97
+ )
98
+ }
@@ -0,0 +1,35 @@
1
+ import type {MotionPreset, MotionPresetConfig, ResponsiveViewSx} from '@hzblj/kara-sx'
2
+ import type {DurationSize, SpringName} from '@hzblj/kara-theme'
3
+ import type {ReactNode} from 'react'
4
+
5
+ /** A preset by name, or one adjusted: `{preset: 'fadeUp', duration: 'slow'}`. Its `delay` is where the first one starts. */
6
+ export type StaggerMotion = MotionPreset | MotionPresetConfig
7
+
8
+ /**
9
+ * A `Box` whose children arrive one after another: each takes `entering` a step later than the one before. Children
10
+ * that arrive together are staggered among themselves, the first ones as much as a later batch (the next page of a
11
+ * list), while a single child added later arrives at once. A `Box`, `RnText` or `Pressable` child takes the motion
12
+ * itself; any other child is wrapped in a `Box` that does. With motion reduced every child fades, still one after
13
+ * another. The React Native tier: it needs no host.
14
+ */
15
+ export type StaggerProps = ResponsiveViewSx & {
16
+ /** What arrives. A child with an `entering` preset of its own keeps it and only takes the delay. */
17
+ children?: ReactNode
18
+ /** How each child arrives. `fadeUp` by default. */
19
+ entering?: StaggerMotion
20
+ /** How a child leaves when it is removed. They leave together, not one after another; none by default. */
21
+ exiting?: StaggerMotion
22
+ /** Which end arrives first. `start` by default. */
23
+ from?: 'end' | 'start'
24
+ /** The step between one child and the next, a duration token. `stagger` (40 ms) by default, `micro` for a slower one. */
25
+ interval?: DurationSize
26
+ /**
27
+ * Moves the other children into place when one arrives or leaves, and the stagger itself to its new size: `true` on
28
+ * the `smooth` spring, or a spring token. Off by default. Give what sits after it `layout` too, so it follows.
29
+ */
30
+ layout?: boolean | SpringName
31
+ /** Test identifier, forwarded to the view. */
32
+ testID?: string
33
+ }
34
+
35
+ export type StaggerComponent = (props: StaggerProps) => ReactNode
@@ -1,6 +1,6 @@
1
1
  import {Column, Surface as ComposeSurface, Shape} from '@expo/ui/jetpack-compose'
2
2
  import {testID as testIdModifier} from '@expo/ui/jetpack-compose/modifiers'
3
- import {useSxModifiers} from '@hzblj/kara-sx'
3
+ import {splitAccessibility, useAccessibilityModifiers, useSxModifiers} from '@hzblj/kara-sx'
4
4
  import {radii, useAppTheme} from '@hzblj/kara-theme'
5
5
  import {useMemo} from 'react'
6
6
  import {useHostGuard} from '../../host-context'
@@ -24,15 +24,17 @@ export const Surface: SurfaceComponent = ({
24
24
  px,
25
25
  py,
26
26
  testID,
27
- ...sx
27
+ ...props
28
28
  }: SurfaceProps) => {
29
29
  useHostGuard('Surface')
30
+ const [accessibility, sx] = splitAccessibility(props)
30
31
 
31
32
  const theme = useAppTheme()
32
33
  const sxModifiers = useSxModifiers({...sx, bg: undefined})
34
+ const accessibilityModifiers = useAccessibilityModifiers(accessibility)
33
35
  const modifiers = useMemo(
34
- () => (testID ? [...sxModifiers, testIdModifier(testID)] : sxModifiers),
35
- [sxModifiers, testID]
36
+ () => [...sxModifiers, ...(testID ? [testIdModifier(testID)] : []), ...accessibilityModifiers],
37
+ [accessibilityModifiers, sxModifiers, testID]
36
38
  )
37
39
  const contentModifiers = useSxModifiers({p, pb, pl, pr, pt, px, py, wide: sx.wide})
38
40
 
@@ -1,13 +1,17 @@
1
1
  import {GlassEffectContainer, VStack} from '@expo/ui/swift-ui'
2
- import {useSxModifiers} from '@hzblj/kara-sx'
2
+ import {splitAccessibility, useAccessibilityModifiers, useSxModifiers} from '@hzblj/kara-sx'
3
+ import {useMemo} from 'react'
3
4
  import {useHostGuard} from '../../host-context'
4
5
  import type {SurfaceComponent, SurfaceProps} from './surface.types'
5
6
 
6
- export const Surface: SurfaceComponent = ({children, glass = false, testID, ...sx}: SurfaceProps) => {
7
+ export const Surface: SurfaceComponent = ({children, glass = false, testID, ...props}: SurfaceProps) => {
7
8
  useHostGuard('Surface')
9
+ const [accessibility, sx] = splitAccessibility(props)
8
10
 
9
11
  // `surface.primary` unless told otherwise, as Material's `Surface` is on Android — without it a plain one drew nothing.
10
- const modifiers = useSxModifiers({bg: 'surface.primary', borderRadius: 'large', p: 's16', ...sx})
12
+ const sxModifiers = useSxModifiers({bg: 'surface.primary', borderRadius: 'large', p: 's16', ...sx})
13
+ const accessibilityModifiers = useAccessibilityModifiers(accessibility)
14
+ const modifiers = useMemo(() => [...sxModifiers, ...accessibilityModifiers], [accessibilityModifiers, sxModifiers])
11
15
  const content = (
12
16
  <VStack alignment="leading" modifiers={modifiers} testID={testID}>
13
17
  {children}
@@ -1,17 +1,18 @@
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
  /**
5
5
  * A container on `surface.primary`, padded `s16` and rounded `large` unless told otherwise: a SwiftUI `VStack` on
6
6
  * iOS, Material's `Surface` around a `Column` on Android.
7
7
  */
8
- export type SurfaceProps = ResponsiveSx & {
9
- /** Stacked vertically, from the leading edge. */
10
- children?: ReactNode
11
- /** iOS wraps the content in a `GlassEffectContainer`; Android raises it to a tonal elevation of `3`. */
12
- glass?: boolean
13
- /** Test identifier, forwarded to the native view. */
14
- testID?: string
15
- }
8
+ export type SurfaceProps = ResponsiveSx &
9
+ AccessibilityProps & {
10
+ /** Stacked vertically, from the leading edge. */
11
+ children?: ReactNode
12
+ /** iOS wraps the content in a `GlassEffectContainer`; Android raises it to a tonal elevation of `3`. */
13
+ glass?: boolean
14
+ /** Test identifier, forwarded to the native view. */
15
+ testID?: string
16
+ }
16
17
 
17
18
  export type SurfaceComponent = (props: SurfaceProps) => ReactNode
@@ -34,7 +34,7 @@ export const Switch: SwitchComponent = ({disabled = false, label, onValueChange,
34
34
  <ComposeSwitch
35
35
  colors={{
36
36
  checkedBorderColor: theme.color('fill.accent.primary'),
37
- checkedThumbColor: theme.color('content.on.bg'),
37
+ checkedThumbColor: theme.color('content.on.accent'),
38
38
  checkedTrackColor: theme.color('fill.accent.primary'),
39
39
  }}
40
40
  enabled={!disabled}
@@ -1,6 +1,6 @@
1
1
  import {Text as ComposeText} from '@expo/ui/jetpack-compose'
2
2
  import {testID as testIdModifier} from '@expo/ui/jetpack-compose/modifiers'
3
- import {useSxModifiers} from '@hzblj/kara-sx'
3
+ import {splitAccessibility, useAccessibilityModifiers, useSxModifiers} from '@hzblj/kara-sx'
4
4
  import {useAppTheme} from '@hzblj/kara-theme'
5
5
  import {useMemo} from 'react'
6
6
 
@@ -33,15 +33,17 @@ export const Text: TextComponent = ({
33
33
  testID,
34
34
  transform,
35
35
  variant = 'body',
36
- ...sx
36
+ ...props
37
37
  }: TextProps) => {
38
38
  useHostGuard('Text')
39
39
  const theme = useAppTheme()
40
40
  const style = theme.typography[variant]
41
+ const [accessibility, sx] = splitAccessibility(props)
41
42
  const sxModifiers = useSxModifiers(sx)
43
+ const accessibilityModifiers = useAccessibilityModifiers(accessibility)
42
44
  const modifiers = useMemo(
43
- () => (testID ? [...sxModifiers, testIdModifier(testID)] : sxModifiers),
44
- [sxModifiers, testID]
45
+ () => [...sxModifiers, ...(testID ? [testIdModifier(testID)] : []), ...accessibilityModifiers],
46
+ [accessibilityModifiers, sxModifiers, testID]
45
47
  )
46
48
 
47
49
  return (
@@ -10,7 +10,7 @@ import {
10
10
  underline,
11
11
  } from '@expo/ui/swift-ui/modifiers'
12
12
  import {scaledFont} from '@hzblj/kara-native/swift-ui'
13
- import {type FrameAlignment, useSxModifiers} from '@hzblj/kara-sx'
13
+ import {type FrameAlignment, splitAccessibility, useAccessibilityModifiers, useSxModifiers} from '@hzblj/kara-sx'
14
14
  import {useAppTheme} from '@hzblj/kara-theme'
15
15
  import {useMemo} from 'react'
16
16
 
@@ -39,13 +39,15 @@ export const Text: TextComponent = ({
39
39
  testID,
40
40
  transform,
41
41
  variant = 'body',
42
- ...sx
42
+ ...props
43
43
  }: TextProps) => {
44
44
  useHostGuard('Text')
45
45
  const theme = useAppTheme()
46
46
  const style = theme.typography[variant]
47
47
  // After the text's own modifiers: the font and colour belong to the glyphs, the padding and frame to the box around.
48
+ const [accessibility, sx] = splitAccessibility(props)
48
49
  const sxModifiers = useSxModifiers(sx, FRAME_ALIGNMENT[align ?? 'left'])
50
+ const accessibilityModifiers = useAccessibilityModifiers(accessibility)
49
51
 
50
52
  const modifiers = useMemo(() => {
51
53
  const list: ModifierConfig[] = [
@@ -76,8 +78,8 @@ export const Text: TextComponent = ({
76
78
  list.push(lineLimit(numberOfLines))
77
79
  }
78
80
 
79
- return [...list, ...sxModifiers]
80
- }, [align, color, decoration, numberOfLines, style, sxModifiers, theme, transform])
81
+ return [...list, ...sxModifiers, ...accessibilityModifiers]
82
+ }, [accessibilityModifiers, align, color, decoration, numberOfLines, style, sxModifiers, theme, transform])
81
83
 
82
84
  return (
83
85
  <SwiftUIText modifiers={modifiers} testID={testID}>
@@ -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>