@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,16 +1,16 @@
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
  export type VStackAlign = 'center' | 'end' | 'start';
5
5
  /** Stacks its children top to bottom: SwiftUI `VStack` / Compose `Column`. */
6
- export type VStackProps = ResponsiveSx & {
6
+ export type VStackProps = ResponsiveSx & AccessibilityProps & {
7
7
  /** The children's horizontal alignment. `start` by default. */
8
8
  align?: VStackAlign;
9
9
  /** Laid out top to bottom, `spacing` apart. */
10
10
  children?: ReactNode;
11
11
  /**
12
12
  * Makes the whole stack pressable. iOS adds a tap gesture with no press feedback; Android a `clickable` whose ripple
13
- * fills the stack's shape.
13
+ * fills the stack's shape. Screen readers then read the stack as one button.
14
14
  */
15
15
  onPress?: () => void;
16
16
  /** The space between children. */
@@ -1,15 +1,15 @@
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
  export type ZStackAlign = 'bottom' | 'center' | 'top';
4
4
  /** Layers its children on one another: SwiftUI `ZStack` / Compose `Box`. */
5
- export type ZStackProps = ResponsiveSx & {
5
+ export type ZStackProps = ResponsiveSx & AccessibilityProps & {
6
6
  /** Where the children sit vertically; they are always centred horizontally. `center` by default. */
7
7
  align?: ZStackAlign;
8
8
  /** Drawn in order, the last on top. */
9
9
  children?: ReactNode;
10
10
  /**
11
11
  * Makes the whole stack pressable. iOS adds a tap gesture with no press feedback; Android a `clickable` whose ripple
12
- * fills the stack's shape.
12
+ * fills the stack's shape. Screen readers then read the stack as one button.
13
13
  */
14
14
  onPress?: () => void;
15
15
  /** Test identifier, forwarded to the native view. */
@@ -6,6 +6,8 @@ import type { ReactNode } from 'react';
6
6
  * their own icons with `Icon` / `RnIcon`.
7
7
  */
8
8
  export type GlyphProps = {
9
+ /** What a screen reader says. Unset, the glyph is decorative and hidden from it. */
10
+ accessibilityLabel?: string;
9
11
  color?: ColorToken;
10
12
  name: GlyphName;
11
13
  size?: SpaceSize;
@@ -1,11 +1,13 @@
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
  * A label on the leading edge and its value on the trailing edge: the settings/detail row.
6
- * Pass `children` instead of `value` when the trailing side is a control rather than text.
7
+ * Pass `children` instead of `value` when the trailing side is a control rather than text. With a `value`, screen
8
+ * readers read the row as one element ("Version, 2.4"); with a control, the control stays its own.
7
9
  */
8
- export type LabeledContentProps = {
10
+ export type LabeledContentProps = AccessibilityProps & {
9
11
  /** The trailing side when it is a control, in place of `value`. */
10
12
  children?: ReactNode;
11
13
  /** One of the app's icons, before the label, in the secondary colour. */
@@ -1,8 +1,9 @@
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
  import type { IconName } from '../../atoms/icon';
4
5
  /** Opens an external URL in the platform browser or the app that claims the scheme. */
5
- export type LinkProps = {
6
+ export type LinkProps = AccessibilityProps & {
6
7
  /** Colours the label and the icon. `content.accent` by default. */
7
8
  color?: ColorToken;
8
9
  /** One of the app's icons, before the label. */
@@ -1,11 +1,13 @@
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
- * A row with an optional icon, a title and subtitle, and a chevron when it can be pressed. A native atom: it renders
6
- * inside `<Screen>` or `<Island>`; `RnListItem` is the same row without a host.
6
+ * A row with an optional icon, a title and subtitle, and a chevron when it can be pressed. Screen readers read it as one
7
+ * element (a button when it is pressable). A native atom: it renders inside `<Screen>` or `<Island>`; `RnListItem` is
8
+ * the same row without a host.
7
9
  */
8
- export type ListItemProps = {
10
+ export type ListItemProps = AccessibilityProps & {
9
11
  /** One of the app's icons, before the title, in the secondary colour. */
10
12
  icon?: IconName;
11
13
  /** Makes the row pressable and adds a chevron on the trailing edge. */
@@ -2,6 +2,6 @@ import type { StepperComponent } from './stepper.types';
2
2
  /**
3
3
  * SwiftUI's `Stepper` at the end of a row, the label leading and the value just before the control — the way iOS's own
4
4
  * settings show a stepped value. The value rolls to the next one (`numericText`), up or down with the change, in
5
- * monospaced digits so the row does not jump.
5
+ * monospaced digits so the row does not jump. With motion reduced it cross-fades instead.
6
6
  */
7
7
  export declare const Stepper: StepperComponent;
@@ -0,0 +1,8 @@
1
+ import { type DurationSize, type EasingName } from '@hzblj/kara-theme';
2
+ import { type WithTimingConfig } from 'react-native-reanimated';
3
+ /**
4
+ * A Reanimated timing from the motion tokens, `smoothOut` unless told otherwise. `ReduceMotion.Never`, because a
5
+ * component has applied `useReducedMotion` before it animates: what is left (a fade, a dim) is meant to run, and
6
+ * Reanimated's own check reads the setting once at launch and never hears of `KaraProvider reduceMotion`.
7
+ */
8
+ export declare const timing: (size: DurationSize, curve?: EasingName) => WithTimingConfig;
@@ -7,6 +7,11 @@ import type { MenuAction } from '../menu';
7
7
  * label of a SwiftUI `Menu`. A native atom: it renders inside `<Screen>` or `<Island>`.
8
8
  */
9
9
  export type FabMenuProps = {
10
+ /**
11
+ * What VoiceOver and TalkBack say for the button, since an icon has no words of its own: "Create". Required for that
12
+ * reason.
13
+ */
14
+ accessibilityLabel: string;
10
15
  /** The actions the button unfolds into. */
11
16
  actions: MenuAction[];
12
17
  /** The closed button's icon, one of the app's. A plus by default. */
@@ -1,16 +1,49 @@
1
- import type { ReactNode } from 'react';
1
+ import type { SpaceSize } from '@hzblj/kara-theme';
2
+ import type { ComponentProps, ReactNode } from 'react';
3
+ import type Animated from 'react-native-reanimated';
4
+ type LayoutAnimation = ComponentProps<typeof Animated.View>['entering'];
5
+ /** How it comes and goes. */
6
+ export type SnackbarAnimation = 'fade' | 'scale' | 'slide';
7
+ /** How long it stays: Material's short (4 s) and long (10 s), never, or a number of milliseconds. */
8
+ export type SnackbarDuration = 'indefinite' | 'long' | 'short' | number;
9
+ /** The edge of its parent it sits on. */
10
+ export type SnackbarPosition = 'bottom' | 'top';
2
11
  /**
3
- * A short message laid over the bottom of its parent, with an optional action. The React Native tier, drawn the same on
4
- * both platforms: it needs no host, and it never hides itself.
12
+ * A short message laid over an edge of its parent, with an optional action at its trailing end. The React Native tier,
13
+ * drawn the same on both platforms (iOS has no snackbar of its own), so it needs no host. Material's options are all
14
+ * here: a duration, a dismiss button, the action on a line of its own. It is announced to VoiceOver and TalkBack when
15
+ * it appears.
5
16
  */
6
17
  export type SnackbarProps = {
7
- /** The action button's text, on the trailing side. No button without it. */
18
+ /** The action button's text, at the trailing end. No button without it. */
8
19
  actionLabel?: string;
20
+ /** Puts the action under the message, at its trailing end: Material's layout for a long message or action. */
21
+ actionOnNewLine?: boolean;
22
+ /**
23
+ * How it comes and goes. `scale` is Material's own (a fade and a scale from 80 %), `slide` rises from its edge as it
24
+ * fades in, `fade` only fades. `scale` on Android and `slide` on iOS by default. With motion reduced, it fades.
25
+ */
26
+ animation?: SnackbarAnimation;
27
+ /** A close button at the trailing end, Material's dismiss action; it calls `onDismiss`. */
28
+ dismissible?: boolean;
29
+ /** How long it stays up before `onDismiss` is called. `indefinite` by default: it never hides itself. */
30
+ duration?: SnackbarDuration;
31
+ /** A Reanimated entering animation of your own, e.g. `FadeInDown.springify()`. It wins over `animation`. */
32
+ entering?: LayoutAnimation;
33
+ /** A Reanimated exiting animation of your own. It wins over `animation`. */
34
+ exiting?: LayoutAnimation;
9
35
  /** The message. */
10
36
  message: string;
37
+ /** Between the snackbar and its edge, a space token. `s24` by default; raise it above a tab bar. */
38
+ offset?: SpaceSize;
11
39
  /** The action was pressed. The snackbar stays up; turn `visible` off to hide it. */
12
40
  onAction?: () => void;
13
- /** Shows the snackbar while true, fading it in and out. It sets no timer of its own. */
41
+ /** It asks to go: its `duration` ran out, or its dismiss button was pressed. Turn `visible` off to hide it. */
42
+ onDismiss?: () => void;
43
+ /** The edge of its parent it sits on. `bottom` by default, Material's place for it. */
44
+ position?: SnackbarPosition;
45
+ /** Shows the snackbar while true, animating it in and out. */
14
46
  visible: boolean;
15
47
  };
16
48
  export type SnackbarComponent = (props: SnackbarProps) => ReactNode;
49
+ export {};
@@ -1,8 +1,9 @@
1
1
  import { type ModifierConfig } from '@expo/ui/jetpack-compose/modifiers';
2
- import { type ResponsiveSx } from '@hzblj/kara-sx';
2
+ import { type AccessibilityProps, type ResponsiveSx } from '@hzblj/kara-sx';
3
3
  /**
4
- * A Compose stack's modifiers in the order a pressable one needs: its frame, shape and background, then the press,
5
- * then its padding. Compose clips a `clickable` to what comes after it, so with the padding first the ripple would stop
6
- * at the padding and a row pressed by its edge would light up only in the middle.
4
+ * A Compose stack's modifiers in the order a pressable one needs: its frame, shape and background, then the press, its
5
+ * semantics, then its padding. Compose clips a `clickable` to what comes after it, so with the padding first the ripple
6
+ * would stop at the padding and a row pressed by its edge would light up only in the middle. The semantics come after
7
+ * the click, because a label that replaces the children's clears whatever semantics are inside it.
7
8
  */
8
- export declare const useStackModifiers: (sx: ResponsiveSx, onPress?: () => void, testID?: string) => ModifierConfig[];
9
+ export declare const useStackModifiers: (props: ResponsiveSx & AccessibilityProps, onPress?: () => void, testID?: string) => ModifierConfig[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hzblj/kara-components",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "The atoms, molecules, organisms and templates of @hzblj/kara-ui — real SwiftUI and Jetpack Compose views through @expo/ui.",
5
5
  "author": "Jan Blazej",
6
6
  "license": "MIT",
@@ -59,9 +59,9 @@
59
59
  ]
60
60
  },
61
61
  "dependencies": {
62
- "@hzblj/kara-native": "^0.10.0",
63
- "@hzblj/kara-sx": "^0.10.0",
64
- "@hzblj/kara-theme": "^0.10.0",
62
+ "@hzblj/kara-native": "^0.11.0",
63
+ "@hzblj/kara-sx": "^0.11.0",
64
+ "@hzblj/kara-theme": "^0.11.0",
65
65
  "sf-symbols-typescript": "^2.2.0"
66
66
  },
67
67
  "peerDependencies": {
@@ -1,5 +1,6 @@
1
1
  import {Host, LoadingIndicator} from '@expo/ui/jetpack-compose'
2
2
  import {fillMaxSize, testID as testIdModifier} from '@expo/ui/jetpack-compose/modifiers'
3
+ import {useAccessibilityModifiers} from '@hzblj/kara-sx'
3
4
  import {useAppTheme} from '@hzblj/kara-theme'
4
5
  import {useMemo} from 'react'
5
6
  import {StyleSheet} from 'react-native'
@@ -17,12 +18,17 @@ const styles = StyleSheet.create({
17
18
  * anywhere and takes its place in the first frame.
18
19
  */
19
20
  export const ActivityIndicator: ActivityIndicatorComponent = ({
21
+ accessibilityLabel,
20
22
  color = 'fill.accent.primary',
21
23
  size = 'small',
22
24
  testID,
23
25
  }: ActivityIndicatorProps) => {
24
26
  const theme = useAppTheme()
25
- const modifiers = useMemo(() => (testID ? [fillMaxSize(), testIdModifier(testID)] : [fillMaxSize()]), [testID])
27
+ const accessibilityModifiers = useAccessibilityModifiers({accessibilityLabel})
28
+ const modifiers = useMemo(
29
+ () => [fillMaxSize(), ...(testID ? [testIdModifier(testID)] : []), ...accessibilityModifiers],
30
+ [accessibilityModifiers, testID]
31
+ )
26
32
 
27
33
  return (
28
34
  <Host colorScheme={theme.dark ? 'dark' : 'light'} seedColor={theme.materialSeed} style={styles[size]}>
@@ -5,11 +5,19 @@ import type {ActivityIndicatorComponent, ActivityIndicatorProps} from './activit
5
5
 
6
6
  /** React Native's spinner, which on iOS is UIKit's own `UIActivityIndicatorView`. */
7
7
  export const ActivityIndicator: ActivityIndicatorComponent = ({
8
+ accessibilityLabel,
8
9
  color = 'fill.accent.primary',
9
10
  size = 'small',
10
11
  testID,
11
12
  }: ActivityIndicatorProps) => {
12
13
  const theme = useAppTheme()
13
14
 
14
- return <RNActivityIndicator color={theme.color(color)} size={size} testID={testID} />
15
+ return (
16
+ <RNActivityIndicator
17
+ accessibilityLabel={accessibilityLabel}
18
+ color={theme.color(color)}
19
+ size={size}
20
+ testID={testID}
21
+ />
22
+ )
15
23
  }
@@ -9,6 +9,8 @@ export type ActivityIndicatorSize = 'large' | 'small'
9
9
  * Android, so it renders anywhere. For how far along something is, use `Progress`.
10
10
  */
11
11
  export type ActivityIndicatorProps = {
12
+ /** What is loading, for VoiceOver and TalkBack: "Loading photos". Unset, the platform's own word ("In progress"). */
13
+ accessibilityLabel?: string
12
14
  /** Its colour. `fill.accent.primary` by default. */
13
15
  color?: ColorToken
14
16
  /** `small` sits in a row or a button, `large` holds a screen that is loading. `small` by default. */
@@ -17,11 +17,15 @@ const VARIANT = {
17
17
  sm: 'caption',
18
18
  } as const
19
19
 
20
- export const Avatar: AvatarComponent = ({initials, size = 'md', source, testID}: AvatarProps) => {
20
+ export const Avatar: AvatarComponent = ({accessibilityLabel, initials, size = 'md', source, testID}: AvatarProps) => {
21
21
  const diameter = DIAMETER[size]
22
22
 
23
23
  return (
24
24
  <Box
25
+ aria-hidden={accessibilityLabel === undefined}
26
+ accessibilityLabel={accessibilityLabel}
27
+ accessibilityRole={accessibilityLabel === undefined ? undefined : 'image'}
28
+ accessible={accessibilityLabel !== undefined}
25
29
  align="center"
26
30
  bg="fill.tertiary"
27
31
  borderRadius="full"
@@ -5,6 +5,11 @@ export type AvatarSize = 'lg' | 'md' | 'sm'
5
5
 
6
6
  /** A round avatar: the image in `source`, or `initials` without one. The React Native tier: it needs no host. */
7
7
  export type AvatarProps = {
8
+ /**
9
+ * Who it is, for VoiceOver and TalkBack: "Jan Blazej". Unset, the avatar is decorative and hidden from them, as one
10
+ * beside the person's name should be.
11
+ */
12
+ accessibilityLabel?: string
8
13
  /** Shown in place of the image when there is no `source`. */
9
14
  initials?: string
10
15
  /** The diameter: `sm` is `s24`, `md` `s40`, `lg` `s64`. `md` by default. */
@@ -9,7 +9,7 @@ type ButtonTokens = {
9
9
  }
10
10
 
11
11
  const TOKENS = {
12
- accent: {content: 'content.on.bg', fill: 'fill.accent.primary', shadow: 'btn.accent'},
12
+ accent: {content: 'content.on.accent', fill: 'fill.accent.primary', shadow: 'btn.accent'},
13
13
  destructive: {content: 'content.on.destructive', fill: 'fill.destructive.primary', shadow: 'btn.destructive'},
14
14
  glass: {content: 'content.primary', fill: 'btn.bg.glass.base', shadow: 'btn.glass'},
15
15
  secondary: {content: 'content.primary', fill: 'btn.bg.secondary.base', shadow: 'btn.secondary'},
@@ -81,4 +81,4 @@ const currentHex = (token: ColorToken, theme: Theme): unknown => {
81
81
  * content colour over a pale one (`fill.accent.base`) — the light container tokens pair with it in both appearances.
82
82
  */
83
83
  export const contentOnTint = (tint: ColorToken, theme: Theme): ColorToken =>
84
- (luminance(currentHex(tint, theme)) ?? 0) > 0.4 ? 'content.primary' : 'content.on.bg'
84
+ (luminance(currentHex(tint, theme)) ?? 0) > 0.4 ? 'content.primary' : 'content.on.accent'
@@ -1,6 +1,6 @@
1
1
  import {Button as ComposeButton, Text as ComposeText, OutlinedButton} 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 {useAccessibilityModifiers, useSxModifiers} from '@hzblj/kara-sx'
4
4
  import {useAppTheme} from '@hzblj/kara-theme'
5
5
  import {useMemo} from 'react'
6
6
  import {useHostGuard} from '../../host-context'
@@ -16,6 +16,7 @@ import {buttonTokens, contentOnTint} from './button-tokens'
16
16
  * colour, as a prominent style does on iOS. `buttonStyle` and `controlSize` are SwiftUI's alone.
17
17
  */
18
18
  export const Button: ButtonComponent = ({
19
+ accessibilityHint,
19
20
  disabled = false,
20
21
  label,
21
22
  onPress,
@@ -28,9 +29,10 @@ export const Button: ButtonComponent = ({
28
29
  const theme = useAppTheme()
29
30
  const tokens = buttonTokens(variant, disabled, theme)
30
31
  const sxModifiers = useSxModifiers({wide})
32
+ const accessibilityModifiers = useAccessibilityModifiers({accessibilityHint}, accessibilityHint !== undefined)
31
33
  const modifiers = useMemo(
32
- () => (testID ? [...sxModifiers, testIdModifier(testID)] : sxModifiers),
33
- [sxModifiers, testID]
34
+ () => [...sxModifiers, ...(testID ? [testIdModifier(testID)] : []), ...accessibilityModifiers],
35
+ [accessibilityModifiers, sxModifiers, testID]
34
36
  )
35
37
  const style = theme.typography.body
36
38
  const outlined = variant === 'glass' && !tint
@@ -6,6 +6,7 @@ import {
6
6
  glassEffect as glassEffectModifier,
7
7
  tint as tintModifier,
8
8
  } from '@expo/ui/swift-ui/modifiers'
9
+ import {useAccessibilityModifiers} from '@hzblj/kara-sx'
9
10
  import {type ColorToken, useAppTheme} from '@hzblj/kara-theme'
10
11
  import {useMemo} from 'react'
11
12
  import {useHostGuard} from '../../host-context'
@@ -29,6 +30,7 @@ const TINT_BY_VARIANT = {
29
30
  } as const satisfies Record<ButtonVariant, ColorToken | undefined>
30
31
 
31
32
  export const Button: ButtonComponent = ({
33
+ accessibilityHint,
32
34
  buttonStyle,
33
35
  controlSize = 'large',
34
36
  disabled = false,
@@ -59,8 +61,14 @@ export const Button: ButtonComponent = ({
59
61
  // A prominent style — and tinted glass — is coloured by the tint, so a caller's own tint picks the label on it.
60
62
  const content = tint && filled && !disabled ? contentOnTint(tint, theme) : tokens.content
61
63
 
64
+ const accessibilityModifiers = useAccessibilityModifiers({accessibilityHint})
65
+
62
66
  const modifiers = useMemo(() => {
63
- const list = [buttonStyleModifier(glassTint ? 'plain' : style), controlSizeModifier(controlSize)]
67
+ const list = [
68
+ buttonStyleModifier(glassTint ? 'plain' : style),
69
+ controlSizeModifier(controlSize),
70
+ ...accessibilityModifiers,
71
+ ]
64
72
 
65
73
  if (glassTint) {
66
74
  list.push(
@@ -75,7 +83,7 @@ export const Button: ButtonComponent = ({
75
83
  }
76
84
 
77
85
  return list
78
- }, [controlSize, disabled, glassTint, resolvedTint, style, theme])
86
+ }, [accessibilityModifiers, controlSize, disabled, glassTint, resolvedTint, style, theme])
79
87
 
80
88
  return (
81
89
  <SwiftUIButton modifiers={modifiers} onPress={onPress} testID={testID}>
@@ -24,6 +24,8 @@ export type ButtonControlSize = 'extraLarge' | 'large' | 'mini' | 'regular' | 's
24
24
 
25
25
  /** A text button: SwiftUI `Button` on iOS, Material's filled button (outlined for `glass`) on Android. */
26
26
  export type ButtonProps = {
27
+ /** What pressing it does, when the label alone does not say. See `AccessibilityProps`. */
28
+ accessibilityHint?: string
27
29
  /**
28
30
  * Replaces the style the variant picks: `borderedProminent` for `accent` and `destructive`, `bordered` for
29
31
  * `secondary`, `glass` for `glass`. iOS only. Android ignores it.
@@ -1,16 +1,18 @@
1
1
  import {Column} 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 {useMemo} from 'react'
5
5
  import {useHostGuard} from '../../host-context'
6
6
  import type {CardComponent, CardProps} from './card.types'
7
7
 
8
- export const Card: CardComponent = ({children, testID, ...sx}: CardProps) => {
8
+ export const Card: CardComponent = ({children, testID, ...props}: CardProps) => {
9
9
  useHostGuard('Card')
10
+ const [accessibility, sx] = splitAccessibility(props)
10
11
  const sxModifiers = useSxModifiers({bg: 'surface.primary', borderRadius: 'large', p: 's16', shadow: 'card', ...sx})
12
+ const accessibilityModifiers = useAccessibilityModifiers(accessibility)
11
13
  const modifiers = useMemo(
12
- () => (testID ? [...sxModifiers, testIdModifier(testID)] : sxModifiers),
13
- [sxModifiers, testID]
14
+ () => [...sxModifiers, ...(testID ? [testIdModifier(testID)] : []), ...accessibilityModifiers],
15
+ [accessibilityModifiers, sxModifiers, testID]
14
16
  )
15
17
 
16
18
  return <Column modifiers={modifiers}>{children}</Column>
@@ -1,11 +1,15 @@
1
1
  import {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 {CardComponent, CardProps} from './card.types'
5
6
 
6
- export const Card: CardComponent = ({children, testID, ...sx}: CardProps) => {
7
+ export const Card: CardComponent = ({children, testID, ...props}: CardProps) => {
7
8
  useHostGuard('Card')
8
- const modifiers = useSxModifiers({bg: 'surface.primary', borderRadius: 'large', p: 's16', shadow: 'card', ...sx})
9
+ const [accessibility, sx] = splitAccessibility(props)
10
+ const sxModifiers = useSxModifiers({bg: 'surface.primary', borderRadius: 'large', p: 's16', shadow: 'card', ...sx})
11
+ const accessibilityModifiers = useAccessibilityModifiers(accessibility)
12
+ const modifiers = useMemo(() => [...sxModifiers, ...accessibilityModifiers], [accessibilityModifiers, sxModifiers])
9
13
 
10
14
  return (
11
15
  <VStack alignment="leading" modifiers={modifiers} testID={testID}>
@@ -1,15 +1,16 @@
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 raised container: SwiftUI `VStack` / Compose `Column` on `surface.primary`, with a large radius, `s16` padding and
6
6
  * the `card` shadow. Any `sx` prop overrides its default.
7
7
  */
8
- export type CardProps = ResponsiveSx & {
9
- /** Native views, stacked vertically from the leading edge. */
10
- children?: ReactNode
11
- /** Test identifier, forwarded to the native view. */
12
- testID?: string
13
- }
8
+ export type CardProps = ResponsiveSx &
9
+ AccessibilityProps & {
10
+ /** Native views, stacked vertically from the leading edge. */
11
+ children?: ReactNode
12
+ /** Test identifier, forwarded to the native view. */
13
+ testID?: string
14
+ }
14
15
 
15
16
  export type CardComponent = (props: CardProps) => ReactNode
@@ -23,7 +23,7 @@ export const Checkbox: CheckboxComponent = ({disabled = false, label, onValueCha
23
23
  )
24
24
  const control = (
25
25
  <ComposeCheckbox
26
- colors={{checkedColor: theme.color('fill.accent.primary'), checkmarkColor: theme.color('content.on.bg')}}
26
+ colors={{checkedColor: theme.color('fill.accent.primary'), checkmarkColor: theme.color('content.on.accent')}}
27
27
  enabled={!disabled}
28
28
  onCheckedChange={onValueChange ?? NOOP}
29
29
  value={value}
@@ -1,20 +1,22 @@
1
1
  import {FloatingActionButton} from '@expo/ui/jetpack-compose'
2
+ import {useAccessibilityModifiers} from '@hzblj/kara-sx'
2
3
  import {useAppTheme} from '@hzblj/kara-theme'
3
4
 
4
5
  import {useHostGuard} from '../../host-context'
5
6
  import {Icon} from '../icon'
6
7
  import type {FabComponent, FabProps} from './fab.types'
7
8
 
8
- export const Fab: FabComponent = ({name, onPress}: FabProps) => {
9
+ export const Fab: FabComponent = ({accessibilityHint, accessibilityLabel, name, onPress}: FabProps) => {
9
10
  useHostGuard('Fab')
10
11
 
11
12
  const theme = useAppTheme()
13
+ const modifiers = useAccessibilityModifiers({accessibilityHint}, accessibilityHint !== undefined)
12
14
 
13
15
  return (
14
- <FloatingActionButton containerColor={theme.color('fill.accent.primary')} onClick={onPress}>
16
+ <FloatingActionButton containerColor={theme.color('fill.accent.primary')} modifiers={modifiers} onClick={onPress}>
15
17
  {/* `@expo/ui`'s FAB is slot-based: content outside `.Icon` is not composed at all. */}
16
18
  <FloatingActionButton.Icon>
17
- <Icon color="content.on.bg" name={name} />
19
+ <Icon accessibilityLabel={accessibilityLabel} color="content.on.accent" name={name} />
18
20
  </FloatingActionButton.Icon>
19
21
  </FloatingActionButton>
20
22
  )
@@ -1,13 +1,14 @@
1
1
  import {Button} from '@expo/ui/swift-ui'
2
2
  import {buttonStyle} from '@expo/ui/swift-ui/modifiers'
3
- import {useSxModifiers} from '@hzblj/kara-sx'
3
+ import {useAccessibilityModifiers, useSxModifiers} from '@hzblj/kara-sx'
4
4
  import {useMemo} from 'react'
5
5
  import {useHostGuard} from '../../host-context'
6
6
  import {Icon} from '../icon'
7
7
  import type {FabComponent, FabProps} from './fab.types'
8
8
 
9
- export const Fab: FabComponent = ({name, onPress, testID}: FabProps) => {
9
+ export const Fab: FabComponent = ({accessibilityHint, accessibilityLabel, name, onPress, testID}: FabProps) => {
10
10
  useHostGuard('Fab')
11
+ const accessibilityModifiers = useAccessibilityModifiers({accessibilityHint})
11
12
 
12
13
  const sxModifiers = useSxModifiers({
13
14
  bg: 'fill.accent.primary',
@@ -17,11 +18,14 @@ export const Fab: FabComponent = ({name, onPress, testID}: FabProps) => {
17
18
  shadow: 'btn.accent',
18
19
  w: 56,
19
20
  })
20
- const modifiers = useMemo(() => [buttonStyle('plain'), ...sxModifiers], [sxModifiers])
21
+ const modifiers = useMemo(
22
+ () => [buttonStyle('plain'), ...sxModifiers, ...accessibilityModifiers],
23
+ [accessibilityModifiers, sxModifiers]
24
+ )
21
25
 
22
26
  return (
23
27
  <Button modifiers={modifiers} onPress={onPress} testID={testID}>
24
- <Icon color="content.on.bg" name={name} />
28
+ <Icon accessibilityLabel={accessibilityLabel} color="content.on.accent" name={name} />
25
29
  </Button>
26
30
  )
27
31
  }
@@ -7,7 +7,14 @@ import type {IconName} from '../icon'
7
7
  * `Button`) on iOS. It renders inline; pinning it to a corner is up to the screen.
8
8
  */
9
9
  export type FabProps = {
10
- /** The icon, one of the app's, drawn in `content.on.bg`. */
10
+ /**
11
+ * What VoiceOver and TalkBack say for the button, since an icon has no words of its own: "Share", "Add to
12
+ * favourites". Required for that reason.
13
+ */
14
+ accessibilityLabel: string
15
+ /** What pressing it does, when the label alone does not say. See `AccessibilityProps`. */
16
+ accessibilityHint?: string
17
+ /** The icon, one of the app's, drawn in `content.on.accent`. */
11
18
  name: IconName
12
19
  /** Called when the button is pressed. */
13
20
  onPress?: () => void
@@ -31,7 +31,7 @@ export const GlassView: GlassViewComponent = ({
31
31
  cornerRadius={radii.full}
32
32
  fallbackTint={withOpacity(base, 'o95')}
33
33
  glassTint={withOpacity(base, tint ? 'o85' : 'o50')}
34
- hairlineColor={withOpacity(String(theme.color('content.on.bg')), theme.dark ? 'o20' : 'o60')}
34
+ hairlineColor={withOpacity(String(theme.color('content.on.accent')), theme.dark ? 'o20' : 'o60')}
35
35
  hairlineWidth={1}
36
36
  saturation={1.6}
37
37
  style={style}
@@ -1,6 +1,6 @@
1
1
  import {HStack as SwiftUIHStack} 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'
@@ -24,13 +24,15 @@ export const HStack: HStackComponent = ({
24
24
  onPress,
25
25
  spacing: gap,
26
26
  testID,
27
- ...sx
27
+ ...props
28
28
  }: HStackProps) => {
29
29
  useHostGuard('HStack')
30
+ const [accessibility, sx] = splitAccessibility(props)
30
31
  const sxModifiers = useSxModifiers(sx, FRAME_ALIGNMENT[align])
32
+ const accessibilityModifiers = useAccessibilityModifiers(accessibility, onPress !== undefined)
31
33
  const modifiers = useMemo(
32
- () => (onPress ? [...sxModifiers, onTapGesture(onPress)] : sxModifiers),
33
- [onPress, sxModifiers]
34
+ () => [...sxModifiers, ...(onPress ? [onTapGesture(onPress)] : []), ...accessibilityModifiers],
35
+ [accessibilityModifiers, onPress, sxModifiers]
34
36
  )
35
37
 
36
38
  return (