@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
@@ -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 {timing} from '@hzblj/kara-sx'
2
+ import {opacity} from '@hzblj/kara-theme'
2
3
  import {cloneElement, type ReactNode, useCallback, useRef} from 'react'
3
4
  import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent} from 'react-native'
4
5
  import {type SharedValue, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'
5
6
 
6
- import {ABox, Box} from '../../atoms/box'
7
+ import {Box} from '../../atoms/box'
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
 
@@ -23,7 +24,7 @@ const EdgeBar = ({children, filled}: Bar) => {
23
24
 
24
25
  return (
25
26
  <Box>
26
- <ABox
27
+ <Box
27
28
  bg="surface.secondary"
28
29
  bottom={0}
29
30
  left={0}
@@ -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>
@@ -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}
@@ -9,6 +9,11 @@ import type {MenuAction} from '../menu'
9
9
  * label of a SwiftUI `Menu`. A native atom: it renders inside `<Screen>` or `<Island>`.
10
10
  */
11
11
  export type FabMenuProps = {
12
+ /**
13
+ * What VoiceOver and TalkBack say for the button, since an icon has no words of its own: "Create". Required for that
14
+ * reason.
15
+ */
16
+ accessibilityLabel: string
12
17
  /** The actions the button unfolds into. */
13
18
  actions: MenuAction[]
14
19
  /** The closed button's icon, one of the app's. A plus by default. */
@@ -1,42 +1,146 @@
1
- import {FadeInDown, FadeOutDown} from 'react-native-reanimated'
1
+ import {type SxEntryExit, timing} from '@hzblj/kara-sx'
2
+ import {spacing, useReducedMotion} from '@hzblj/kara-theme'
3
+ import {useEffect, useRef} from 'react'
4
+ import {AccessibilityInfo, Platform} from 'react-native'
5
+ import {type EntryExitAnimationFunction, withTiming} from 'react-native-reanimated'
2
6
 
3
- import {ABox} from '../../atoms/box'
7
+ import {Box} from '../../atoms/box'
4
8
  import {Pressable} from '../../atoms/pressable'
5
9
  import {RnText} from '../../atoms/rn-text'
6
- import type {SnackbarComponent, SnackbarProps} from './snackbar.types'
10
+ import {RnGlyph} from '../../glyph'
11
+ import type {SnackbarAnimation, SnackbarComponent, SnackbarDuration, SnackbarProps} from './snackbar.types'
12
+
13
+ /** Material's `SnackbarDuration.Short` and `Long`. */
14
+ const DURATIONS = {long: 10_000, short: 4_000} as const
15
+
16
+ const PLATFORM_ANIMATION: SnackbarAnimation = Platform.OS === 'android' ? 'scale' : 'slide'
17
+
18
+ /** The close glyph at the label's height: an SF Symbol fills its box, Material's icon sits inside a padded one. */
19
+ const CLOSE_SIZE = Platform.OS === 'ios' ? 's12' : 's16'
20
+
21
+ const GROW = timing('quick')
22
+
23
+ /** Material's snackbar enter: it fades in while it grows from 80 %, in 150 ms. Further than kara's `scale` preset. */
24
+ const scaleIn: EntryExitAnimationFunction = () => {
25
+ 'worklet'
26
+
27
+ return {
28
+ animations: {opacity: withTiming(1, GROW), transform: [{scale: withTiming(1, GROW)}]},
29
+ initialValues: {opacity: 0, transform: [{scale: 0.8}]},
30
+ }
31
+ }
32
+
33
+ type Transition = {entering: SxEntryExit; exiting: SxEntryExit}
34
+
35
+ /**
36
+ * kara's motion presets, which arrive in `fast`, leave in `quick` and fade with motion reduced by themselves. A slide
37
+ * travels further than a preset's default: in from the edge it sits on, and back out toward it.
38
+ */
39
+ const TRANSITIONS = {
40
+ fade: {entering: 'fade', exiting: 'fade'},
41
+ // Material's exit is a 75 ms fade: `micro`.
42
+ scale: {entering: scaleIn, exiting: {duration: 'micro', preset: 'fade'}},
43
+ slideBottom: {entering: {distance: 's24', preset: 'fadeUp'}, exiting: {distance: 's24', preset: 'fadeDown'}},
44
+ slideTop: {entering: {distance: 's24', preset: 'fadeDown'}, exiting: {distance: 's24', preset: 'fadeUp'}},
45
+ } as const satisfies Record<string, Transition>
46
+
47
+ const transitionsFor = (animation: SnackbarAnimation, position: 'bottom' | 'top'): Transition => {
48
+ if (animation === 'slide') {
49
+ return position === 'top' ? TRANSITIONS.slideTop : TRANSITIONS.slideBottom
50
+ }
51
+
52
+ return TRANSITIONS[animation]
53
+ }
54
+
55
+ const millisecondsOf = (duration: SnackbarDuration) =>
56
+ typeof duration === 'number' ? duration : duration === 'indefinite' ? undefined : DURATIONS[duration]
57
+
58
+ export const Snackbar: SnackbarComponent = ({
59
+ actionLabel,
60
+ actionOnNewLine = false,
61
+ animation = PLATFORM_ANIMATION,
62
+ dismissible = false,
63
+ duration = 'indefinite',
64
+ entering,
65
+ exiting,
66
+ message,
67
+ offset = 's24',
68
+ onAction,
69
+ onDismiss,
70
+ position = 'bottom',
71
+ visible,
72
+ }: SnackbarProps) => {
73
+ const reduced = useReducedMotion()
74
+ // A caller's inline `onDismiss` is a new function every render; the timer must not restart for it.
75
+ const dismiss = useRef(onDismiss)
76
+ dismiss.current = onDismiss
77
+
78
+ // biome-ignore lint/correctness/useExhaustiveDependencies: a new `message` restarts the timer, as a new snackbar
79
+ useEffect(() => {
80
+ const milliseconds = millisecondsOf(duration)
81
+
82
+ if (!visible || milliseconds === undefined) {
83
+ return
84
+ }
85
+
86
+ const timer = setTimeout(() => dismiss.current?.(), milliseconds)
87
+
88
+ return () => clearTimeout(timer)
89
+ }, [duration, message, visible])
90
+
91
+ // Material's snackbar is announced; a React Native view is not unless asked.
92
+ useEffect(() => {
93
+ if (visible) {
94
+ AccessibilityInfo.announceForAccessibility(message)
95
+ }
96
+ }, [message, visible])
7
97
 
8
- export const Snackbar: SnackbarComponent = ({actionLabel, message, onAction, visible}: SnackbarProps) => {
9
98
  if (!visible) {
10
99
  return null
11
100
  }
12
101
 
102
+ const edge = position === 'top' ? {top: spacing[offset]} : {bottom: spacing[offset]}
103
+ const hasTrailing = Boolean(actionLabel) || dismissible
104
+ // The presets fade with motion reduced by themselves; Material's scale is a worklet of its own, so it is picked out.
105
+ const preset = transitionsFor(reduced && animation === 'scale' ? 'fade' : animation, position)
106
+
13
107
  return (
14
- <ABox
15
- align="center"
108
+ <Box
109
+ align={actionOnNewLine ? 'stretch' : 'center'}
16
110
  bg="surface.high.contrast"
17
111
  borderRadius="medium"
18
- bottom={24}
19
- entering={FadeInDown}
20
- exiting={FadeOutDown}
21
- flexDirection="row"
22
- gap="s12"
23
- left={16}
112
+ flexDirection={actionOnNewLine ? 'column' : 'row'}
113
+ gap={actionOnNewLine ? 's8' : 's12'}
114
+ left={spacing.s16}
24
115
  position="absolute"
25
116
  px="s16"
26
117
  py="s12"
27
- right={16}
118
+ right={spacing.s16}
28
119
  shadow="snackbar"
120
+ {...edge}
121
+ entering={entering ?? preset.entering}
122
+ exiting={exiting ?? preset.exiting}
29
123
  >
30
- <RnText color="content.on.high.contrast" variant="footnote">
124
+ {/* It takes the room, so the action sits at the trailing end, as far in from it as the message is from the start. */}
125
+ <RnText color="content.on.high.contrast" flex={actionOnNewLine ? undefined : 1} variant="footnote">
31
126
  {message}
32
127
  </RnText>
33
- {actionLabel ? (
34
- <Pressable onPress={onAction}>
35
- <RnText color="content.accent" variant="footnoteMedium">
36
- {actionLabel}
37
- </RnText>
38
- </Pressable>
128
+ {hasTrailing ? (
129
+ <Box align="center" alignSelf={actionOnNewLine ? 'flex-end' : undefined} flexDirection="row" gap="s16">
130
+ {actionLabel ? (
131
+ <Pressable onPress={onAction} pressEffect="opacity" ripple={false}>
132
+ <RnText color="content.accent" variant="footnoteMedium">
133
+ {actionLabel}
134
+ </RnText>
135
+ </Pressable>
136
+ ) : null}
137
+ {dismissible ? (
138
+ <Pressable accessibilityLabel="Dismiss" onPress={onDismiss} pressEffect="opacity" ripple={false}>
139
+ <RnGlyph color="content.on.high.contrast" name="close" size={CLOSE_SIZE} />
140
+ </Pressable>
141
+ ) : null}
142
+ </Box>
39
143
  ) : null}
40
- </ABox>
144
+ </Box>
41
145
  )
42
146
  }
@@ -1,17 +1,51 @@
1
+ import type {SxEntryExit} from '@hzblj/kara-sx'
2
+ import type {SpaceSize} from '@hzblj/kara-theme'
1
3
  import type {ReactNode} from 'react'
2
4
 
5
+ /** How it comes and goes. */
6
+ export type SnackbarAnimation = 'fade' | 'scale' | 'slide'
7
+
8
+ /** How long it stays: Material's short (4 s) and long (10 s), never, or a number of milliseconds. */
9
+ export type SnackbarDuration = 'indefinite' | 'long' | 'short' | number
10
+
11
+ /** The edge of its parent it sits on. */
12
+ export type SnackbarPosition = 'bottom' | 'top'
13
+
3
14
  /**
4
- * A short message laid over the bottom of its parent, with an optional action. The React Native tier, drawn the same on
5
- * both platforms: it needs no host, and it never hides itself.
15
+ * A short message laid over an edge of its parent, with an optional action at its trailing end. The React Native tier,
16
+ * drawn the same on both platforms (iOS has no snackbar of its own), so it needs no host. Material's options are all
17
+ * here: a duration, a dismiss button, the action on a line of its own. It is announced to VoiceOver and TalkBack when
18
+ * it appears.
6
19
  */
7
20
  export type SnackbarProps = {
8
- /** The action button's text, on the trailing side. No button without it. */
21
+ /** The action button's text, at the trailing end. No button without it. */
9
22
  actionLabel?: string
23
+ /** Puts the action under the message, at its trailing end: Material's layout for a long message or action. */
24
+ actionOnNewLine?: boolean
25
+ /**
26
+ * How it comes and goes. `scale` is Material's own (a fade and a scale from 80 %), `slide` rises from its edge as it
27
+ * fades in, `fade` only fades. `scale` on Android and `slide` on iOS by default. With motion reduced, it fades.
28
+ */
29
+ animation?: SnackbarAnimation
30
+ /** A close button at the trailing end, Material's dismiss action; it calls `onDismiss`. */
31
+ dismissible?: boolean
32
+ /** How long it stays up before `onDismiss` is called. `indefinite` by default: it never hides itself. */
33
+ duration?: SnackbarDuration
34
+ /** How it arrives instead: a motion preset (`'scale'`), or Reanimated's own, e.g. `FadeInDown.springify()`. It wins over `animation`. */
35
+ entering?: SxEntryExit
36
+ /** How it leaves instead: a motion preset, or Reanimated's own. It wins over `animation`. */
37
+ exiting?: SxEntryExit
10
38
  /** The message. */
11
39
  message: string
40
+ /** Between the snackbar and its edge, a space token. `s24` by default; raise it above a tab bar. */
41
+ offset?: SpaceSize
12
42
  /** The action was pressed. The snackbar stays up; turn `visible` off to hide it. */
13
43
  onAction?: () => void
14
- /** Shows the snackbar while true, fading it in and out. It sets no timer of its own. */
44
+ /** It asks to go: its `duration` ran out, or its dismiss button was pressed. Turn `visible` off to hide it. */
45
+ onDismiss?: () => void
46
+ /** The edge of its parent it sits on. `bottom` by default, Material's place for it. */
47
+ position?: SnackbarPosition
48
+ /** Shows the snackbar while true, animating it in and out. */
15
49
  visible: boolean
16
50
  }
17
51
 
@@ -49,7 +49,7 @@ type ActionButtonProps = {action: SwipeAction; onDone: () => void}
49
49
  const ActionButton = ({action, onDone}: ActionButtonProps) => {
50
50
  const theme = useAppTheme()
51
51
  const fill = action.tint ?? (action.destructive ? 'fill.destructive.primary' : 'fill.accent.primary')
52
- const content = !action.tint && action.destructive ? 'content.on.destructive' : 'content.on.bg'
52
+ const content = !action.tint && action.destructive ? 'content.on.destructive' : 'content.on.accent'
53
53
 
54
54
  const onClick = useCallback(() => {
55
55
  onDone()
@@ -1,24 +1,37 @@
1
1
  import {clickable, type ModifierConfig, testID as testIdModifier} from '@expo/ui/jetpack-compose/modifiers'
2
- import {type ResponsiveSx, useSxModifiers} from '@hzblj/kara-sx'
2
+ import {
3
+ type AccessibilityProps,
4
+ type ResponsiveSx,
5
+ splitAccessibility,
6
+ useAccessibilityModifiers,
7
+ useSxModifiers,
8
+ } from '@hzblj/kara-sx'
3
9
  import {useMemo} from 'react'
4
10
 
5
11
  /**
6
- * A Compose stack's modifiers in the order a pressable one needs: its frame, shape and background, then the press,
7
- * then its padding. Compose clips a `clickable` to what comes after it, so with the padding first the ripple would stop
8
- * at the padding and a row pressed by its edge would light up only in the middle.
12
+ * A Compose stack's modifiers in the order a pressable one needs: its frame, shape and background, then the press, its
13
+ * semantics, then its padding. Compose clips a `clickable` to what comes after it, so with the padding first the ripple
14
+ * would stop at the padding and a row pressed by its edge would light up only in the middle. The semantics come after
15
+ * the click, because a label that replaces the children's clears whatever semantics are inside it.
9
16
  */
10
- export const useStackModifiers = (sx: ResponsiveSx, onPress?: () => void, testID?: string): ModifierConfig[] => {
11
- const {p, pb, pl, pr, pt, px, py, ...frame} = sx
17
+ export const useStackModifiers = (
18
+ props: ResponsiveSx & AccessibilityProps,
19
+ onPress?: () => void,
20
+ testID?: string
21
+ ): ModifierConfig[] => {
22
+ const [accessibility, {p, pb, pl, pr, pt, px, py, ...frame}] = splitAccessibility(props)
12
23
  const frameModifiers = useSxModifiers(frame)
13
24
  const paddingModifiers = useSxModifiers({p, pb, pl, pr, pt, px, py})
25
+ const accessibilityModifiers = useAccessibilityModifiers(accessibility, onPress !== undefined)
14
26
 
15
27
  return useMemo(
16
28
  () => [
17
29
  ...frameModifiers,
18
30
  ...(testID ? [testIdModifier(testID)] : []),
19
31
  ...(onPress ? [clickable(onPress)] : []),
32
+ ...accessibilityModifiers,
20
33
  ...paddingModifiers,
21
34
  ],
22
- [frameModifiers, onPress, paddingModifiers, testID]
35
+ [accessibilityModifiers, frameModifiers, onPress, paddingModifiers, testID]
23
36
  )
24
37
  }
@@ -1,6 +0,0 @@
1
- "use strict";
2
-
3
- import Animated from 'react-native-reanimated';
4
- import { Box } from "./box.js";
5
- export const ABox = Animated.createAnimatedComponent(Box);
6
- //# sourceMappingURL=a-box.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["Animated","Box","ABox","createAnimatedComponent"],"sourceRoot":"../../../../src","sources":["atoms/box/a-box.ts"],"mappings":";;AAAA,OAAOA,QAAQ,MAAM,yBAAyB;AAE9C,SAAQC,GAAG,QAAO,UAAO;AAEzB,OAAO,MAAMC,IAAI,GAAGF,QAAQ,CAACG,uBAAuB,CAACF,GAAG,CAAC","ignoreList":[]}