@hzblj/kara-components 0.16.0 → 0.18.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 (211) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/lib/module/atoms/badge/badge.android.js +5 -0
  3. package/lib/module/atoms/badge/badge.android.js.map +1 -1
  4. package/lib/module/atoms/badge/badge.ios.js +2 -0
  5. package/lib/module/atoms/badge/badge.ios.js.map +1 -1
  6. package/lib/module/atoms/button/button-tokens.js +8 -0
  7. package/lib/module/atoms/button/button-tokens.js.map +1 -1
  8. package/lib/module/atoms/button/button.android.js +43 -15
  9. package/lib/module/atoms/button/button.android.js.map +1 -1
  10. package/lib/module/atoms/button/button.ios.js +66 -10
  11. package/lib/module/atoms/button/button.ios.js.map +1 -1
  12. package/lib/module/atoms/checkbox/checkbox.android.js +5 -1
  13. package/lib/module/atoms/checkbox/checkbox.android.js.map +1 -1
  14. package/lib/module/atoms/checkbox/checkbox.ios.js +3 -1
  15. package/lib/module/atoms/checkbox/checkbox.ios.js.map +1 -1
  16. package/lib/module/atoms/color-picker/color-picker.android.js +4 -0
  17. package/lib/module/atoms/color-picker/color-picker.android.js.map +1 -1
  18. package/lib/module/atoms/color-picker/color-picker.ios.js +24 -2
  19. package/lib/module/atoms/color-picker/color-picker.ios.js.map +1 -1
  20. package/lib/module/atoms/date-picker/date-picker.android.js +85 -10
  21. package/lib/module/atoms/date-picker/date-picker.android.js.map +1 -1
  22. package/lib/module/atoms/date-picker/date-picker.ios.js +9 -7
  23. package/lib/module/atoms/date-picker/date-picker.ios.js.map +1 -1
  24. package/lib/module/atoms/divider/divider.android.js +17 -4
  25. package/lib/module/atoms/divider/divider.android.js.map +1 -1
  26. package/lib/module/atoms/divider/divider.ios.js +4 -0
  27. package/lib/module/atoms/divider/divider.ios.js.map +1 -1
  28. package/lib/module/atoms/fab/fab.android.js +64 -8
  29. package/lib/module/atoms/fab/fab.android.js.map +1 -1
  30. package/lib/module/atoms/fab/fab.ios.js +68 -12
  31. package/lib/module/atoms/fab/fab.ios.js.map +1 -1
  32. package/lib/module/atoms/icon-button/icon-button.android.js +48 -5
  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 +60 -10
  35. package/lib/module/atoms/icon-button/icon-button.ios.js.map +1 -1
  36. package/lib/module/atoms/list/list.android.js +32 -4
  37. package/lib/module/atoms/list/list.android.js.map +1 -1
  38. package/lib/module/atoms/list/list.ios.js +15 -5
  39. package/lib/module/atoms/list/list.ios.js.map +1 -1
  40. package/lib/module/atoms/picker/picker.ios.js +27 -15
  41. package/lib/module/atoms/picker/picker.ios.js.map +1 -1
  42. package/lib/module/atoms/progress/progress.android.js +21 -7
  43. package/lib/module/atoms/progress/progress.android.js.map +1 -1
  44. package/lib/module/atoms/progress/progress.ios.js +5 -1
  45. package/lib/module/atoms/progress/progress.ios.js.map +1 -1
  46. package/lib/module/atoms/slider/slider.android.js +16 -2
  47. package/lib/module/atoms/slider/slider.android.js.map +1 -1
  48. package/lib/module/atoms/slider/slider.ios.js +22 -2
  49. package/lib/module/atoms/slider/slider.ios.js.map +1 -1
  50. package/lib/module/atoms/switch/switch.android.js +5 -1
  51. package/lib/module/atoms/switch/switch.android.js.map +1 -1
  52. package/lib/module/atoms/switch/switch.ios.js +3 -1
  53. package/lib/module/atoms/switch/switch.ios.js.map +1 -1
  54. package/lib/module/atoms/text/spans.js +2 -1
  55. package/lib/module/atoms/text/spans.js.map +1 -1
  56. package/lib/module/atoms/text/text.android.js +18 -6
  57. package/lib/module/atoms/text/text.android.js.map +1 -1
  58. package/lib/module/atoms/text/text.ios.js +6 -2
  59. package/lib/module/atoms/text/text.ios.js.map +1 -1
  60. package/lib/module/atoms/text-field/text-field-input.js +14 -0
  61. package/lib/module/atoms/text-field/text-field-input.js.map +1 -0
  62. package/lib/module/atoms/text-field/text-field.android.js +113 -6
  63. package/lib/module/atoms/text-field/text-field.android.js.map +1 -1
  64. package/lib/module/atoms/text-field/text-field.ios.js +75 -3
  65. package/lib/module/atoms/text-field/text-field.ios.js.map +1 -1
  66. package/lib/module/molecules/collapsible/collapsible.android.js +4 -3
  67. package/lib/module/molecules/collapsible/collapsible.android.js.map +1 -1
  68. package/lib/module/molecules/collapsible/collapsible.ios.js +3 -1
  69. package/lib/module/molecules/collapsible/collapsible.ios.js.map +1 -1
  70. package/lib/module/molecules/radio-group/radio-group.android.js +10 -2
  71. package/lib/module/molecules/radio-group/radio-group.android.js.map +1 -1
  72. package/lib/module/molecules/radio-group/radio-group.ios.js +6 -2
  73. package/lib/module/molecules/radio-group/radio-group.ios.js.map +1 -1
  74. package/lib/module/molecules/search-bar/search-bar.js +9 -0
  75. package/lib/module/molecules/search-bar/search-bar.js.map +1 -1
  76. package/lib/module/molecules/stepper/stepper.android.js +25 -13
  77. package/lib/module/molecules/stepper/stepper.android.js.map +1 -1
  78. package/lib/module/molecules/stepper/stepper.ios.js +12 -6
  79. package/lib/module/molecules/stepper/stepper.ios.js.map +1 -1
  80. package/lib/module/organisms/action-sheet/action-sheet.android.js +16 -8
  81. package/lib/module/organisms/action-sheet/action-sheet.android.js.map +1 -1
  82. package/lib/module/organisms/action-sheet/action-sheet.ios.js +16 -3
  83. package/lib/module/organisms/action-sheet/action-sheet.ios.js.map +1 -1
  84. package/lib/module/organisms/alert/alert.android.js +4 -3
  85. package/lib/module/organisms/alert/alert.android.js.map +1 -1
  86. package/lib/module/organisms/alert/alert.ios.js +3 -2
  87. package/lib/module/organisms/alert/alert.ios.js.map +1 -1
  88. package/lib/module/organisms/context-menu/context-menu.android.js +3 -1
  89. package/lib/module/organisms/context-menu/context-menu.android.js.map +1 -1
  90. package/lib/module/organisms/context-menu/context-menu.ios.js +1 -0
  91. package/lib/module/organisms/context-menu/context-menu.ios.js.map +1 -1
  92. package/lib/module/organisms/fab-menu/fab-menu.android.js +3 -0
  93. package/lib/module/organisms/fab-menu/fab-menu.android.js.map +1 -1
  94. package/lib/module/organisms/fab-menu/fab-menu.ios.js +3 -0
  95. package/lib/module/organisms/fab-menu/fab-menu.ios.js.map +1 -1
  96. package/lib/module/organisms/menu/menu-parts.js +20 -0
  97. package/lib/module/organisms/menu/menu-parts.js.map +1 -0
  98. package/lib/module/organisms/menu/menu.android.js +51 -19
  99. package/lib/module/organisms/menu/menu.android.js.map +1 -1
  100. package/lib/module/organisms/menu/menu.ios.js +71 -8
  101. package/lib/module/organisms/menu/menu.ios.js.map +1 -1
  102. package/lib/module/organisms/sheet/sheet.js +2 -1
  103. package/lib/module/organisms/sheet/sheet.js.map +1 -1
  104. package/lib/typescript/atoms/badge/badge.types.d.ts +2 -0
  105. package/lib/typescript/atoms/button/button-tokens.d.ts +6 -2
  106. package/lib/typescript/atoms/button/button.types.d.ts +19 -5
  107. package/lib/typescript/atoms/checkbox/checkbox.types.d.ts +2 -0
  108. package/lib/typescript/atoms/color-picker/color-picker.types.d.ts +2 -0
  109. package/lib/typescript/atoms/date-picker/date-picker.ios.d.ts +5 -5
  110. package/lib/typescript/atoms/date-picker/date-picker.types.d.ts +30 -7
  111. package/lib/typescript/atoms/divider/divider.ios.d.ts +4 -0
  112. package/lib/typescript/atoms/divider/divider.types.d.ts +13 -2
  113. package/lib/typescript/atoms/fab/fab.types.d.ts +21 -4
  114. package/lib/typescript/atoms/icon-button/icon-button.types.d.ts +23 -4
  115. package/lib/typescript/atoms/list/list.types.d.ts +15 -1
  116. package/lib/typescript/atoms/picker/picker.ios.d.ts +5 -3
  117. package/lib/typescript/atoms/picker/picker.types.d.ts +11 -3
  118. package/lib/typescript/atoms/progress/progress.types.d.ts +13 -2
  119. package/lib/typescript/atoms/slider/slider.ios.d.ts +4 -0
  120. package/lib/typescript/atoms/slider/slider.types.d.ts +7 -0
  121. package/lib/typescript/atoms/switch/switch.types.d.ts +2 -0
  122. package/lib/typescript/atoms/text/spans.d.ts +2 -0
  123. package/lib/typescript/atoms/text/text.types.d.ts +9 -1
  124. package/lib/typescript/atoms/text-field/text-field-input.d.ts +7 -0
  125. package/lib/typescript/atoms/text-field/text-field.types.d.ts +77 -4
  126. package/lib/typescript/molecules/collapsible/collapsible.types.d.ts +2 -0
  127. package/lib/typescript/molecules/radio-group/radio-group.types.d.ts +4 -0
  128. package/lib/typescript/molecules/search-bar/search-bar.types.d.ts +11 -1
  129. package/lib/typescript/molecules/stepper/stepper.ios.d.ts +2 -1
  130. package/lib/typescript/molecules/stepper/stepper.types.d.ts +17 -0
  131. package/lib/typescript/organisms/action-sheet/action-sheet.ios.d.ts +4 -0
  132. package/lib/typescript/organisms/action-sheet/action-sheet.types.d.ts +8 -3
  133. package/lib/typescript/organisms/alert/alert.types.d.ts +8 -3
  134. package/lib/typescript/organisms/menu/menu-parts.d.ts +14 -0
  135. package/lib/typescript/organisms/menu/menu.types.d.ts +35 -7
  136. package/lib/typescript/organisms/sheet/sheet.types.d.ts +7 -1
  137. package/package.json +4 -4
  138. package/src/atoms/badge/badge.android.tsx +9 -2
  139. package/src/atoms/badge/badge.ios.tsx +2 -2
  140. package/src/atoms/badge/badge.types.ts +2 -0
  141. package/src/atoms/button/button-tokens.ts +6 -3
  142. package/src/atoms/button/button.android.tsx +35 -11
  143. package/src/atoms/button/button.ios.tsx +72 -8
  144. package/src/atoms/button/button.types.ts +20 -5
  145. package/src/atoms/checkbox/checkbox.android.tsx +5 -2
  146. package/src/atoms/checkbox/checkbox.ios.tsx +2 -2
  147. package/src/atoms/checkbox/checkbox.types.ts +2 -0
  148. package/src/atoms/color-picker/color-picker.android.tsx +10 -2
  149. package/src/atoms/color-picker/color-picker.ios.tsx +24 -1
  150. package/src/atoms/color-picker/color-picker.types.ts +2 -0
  151. package/src/atoms/date-picker/date-picker.android.tsx +86 -16
  152. package/src/atoms/date-picker/date-picker.ios.tsx +20 -8
  153. package/src/atoms/date-picker/date-picker.types.ts +31 -7
  154. package/src/atoms/divider/divider.android.tsx +14 -3
  155. package/src/atoms/divider/divider.ios.tsx +4 -0
  156. package/src/atoms/divider/divider.types.ts +14 -2
  157. package/src/atoms/fab/fab.android.tsx +61 -11
  158. package/src/atoms/fab/fab.ios.tsx +48 -13
  159. package/src/atoms/fab/fab.types.ts +22 -4
  160. package/src/atoms/icon-button/icon-button.android.tsx +60 -10
  161. package/src/atoms/icon-button/icon-button.ios.tsx +77 -8
  162. package/src/atoms/icon-button/icon-button.types.ts +24 -4
  163. package/src/atoms/list/list.android.tsx +26 -4
  164. package/src/atoms/list/list.ios.tsx +20 -5
  165. package/src/atoms/list/list.types.ts +16 -1
  166. package/src/atoms/picker/picker.ios.tsx +46 -16
  167. package/src/atoms/picker/picker.types.ts +12 -3
  168. package/src/atoms/progress/progress.android.tsx +32 -6
  169. package/src/atoms/progress/progress.ios.tsx +11 -5
  170. package/src/atoms/progress/progress.types.ts +13 -2
  171. package/src/atoms/slider/slider.android.tsx +18 -2
  172. package/src/atoms/slider/slider.ios.tsx +38 -2
  173. package/src/atoms/slider/slider.types.ts +7 -0
  174. package/src/atoms/switch/switch.android.tsx +5 -2
  175. package/src/atoms/switch/switch.ios.tsx +2 -2
  176. package/src/atoms/switch/switch.types.ts +2 -0
  177. package/src/atoms/text/spans.ts +2 -1
  178. package/src/atoms/text/text.android.tsx +20 -7
  179. package/src/atoms/text/text.ios.tsx +7 -0
  180. package/src/atoms/text/text.types.ts +10 -1
  181. package/src/atoms/text-field/text-field-input.ts +17 -0
  182. package/src/atoms/text-field/text-field.android.tsx +132 -9
  183. package/src/atoms/text-field/text-field.ios.tsx +110 -6
  184. package/src/atoms/text-field/text-field.types.ts +98 -4
  185. package/src/molecules/collapsible/collapsible.android.tsx +10 -2
  186. package/src/molecules/collapsible/collapsible.ios.tsx +8 -2
  187. package/src/molecules/collapsible/collapsible.types.ts +2 -0
  188. package/src/molecules/radio-group/radio-group.android.tsx +18 -4
  189. package/src/molecules/radio-group/radio-group.ios.tsx +19 -5
  190. package/src/molecules/radio-group/radio-group.types.ts +4 -0
  191. package/src/molecules/search-bar/search-bar.tsx +9 -0
  192. package/src/molecules/search-bar/search-bar.types.ts +12 -1
  193. package/src/molecules/stepper/stepper.android.tsx +24 -4
  194. package/src/molecules/stepper/stepper.ios.tsx +23 -8
  195. package/src/molecules/stepper/stepper.types.ts +17 -0
  196. package/src/organisms/action-sheet/action-sheet.android.tsx +23 -11
  197. package/src/organisms/action-sheet/action-sheet.ios.tsx +20 -3
  198. package/src/organisms/action-sheet/action-sheet.types.ts +8 -3
  199. package/src/organisms/alert/alert.android.tsx +11 -8
  200. package/src/organisms/alert/alert.ios.tsx +2 -1
  201. package/src/organisms/alert/alert.types.ts +8 -3
  202. package/src/organisms/context-menu/context-menu.android.tsx +3 -2
  203. package/src/organisms/context-menu/context-menu.ios.tsx +1 -0
  204. package/src/organisms/fab-menu/fab-menu.android.tsx +3 -0
  205. package/src/organisms/fab-menu/fab-menu.ios.tsx +9 -1
  206. package/src/organisms/menu/menu-parts.ts +21 -0
  207. package/src/organisms/menu/menu.android.tsx +39 -10
  208. package/src/organisms/menu/menu.ios.tsx +55 -5
  209. package/src/organisms/menu/menu.types.ts +30 -7
  210. package/src/organisms/sheet/sheet.tsx +2 -1
  211. package/src/organisms/sheet/sheet.types.ts +7 -1
@@ -1,11 +1,21 @@
1
1
  import type { ReactNode } from 'react';
2
+ import type { TextFieldAutoCapitalize } from '../../atoms/text-field';
2
3
  /**
3
4
  * A search field drawn the same on both platforms: a rounded box with a magnifier, a `TextInput` and a clear button
4
- * while there is text. The React Native tier: it needs no host.
5
+ * while there is text. The keyboard's return key reads Search, and a query is taken as typed: no capitals and no
6
+ * corrections unless asked for. The React Native tier: it needs no host.
5
7
  */
6
8
  export type SearchBarProps = {
9
+ /** When the keyboard turns Shift on by itself. `none` by default: a query is not a sentence. */
10
+ autoCapitalize?: TextFieldAutoCapitalize;
11
+ /** Lets the keyboard correct words as they are typed. Off by default, so a query is searched as typed. */
12
+ autoCorrect?: boolean;
13
+ /** Takes focus, and brings up the keyboard, as soon as the field appears. Off by default. */
14
+ autoFocus?: boolean;
7
15
  /** Every edit, and `''` when the clear button is pressed. */
8
16
  onChangeText?: (text: string) => void;
17
+ /** Called with the query when the keyboard's Search key is pressed. The keyboard goes down after it. */
18
+ onSubmit?: (text: string) => void;
9
19
  /** Shown while the field is empty. `Search` by default. */
10
20
  placeholder?: string;
11
21
  /** Test identifier, put on the field's container view rather than on the `TextInput`. */
@@ -2,6 +2,7 @@ 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. With motion reduced it cross-fades instead.
5
+ * monospaced digits so the row does not jump. With motion reduced it cross-fades instead. Its buttons are labelled by
6
+ * the system, so `decrementLabel` and `incrementLabel` are Android's alone.
6
7
  */
7
8
  export declare const Stepper: StepperComponent;
@@ -4,6 +4,18 @@ import type { ReactNode } from 'react';
4
4
  * two icon buttons in a row. A native atom: it renders inside `<Screen>` or `<Island>`.
5
5
  */
6
6
  export type StepperProps = {
7
+ /**
8
+ * What TalkBack says for the minus button. `Decrement` by default: pass the app's own words, in its language.
9
+ * Android only. iOS ignores it: its native stepper labels its buttons itself, in the device's language.
10
+ */
11
+ decrementLabel?: string;
12
+ /** Stops the user changing the value, and dims the row. */
13
+ disabled?: boolean;
14
+ /**
15
+ * What TalkBack says for the plus button. `Increment` by default: pass the app's own words, in its language. Android
16
+ * only. iOS ignores it: its native stepper labels its buttons itself, in the device's language.
17
+ */
18
+ incrementLabel?: string;
7
19
  /** The leading text. The value is shown too: before the control on iOS, between the buttons on Android. */
8
20
  label: string;
9
21
  /** The highest value. When unset, Android has no upper limit and iOS stops at `100`. */
@@ -14,6 +26,11 @@ export type StepperProps = {
14
26
  onValueChange: (value: number) => void;
15
27
  /** How much one press adds or takes away. `1` by default. */
16
28
  step?: number;
29
+ /**
30
+ * Test identifier, on the control rather than the row: SwiftUI's `Stepper` on iOS, the minus and plus buttons with
31
+ * the value between them on Android. Reach a button by what screen readers say for it.
32
+ */
33
+ testID?: string;
17
34
  /** The current value. Update it from `onValueChange`. iOS's native stepper counts in whole numbers. */
18
35
  value: number;
19
36
  };
@@ -1,2 +1,6 @@
1
1
  import type { ActionSheetComponent } from './action-sheet.types';
2
+ /**
3
+ * `titleVisibility="visible"`: left to `automatic`, SwiftUI drops the title of a dialog presented as an action sheet,
4
+ * and the actions lose what they are about.
5
+ */
2
6
  export declare const ActionSheet: ActionSheetComponent;
@@ -2,14 +2,19 @@ import type { ReactNode } from 'react';
2
2
  import type { MenuAction } from '../menu';
3
3
  /**
4
4
  * A list of actions to pick one from: SwiftUI's confirmation dialog on iOS, a `Sheet` of Material rows (icon and label)
5
- * under the title on Android. A native atom: it renders inside `<Screen>` or `<Island>`.
5
+ * under the title and message on Android. A native atom: it renders inside `<Screen>` or `<Island>`.
6
6
  */
7
7
  export type ActionSheetProps = {
8
- /** The choices, top to bottom. Picking one closes the sheet. */
8
+ /** The choices, top to bottom. Picking one closes the sheet. A `disabled` one stays in the list, greyed. */
9
9
  actions: MenuAction[];
10
+ /**
11
+ * A line under the title saying what the choice is about: "The photo will be removed from all your devices." Unset,
12
+ * the title stands alone.
13
+ */
14
+ message?: string;
10
15
  /** The sheet closed (after a pick too), so turn `visible` off. */
11
16
  onDismiss: () => void;
12
- /** The heading above the actions. Android always shows it; on iOS the system decides, and may hide it. */
17
+ /** The heading above the actions, shown on both platforms. */
13
18
  title: string;
14
19
  /** Presents the sheet while true. */
15
20
  visible: boolean;
@@ -1,9 +1,14 @@
1
1
  import type { ReactNode } from 'react';
2
2
  /**
3
- * A modal alert with a confirm button and Cancel: SwiftUI's alert on iOS, Material's `AlertDialog` on Android. A native
4
- * atom: it renders inside `<Screen>` or `<Island>`.
3
+ * A modal alert with a confirm button and a cancel button (or the confirm button alone): SwiftUI's alert on iOS,
4
+ * Material's `AlertDialog` on Android. A native atom: it renders inside `<Screen>` or `<Island>`.
5
5
  */
6
6
  export type AlertProps = {
7
+ /**
8
+ * The cancel button's label, in the app's language: "Zrušit". `Cancel` by default. `null` leaves the button out, for
9
+ * an alert that only informs and is closed by its confirm button alone ("OK").
10
+ */
11
+ cancelLabel?: string | null;
7
12
  /** The confirm button's label. `OK` by default. */
8
13
  confirmLabel?: string;
9
14
  /** Marks the confirm button destructive: the system's destructive role on iOS, the destructive colour on Android. */
@@ -12,7 +17,7 @@ export type AlertProps = {
12
17
  message?: string;
13
18
  /** The confirm button was pressed. `onDismiss` follows, as it does for every way the alert closes. */
14
19
  onConfirm?: () => void;
15
- /** The alert closed (either button, or on Android a tap outside or back), so turn `visible` off. */
20
+ /** The alert closed (any of its buttons, or on Android a tap outside or back), so turn `visible` off. */
16
21
  onDismiss: () => void;
17
22
  /** The alert's heading. */
18
23
  title: string;
@@ -0,0 +1,14 @@
1
+ import type { ColorToken } from '@hzblj/kara-theme';
2
+ import type { IconName } from '../../atoms/icon';
3
+ import type { MenuAction, MenuProps } from './menu.types';
4
+ /**
5
+ * An action's colour in a row kara draws itself (the Android menus and sheets): greyed when disabled, as `IconButton`
6
+ * greys, red when destructive. A Compose `DropdownMenuItem` cannot grey its own text here, since the text sets its
7
+ * colour explicitly.
8
+ */
9
+ export declare const menuActionColor: ({ destructive, disabled }: MenuAction) => ColorToken;
10
+ /** A trigger with no text: the props' union guarantees its icon and the words a screen reader says for it. */
11
+ export declare const isIconOnly: (props: MenuProps) => props is MenuProps & {
12
+ accessibilityLabel: string;
13
+ icon: IconName;
14
+ };
@@ -8,8 +8,13 @@ export type MenuAction = {
8
8
  */
9
9
  destructive?: boolean;
10
10
  /**
11
- * One of the app's icons, before the label. `ContextMenu` and `FabMenu` show it on both platforms, `Menu` and
12
- * `ActionSheet` on Android only.
11
+ * Greys the action out and stops it responding, for one that does not apply right now: "Paste" with nothing copied.
12
+ * Off by default. `FabMenu` ignores it on Android, where Material's FAB menu items have no disabled state.
13
+ */
14
+ disabled?: boolean;
15
+ /**
16
+ * One of the app's icons, before the label. `Menu`, `ContextMenu` and `FabMenu` show it on both platforms,
17
+ * `ActionSheet` on Android only: an iOS confirmation dialog draws text alone.
13
18
  */
14
19
  icon?: IconName;
15
20
  /** The action's text. Rows are keyed by it, so keep labels unique within one list. */
@@ -17,14 +22,37 @@ export type MenuAction = {
17
22
  /** Called when the action is chosen; the menu closes by itself. */
18
23
  onPress: () => void;
19
24
  };
25
+ /** What the trigger needs: its text, or an icon and the words a screen reader says for it. */
26
+ type MenuTrigger = {
27
+ label: string;
28
+ } | {
29
+ accessibilityLabel: string;
30
+ icon: IconName;
31
+ };
20
32
  /**
21
- * A text button that opens a list of actions: a SwiftUI `Menu` on iOS, a Material dropdown anchored to it on Android.
22
- * A native atom: it renders inside `<Screen>` or `<Island>`.
33
+ * A button that opens a list of actions: a SwiftUI `Menu` on iOS, a Material dropdown anchored to it on Android. The
34
+ * trigger is text, an icon and text, or an icon alone. A native atom: it renders inside `<Screen>` or `<Island>`.
23
35
  */
24
- export type MenuProps = {
36
+ export type MenuProps = MenuTrigger & {
37
+ /**
38
+ * What VoiceOver and TalkBack say for the trigger: "More options". Required with an `icon` and no `label`, since an
39
+ * icon has no words of its own. With a label, it replaces the label for screen readers.
40
+ */
41
+ accessibilityLabel?: string;
25
42
  /** The menu's actions, top to bottom. */
26
43
  actions: MenuAction[];
27
- /** The trigger's text. Android draws it in the accent colour, with a chevron. */
28
- label: string;
44
+ /**
45
+ * One of the app's icons on the trigger. Before the `label`, in the same colour, when there is one; alone, the
46
+ * trigger is drawn as an `IconButton` (`content.primary`, in a 44pt target on iOS and 48dp on Android) and needs an
47
+ * `accessibilityLabel`.
48
+ */
49
+ icon?: IconName;
50
+ /**
51
+ * The trigger's text. Android draws it in the accent colour, with a chevron. Required unless there is an `icon`.
52
+ */
53
+ label?: string;
54
+ /** Test identifier, forwarded to the trigger. Reach an action by its label once the menu is open. */
55
+ testID?: string;
29
56
  };
30
57
  export type MenuComponent = (props: MenuProps) => ReactNode;
58
+ export {};
@@ -46,7 +46,7 @@ export type SheetRef = {
46
46
  * one; native atoms in its content go in an `Island`.
47
47
  */
48
48
  export type SheetProps = {
49
- /** The sheet's colour. `surface.primary` by default. */
49
+ /** The sheet's colour. `surface.primary` by default; ignored on iOS with `systemBackground`. */
50
50
  bg?: ColorToken;
51
51
  /** The content. A `<Sheet.Header>` among the direct children becomes the native header instead. */
52
52
  children: ReactNode;
@@ -75,6 +75,12 @@ export type SheetProps = {
75
75
  * and a header scrolls with the iOS 26 edge effect. Off by default; set it with a scrolling body.
76
76
  */
77
77
  scrollable?: boolean;
78
+ /**
79
+ * iOS: the system's own sheet background instead of `bg` — untinted Liquid Glass on iOS 26, a material blur before
80
+ * it — for a sheet that floats over content it should let through, like a map. Keep the content free of fills. Android
81
+ * keeps `bg`. Off by default.
82
+ */
83
+ systemBackground?: boolean;
78
84
  /** Test identifier, forwarded to the native sheet. */
79
85
  testID?: string;
80
86
  /** Presents the sheet while true; going false dismisses it. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hzblj/kara-components",
3
- "version": "0.16.0",
3
+ "version": "0.18.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",
@@ -61,9 +61,9 @@
61
61
  "dependencies": {
62
62
  "@hzblj/expo-native-icons": "^0.1.0",
63
63
  "@hzblj/expo-rich-haptics": "^0.1.0",
64
- "@hzblj/kara-native": "^0.16.0",
65
- "@hzblj/kara-sx": "^0.16.0",
66
- "@hzblj/kara-theme": "^0.16.0",
64
+ "@hzblj/kara-native": "^0.18.0",
65
+ "@hzblj/kara-sx": "^0.18.0",
66
+ "@hzblj/kara-theme": "^0.18.0",
67
67
  "sf-symbols-typescript": "^2.2.0"
68
68
  },
69
69
  "peerDependencies": {
@@ -1,17 +1,24 @@
1
1
  import {Badge as ComposeBadge, Text as ComposeText} from '@expo/ui/jetpack-compose'
2
+ import {testID as testIdModifier} from '@expo/ui/jetpack-compose/modifiers'
2
3
  import {useAppTheme} from '@hzblj/kara-theme'
4
+ import {useMemo} from 'react'
3
5
  import {useHostGuard} from '../../host-context'
4
6
  import type {BadgeComponent, BadgeProps} from './badge.types'
5
7
  import {BADGE_TOKENS} from './badge-tokens'
6
8
 
7
9
  /** Material's badge. Its content is a Compose `Text` — `@expo/ui`'s badge takes composable content, not a string. */
8
- export const Badge: BadgeComponent = ({label, tone = 'neutral'}: BadgeProps) => {
10
+ export const Badge: BadgeComponent = ({label, testID, tone = 'neutral'}: BadgeProps) => {
9
11
  useHostGuard('Badge')
10
12
  const theme = useAppTheme()
11
13
  const tokens = BADGE_TOKENS[tone]
14
+ const modifiers = useMemo(() => (testID ? [testIdModifier(testID)] : undefined), [testID])
12
15
 
13
16
  return (
14
- <ComposeBadge containerColor={theme.color(tokens.fill)} contentColor={theme.color(tokens.content)}>
17
+ <ComposeBadge
18
+ containerColor={theme.color(tokens.fill)}
19
+ contentColor={theme.color(tokens.content)}
20
+ modifiers={modifiers}
21
+ >
15
22
  <ComposeText color={theme.colorString(tokens.content)} style={theme.typography.captionMedium}>
16
23
  {label}
17
24
  </ComposeText>
@@ -5,13 +5,13 @@ import {Text} from '../text'
5
5
  import type {BadgeComponent, BadgeProps} from './badge.types'
6
6
  import {BADGE_TOKENS} from './badge-tokens'
7
7
 
8
- export const Badge: BadgeComponent = ({label, tone = 'neutral'}: BadgeProps) => {
8
+ export const Badge: BadgeComponent = ({label, testID, tone = 'neutral'}: BadgeProps) => {
9
9
  useHostGuard('Badge')
10
10
  const tokens = BADGE_TOKENS[tone]
11
11
  const modifiers = useSxModifiers({bg: tokens.fill, borderRadius: 'full', px: 's8', py: 's2'})
12
12
 
13
13
  return (
14
- <HStack modifiers={modifiers}>
14
+ <HStack modifiers={modifiers} testID={testID}>
15
15
  <Text color={tokens.content} variant="labelMedium">
16
16
  {label}
17
17
  </Text>
@@ -6,6 +6,8 @@ export type BadgeTone = 'accent' | 'destructive' | 'neutral' | 'success'
6
6
  export type BadgeProps = {
7
7
  /** The text inside. */
8
8
  label: string
9
+ /** Test identifier, forwarded to the native view. */
10
+ testID?: string
9
11
  /** The fill and text colours. `neutral` by default. */
10
12
  tone?: BadgeTone
11
13
  }
@@ -2,16 +2,18 @@ import type {ColorToken, ShadowName, Theme} from '@hzblj/kara-theme'
2
2
 
3
3
  import type {ButtonVariant} from './button.types'
4
4
 
5
+ /** `fill` and `shadow` are unset for `plain`: a text button has no container to fill or lift. */
5
6
  type ButtonTokens = {
6
7
  content: ColorToken
7
- fill: ColorToken
8
- shadow: ShadowName
8
+ fill: ColorToken | undefined
9
+ shadow: ShadowName | undefined
9
10
  }
10
11
 
11
12
  const TOKENS = {
12
13
  accent: {content: 'content.on.accent', fill: 'fill.accent.primary', shadow: 'btn.accent'},
13
14
  destructive: {content: 'content.on.destructive', fill: 'fill.destructive.primary', shadow: 'btn.destructive'},
14
15
  glass: {content: 'content.primary', fill: 'btn.bg.glass.base', shadow: 'btn.glass'},
16
+ plain: {content: 'content.accent', fill: undefined, shadow: undefined},
15
17
  secondary: {content: 'content.primary', fill: 'btn.bg.secondary.base', shadow: 'btn.secondary'},
16
18
  } as const satisfies Record<ButtonVariant, ButtonTokens>
17
19
 
@@ -26,8 +28,9 @@ export const buttonTokens = (variant: ButtonVariant, disabled: boolean, theme: T
26
28
  accent: 'fill.accent.disabled',
27
29
  destructive: 'fill.destructive.disabled',
28
30
  glass: 'btn.bg.glass.disabled',
31
+ plain: undefined,
29
32
  secondary: 'btn.bg.secondary.disabled',
30
- } as const satisfies Record<ButtonVariant, ColorToken>
33
+ } as const satisfies Record<ButtonVariant, ColorToken | undefined>
31
34
 
32
35
  void theme
33
36
 
@@ -1,23 +1,43 @@
1
- import {Button as ComposeButton, Text as ComposeText, OutlinedButton} from '@expo/ui/jetpack-compose'
2
- import {testID as testIdModifier} from '@expo/ui/jetpack-compose/modifiers'
1
+ import {
2
+ Button as ComposeButton,
3
+ Spacer as ComposeSpacer,
4
+ Text as ComposeText,
5
+ OutlinedButton,
6
+ TextButton,
7
+ } from '@expo/ui/jetpack-compose'
8
+ import {testID as testIdModifier, width as widthModifier} from '@expo/ui/jetpack-compose/modifiers'
3
9
  import {useAccessibilityModifiers, useSxModifiers} from '@hzblj/kara-sx'
4
- import {useAppTheme} from '@hzblj/kara-theme'
10
+ import {spacing, useAppTheme} from '@hzblj/kara-theme'
5
11
  import {useMemo} from 'react'
6
12
  import {useHostGuard} from '../../host-context'
13
+ import {Icon} from '../icon'
7
14
  import type {ButtonComponent, ButtonProps} from './button.types'
8
15
  import {buttonTokens, contentOnTint} from './button-tokens'
9
16
 
17
+ /** Material's `ButtonDefaults.IconSpacing`: the gap between a button's icon and its label. */
18
+ const ICON_GAP = [widthModifier(spacing.s8)]
19
+
20
+ /**
21
+ * Material's padding for a button with a leading icon (`ButtonWithIconContentPadding`,
22
+ * `TextButtonWithIconContentPadding`): the icon side a step tighter than the label's. Top and bottom keep the defaults.
23
+ */
24
+ const WITH_ICON_PADDING = {end: spacing.s24, start: spacing.s16}
25
+ const TEXT_WITH_ICON_PADDING = {end: spacing.s16, start: spacing.s12}
26
+
10
27
  /**
11
28
  * Material's filled button, in its own stadium shape. No `borderRadius` or `shadow` from the tokens: those shape the
12
29
  * SwiftUI button's glossy ring, and laid over a Compose button they clip and outline a rounded rectangle around the
13
- * pill Material draws. The label is a Compose `Text` — `@expo/ui`'s button takes composable content, not a string.
30
+ * pill Material draws. The label is a Compose `Text` — `@expo/ui`'s button takes composable content, not a string —
31
+ * after the `icon`, when there is one, in the same row.
14
32
  *
15
- * Material has no glass, so `glass` is its outlined button — and a `tint` makes any variant a filled button in that
16
- * colour, as a prominent style does on iOS. `buttonStyle` and `controlSize` are SwiftUI's alone.
33
+ * Material has no glass, so `glass` is its outlined button — and a `tint` makes any variant but `plain` a filled
34
+ * button in that colour, as a prominent style does on iOS. `plain` is Material's `TextButton`, and a `tint` colours its
35
+ * label. `buttonStyle` and `controlSize` are SwiftUI's alone.
17
36
  */
18
37
  export const Button: ButtonComponent = ({
19
38
  accessibilityHint,
20
39
  disabled = false,
40
+ icon,
21
41
  label,
22
42
  onPress,
23
43
  testID,
@@ -35,27 +55,31 @@ export const Button: ButtonComponent = ({
35
55
  [accessibilityModifiers, sxModifiers, testID]
36
56
  )
37
57
  const style = theme.typography.body
58
+ const text = variant === 'plain'
38
59
  const outlined = variant === 'glass' && !tint
39
60
  const fill = tint && !disabled ? tint : tokens.fill
40
- const content = tint && !disabled ? contentOnTint(tint, theme) : tokens.content
41
- const MaterialButton = outlined ? OutlinedButton : ComposeButton
61
+ const content = !tint || disabled ? tokens.content : text ? tint : contentOnTint(tint, theme)
62
+ const MaterialButton = text ? TextButton : outlined ? OutlinedButton : ComposeButton
42
63
 
43
64
  return (
44
65
  <MaterialButton
45
66
  colors={
46
- outlined
47
- ? {contentColor: theme.color(content), disabledContentColor: theme.color(content)}
48
- : {
67
+ fill && !text && !outlined
68
+ ? {
49
69
  containerColor: theme.color(fill),
50
70
  contentColor: theme.color(content),
51
71
  disabledContainerColor: theme.color(fill),
52
72
  disabledContentColor: theme.color(content),
53
73
  }
74
+ : {contentColor: theme.color(content), disabledContentColor: theme.color(content)}
54
75
  }
76
+ contentPadding={icon ? (text ? TEXT_WITH_ICON_PADDING : WITH_ICON_PADDING) : undefined}
55
77
  enabled={!disabled}
56
78
  modifiers={modifiers}
57
79
  onClick={onPress}
58
80
  >
81
+ {icon ? <Icon color={content} name={icon} size="s20" /> : null}
82
+ {icon ? <ComposeSpacer modifiers={ICON_GAP} /> : null}
59
83
  <ComposeText
60
84
  color={theme.colorString(content)}
61
85
  style={{
@@ -1,15 +1,19 @@
1
- import {Button as SwiftUIButton} from '@expo/ui/swift-ui'
1
+ import {Button as SwiftUIButton, Label as SwiftUILabel} from '@expo/ui/swift-ui'
2
2
  import {
3
3
  buttonStyle as buttonStyleModifier,
4
+ contentShape,
4
5
  controlSize as controlSizeModifier,
5
6
  disabled as disabledModifier,
6
7
  glassEffect as glassEffectModifier,
8
+ labelStyle,
9
+ shapes,
7
10
  tint as tintModifier,
8
11
  } from '@expo/ui/swift-ui/modifiers'
9
12
  import {useAccessibilityModifiers} from '@hzblj/kara-sx'
10
- import {type ColorToken, useAppTheme} from '@hzblj/kara-theme'
13
+ import {type ColorToken, type Theme, useAppTheme} from '@hzblj/kara-theme'
11
14
  import {useMemo} from 'react'
12
15
  import {useHostGuard} from '../../host-context'
16
+ import {Icon} from '../icon'
13
17
  import {Text} from '../text'
14
18
  import {VStack} from '../v-stack'
15
19
  import type {ButtonComponent, ButtonProps, ButtonStyleName, ButtonVariant} from './button.types'
@@ -19,6 +23,7 @@ const STYLE_BY_VARIANT = {
19
23
  accent: 'borderedProminent',
20
24
  destructive: 'borderedProminent',
21
25
  glass: 'glass',
26
+ plain: 'borderless',
22
27
  secondary: 'bordered',
23
28
  } as const satisfies Record<ButtonVariant, ButtonStyleName>
24
29
 
@@ -26,14 +31,56 @@ const TINT_BY_VARIANT = {
26
31
  accent: 'fill.accent.primary',
27
32
  destructive: 'fill.destructive.primary',
28
33
  glass: undefined,
34
+ plain: undefined,
29
35
  secondary: undefined,
30
36
  } as const satisfies Record<ButtonVariant, ColorToken | undefined>
31
37
 
38
+ /** Both, wherever the button sits: a toolbar or a menu would otherwise show the icon alone. */
39
+ const TITLE_AND_ICON = [labelStyle('titleAndIcon')]
40
+
41
+ /**
42
+ * `borderless` draws nothing around the label, so the label is the button: padding takes it to a 44pt tap target,
43
+ * and a rectangle content shape makes the whole of it (the padding, and the width `wide` gives it) take the tap,
44
+ * not only the glyphs.
45
+ */
46
+ const TEXT_BUTTON_TARGET = {modifiers: [contentShape(shapes.rectangle())]}
47
+
48
+ /**
49
+ * A prominent style (and tinted glass) is coloured by the tint, so a caller's own tint picks the label on it. A text
50
+ * style has nothing to fill, so there the tint is the label's own colour.
51
+ */
52
+ const labelColor = ({
53
+ disabled,
54
+ filled,
55
+ textOnly,
56
+ theme,
57
+ tint,
58
+ tokens,
59
+ }: {
60
+ disabled: boolean
61
+ filled: boolean
62
+ textOnly: boolean
63
+ theme: Theme
64
+ tint: ColorToken | undefined
65
+ tokens: {content: ColorToken}
66
+ }): ColorToken => {
67
+ if (!tint || disabled) {
68
+ return tokens.content
69
+ }
70
+
71
+ if (filled) {
72
+ return contentOnTint(tint, theme)
73
+ }
74
+
75
+ return textOnly ? tint : tokens.content
76
+ }
77
+
32
78
  export const Button: ButtonComponent = ({
33
79
  accessibilityHint,
34
80
  buttonStyle,
35
81
  controlSize = 'large',
36
82
  disabled = false,
83
+ icon,
37
84
  label,
38
85
  onPress,
39
86
  testID,
@@ -58,8 +105,10 @@ export const Button: ButtonComponent = ({
58
105
  */
59
106
  const glassTint = style === 'glass' && resolvedTint ? theme.color(resolvedTint) : undefined
60
107
  const filled = Boolean(glassTint) || style === 'borderedProminent' || style === 'glassProminent'
61
- // A prominent style — and tinted glass — is coloured by the tint, so a caller's own tint picks the label on it.
62
- const content = tint && filled && !disabled ? contentOnTint(tint, theme) : tokens.content
108
+ const textOnly = style === 'borderless' || style === 'plain'
109
+ // Only the `plain` variant's own text button is padded out; another variant restyled `plain` keeps its layout.
110
+ const textButton = variant === 'plain' && textOnly
111
+ const content = labelColor({disabled, filled, textOnly, theme, tint, tokens})
63
112
 
64
113
  const accessibilityModifiers = useAccessibilityModifiers({accessibilityHint})
65
114
 
@@ -85,12 +134,27 @@ export const Button: ButtonComponent = ({
85
134
  return list
86
135
  }, [accessibilityModifiers, controlSize, disabled, glassTint, resolvedTint, style, theme])
87
136
 
137
+ const text = (
138
+ <Text color={content} variant="body">
139
+ {label}
140
+ </Text>
141
+ )
142
+
88
143
  return (
89
144
  <SwiftUIButton modifiers={modifiers} onPress={onPress} testID={testID}>
90
- <VStack align="center" py={glassTint ? 's16' : undefined} wide={wide}>
91
- <Text color={content} variant="body">
92
- {label}
93
- </Text>
145
+ <VStack
146
+ align="center"
147
+ ios={textButton ? TEXT_BUTTON_TARGET : undefined}
148
+ py={glassTint ? 's16' : textButton ? 's12' : undefined}
149
+ wide={wide}
150
+ >
151
+ {icon ? (
152
+ <SwiftUILabel icon={<Icon color={content} name={icon} size="s20" />} modifiers={TITLE_AND_ICON}>
153
+ {text}
154
+ </SwiftUILabel>
155
+ ) : (
156
+ text
157
+ )}
94
158
  </VStack>
95
159
  </SwiftUIButton>
96
160
  )
@@ -1,7 +1,9 @@
1
1
  import type {ColorToken} from '@hzblj/kara-theme'
2
2
  import type {ReactNode} from 'react'
3
3
 
4
- export type ButtonVariant = 'accent' | 'destructive' | 'glass' | 'secondary'
4
+ import type {IconName} from '../icon'
5
+
6
+ export type ButtonVariant = 'accent' | 'destructive' | 'glass' | 'plain' | 'secondary'
5
7
 
6
8
  /**
7
9
  * SwiftUI `ButtonStyle`. iOS only: Compose has no equivalent, so Android ignores it and keeps
@@ -22,19 +24,28 @@ export type ButtonStyleName =
22
24
  */
23
25
  export type ButtonControlSize = 'extraLarge' | 'large' | 'mini' | 'regular' | 'small'
24
26
 
25
- /** A text button: SwiftUI `Button` on iOS, Material's filled button (outlined for `glass`) on Android. */
27
+ /**
28
+ * A text button: SwiftUI `Button` on iOS, Material's filled button on Android (outlined for `glass`, a `TextButton`
29
+ * for `plain`).
30
+ */
26
31
  export type ButtonProps = {
27
32
  /** What pressing it does, when the label alone does not say. See `AccessibilityProps`. */
28
33
  accessibilityHint?: string
29
34
  /**
30
35
  * Replaces the style the variant picks: `borderedProminent` for `accent` and `destructive`, `bordered` for
31
- * `secondary`, `glass` for `glass`. iOS only. Android ignores it.
36
+ * `secondary`, `glass` for `glass`, `borderless` for `plain`. iOS only. Android ignores it.
32
37
  */
33
38
  buttonStyle?: ButtonStyleName
34
39
  /** SwiftUI's control size. `large` by default. iOS only. Android ignores it. */
35
40
  controlSize?: ButtonControlSize
36
41
  /** Draws the button in its disabled colours and stops it responding to presses. */
37
42
  disabled?: boolean
43
+ /**
44
+ * One of the app's icons, before the label and in the label's colour. iOS sets the two as a SwiftUI `Label`; Android
45
+ * puts the icon in the button's content row, as Material's buttons with an icon do. Screen readers read the label
46
+ * only.
47
+ */
48
+ icon?: IconName
38
49
  /** The button's text. */
39
50
  label: string
40
51
  /** Called when the button is pressed. */
@@ -44,10 +55,14 @@ export type ButtonProps = {
44
55
  /**
45
56
  * Overrides the variant's own tint. A prominent style takes it as the fill and picks a contrasting
46
57
  * label. `glass` has to reach the material through `glassEffect` instead, so tinting one costs it
47
- * the stock `.glass` style (see the iOS implementation).
58
+ * the stock `.glass` style (see the iOS implementation). `plain` has no fill, so there it colours the label.
48
59
  */
49
60
  tint?: ColorToken
50
- /** The look. `accent` by default. Material has no glass, so on Android `glass` is an outlined button. */
61
+ /**
62
+ * The look. `accent` by default. Material has no glass, so on Android `glass` is an outlined button. `plain` is a
63
+ * text button in the accent, with no fill or border, for a lesser action beside a prominent one ("Skip", "Forgot
64
+ * password?"): SwiftUI's `borderless` style, Material's `TextButton`.
65
+ */
51
66
  variant?: ButtonVariant
52
67
  /** Stretches the button to the full width it is offered. On by default. */
53
68
  wide?: boolean
@@ -1,5 +1,5 @@
1
1
  import {Checkbox as ComposeCheckbox, Row, Text} from '@expo/ui/jetpack-compose'
2
- import {clickable, fillMaxWidth} from '@expo/ui/jetpack-compose/modifiers'
2
+ import {clickable, fillMaxWidth, testID as testIdModifier} from '@expo/ui/jetpack-compose/modifiers'
3
3
  import {spacing, useAppTheme} from '@hzblj/kara-theme'
4
4
  import {useMemo} from 'react'
5
5
 
@@ -14,17 +14,20 @@ const NOOP = () => undefined
14
14
  * gets a handler, so a disabled or read-only box keeps the touch target an enabled one has, and lines up with it;
15
15
  * `enabled` is what stops the input.
16
16
  */
17
- export const Checkbox: CheckboxComponent = ({disabled = false, label, onValueChange, value}: CheckboxProps) => {
17
+ export const Checkbox: CheckboxComponent = ({disabled = false, label, onValueChange, testID, value}: CheckboxProps) => {
18
18
  useHostGuard('Checkbox')
19
19
  const theme = useAppTheme()
20
20
  const rowModifiers = useMemo(
21
21
  () => (disabled || !onValueChange ? [fillMaxWidth()] : [fillMaxWidth(), clickable(() => onValueChange(!value))]),
22
22
  [disabled, onValueChange, value]
23
23
  )
24
+ // On the box, not the row: the box is its own toggleable node inside the row, and the one a test reads.
25
+ const controlModifiers = useMemo(() => (testID ? [testIdModifier(testID)] : undefined), [testID])
24
26
  const control = (
25
27
  <ComposeCheckbox
26
28
  colors={{checkedColor: theme.color('fill.accent.primary'), checkmarkColor: theme.color('content.on.accent')}}
27
29
  enabled={!disabled}
30
+ modifiers={controlModifiers}
28
31
  onCheckedChange={onValueChange ?? NOOP}
29
32
  value={value}
30
33
  />