@utilitywarehouse/hearth-react-native 0.27.2 → 0.28.0-testid-fix-1

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 (155) hide show
  1. package/.turbo/turbo-build.log +5 -4
  2. package/.turbo/turbo-lint.log +70 -69
  3. package/CHANGELOG.md +149 -0
  4. package/build/components/Button/ButtonRoot.js +8 -0
  5. package/build/components/Combobox/Combobox.context.d.ts +13 -0
  6. package/build/components/Combobox/Combobox.context.js +9 -0
  7. package/build/components/Combobox/Combobox.d.ts +6 -0
  8. package/build/components/Combobox/Combobox.js +246 -0
  9. package/build/components/Combobox/Combobox.props.d.ts +180 -0
  10. package/build/components/Combobox/Combobox.props.js +1 -0
  11. package/build/components/Combobox/ComboboxOption.d.ts +6 -0
  12. package/build/components/Combobox/ComboboxOption.js +56 -0
  13. package/build/components/Combobox/index.d.ts +4 -0
  14. package/build/components/Combobox/index.js +3 -0
  15. package/build/components/DatePicker/TimePicker.d.ts +3 -0
  16. package/build/components/DatePicker/TimePicker.js +84 -0
  17. package/build/components/DatePicker/time-picker/animated-math.d.ts +4 -0
  18. package/build/components/DatePicker/time-picker/animated-math.js +19 -0
  19. package/build/components/DatePicker/time-picker/period-native.d.ts +6 -0
  20. package/build/components/DatePicker/time-picker/period-native.js +17 -0
  21. package/build/components/DatePicker/time-picker/period-picker.d.ts +6 -0
  22. package/build/components/DatePicker/time-picker/period-picker.js +10 -0
  23. package/build/components/DatePicker/time-picker/period-web.d.ts +6 -0
  24. package/build/components/DatePicker/time-picker/period-web.js +21 -0
  25. package/build/components/DatePicker/time-picker/wheel-native.d.ts +8 -0
  26. package/build/components/DatePicker/time-picker/wheel-native.js +19 -0
  27. package/build/components/DatePicker/time-picker/wheel-picker/index.d.ts +2 -0
  28. package/build/components/DatePicker/time-picker/wheel-picker/index.js +2 -0
  29. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.d.ts +16 -0
  30. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.js +97 -0
  31. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.d.ts +21 -0
  32. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.js +88 -0
  33. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.d.ts +23 -0
  34. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.js +21 -0
  35. package/build/components/DatePicker/time-picker/wheel-web.d.ts +8 -0
  36. package/build/components/DatePicker/time-picker/wheel-web.js +146 -0
  37. package/build/components/DatePicker/time-picker/wheel.d.ts +8 -0
  38. package/build/components/DatePicker/time-picker/wheel.js +10 -0
  39. package/build/components/List/List.js +2 -2
  40. package/build/components/Modal/Modal.js +31 -42
  41. package/build/components/Modal/Modal.web.js +3 -3
  42. package/build/components/Pagination/Pagination.d.ts +6 -0
  43. package/build/components/Pagination/Pagination.js +125 -0
  44. package/build/components/Pagination/Pagination.props.d.ts +26 -0
  45. package/build/components/Pagination/Pagination.props.js +1 -0
  46. package/build/components/Pagination/Pagination.utils.d.ts +2 -0
  47. package/build/components/Pagination/Pagination.utils.js +20 -0
  48. package/build/components/Pagination/Pagination.utils.test.d.ts +1 -0
  49. package/build/components/Pagination/Pagination.utils.test.js +16 -0
  50. package/build/components/Pagination/index.d.ts +2 -0
  51. package/build/components/Pagination/index.js +1 -0
  52. package/build/components/SafeAreaView/SafeAreaView.d.ts +5 -0
  53. package/build/components/SafeAreaView/SafeAreaView.js +117 -0
  54. package/build/components/SafeAreaView/SafeAreaView.props.d.ts +17 -0
  55. package/build/components/SafeAreaView/SafeAreaView.props.js +1 -0
  56. package/build/components/SafeAreaView/index.d.ts +2 -0
  57. package/build/components/SafeAreaView/index.js +1 -0
  58. package/build/components/Select/Select.d.ts +1 -1
  59. package/build/components/Select/Select.js +6 -5
  60. package/build/components/Select/Select.props.d.ts +4 -0
  61. package/build/components/Select/SelectOption.d.ts +1 -1
  62. package/build/components/Select/SelectOption.js +2 -2
  63. package/build/components/Table/Table.context.d.ts +12 -0
  64. package/build/components/Table/Table.context.js +9 -0
  65. package/build/components/Table/Table.d.ts +6 -0
  66. package/build/components/Table/Table.js +71 -0
  67. package/build/components/Table/Table.props.d.ts +56 -0
  68. package/build/components/Table/Table.props.js +1 -0
  69. package/build/components/Table/Table.utils.d.ts +5 -0
  70. package/build/components/Table/Table.utils.js +48 -0
  71. package/build/components/Table/Table.utils.test.d.ts +1 -0
  72. package/build/components/Table/Table.utils.test.js +71 -0
  73. package/build/components/Table/TableBody.d.ts +6 -0
  74. package/build/components/Table/TableBody.js +16 -0
  75. package/build/components/Table/TableCell.d.ts +10 -0
  76. package/build/components/Table/TableCell.js +44 -0
  77. package/build/components/Table/TableHeader.d.ts +6 -0
  78. package/build/components/Table/TableHeader.js +24 -0
  79. package/build/components/Table/TableHeaderCell.d.ts +10 -0
  80. package/build/components/Table/TableHeaderCell.js +97 -0
  81. package/build/components/Table/TablePagination.d.ts +6 -0
  82. package/build/components/Table/TablePagination.js +7 -0
  83. package/build/components/Table/TableRow.d.ts +8 -0
  84. package/build/components/Table/TableRow.js +25 -0
  85. package/build/components/Table/index.d.ts +8 -0
  86. package/build/components/Table/index.js +7 -0
  87. package/build/components/Timeline/Timeline.d.ts +6 -0
  88. package/build/components/Timeline/Timeline.js +34 -0
  89. package/build/components/Timeline/Timeline.props.d.ts +47 -0
  90. package/build/components/Timeline/Timeline.props.js +1 -0
  91. package/build/components/Timeline/TimelineItem.d.ts +6 -0
  92. package/build/components/Timeline/TimelineItem.js +235 -0
  93. package/build/components/Timeline/index.d.ts +3 -0
  94. package/build/components/Timeline/index.js +2 -0
  95. package/build/components/VerificationInput/VerificationInput.js +3 -3
  96. package/build/components/index.d.ts +5 -0
  97. package/build/components/index.js +5 -0
  98. package/build/tokens/components/dark/timeline.d.ts +2 -2
  99. package/build/tokens/components/dark/timeline.js +2 -2
  100. package/docs/components/AllComponents.web.tsx +106 -23
  101. package/docs/llm-docs/unistyles-llms-full.txt +1132 -534
  102. package/docs/llm-docs/unistyles-llms-small.txt +37 -37
  103. package/package.json +4 -4
  104. package/src/components/Button/Button.stories.tsx +43 -7
  105. package/src/components/Button/ButtonRoot.tsx +8 -0
  106. package/src/components/Combobox/Combobox.context.ts +26 -0
  107. package/src/components/Combobox/Combobox.docs.mdx +277 -0
  108. package/src/components/Combobox/Combobox.figma.tsx +60 -0
  109. package/src/components/Combobox/Combobox.props.ts +187 -0
  110. package/src/components/Combobox/Combobox.stories.tsx +233 -0
  111. package/src/components/Combobox/Combobox.tsx +446 -0
  112. package/src/components/Combobox/ComboboxOption.tsx +100 -0
  113. package/src/components/Combobox/index.ts +9 -0
  114. package/src/components/List/List.tsx +5 -4
  115. package/src/components/Modal/Modal.tsx +67 -74
  116. package/src/components/Modal/Modal.web.tsx +3 -3
  117. package/src/components/Pagination/Pagination.docs.mdx +99 -0
  118. package/src/components/Pagination/Pagination.figma.tsx +20 -0
  119. package/src/components/Pagination/Pagination.props.ts +28 -0
  120. package/src/components/Pagination/Pagination.stories.tsx +88 -0
  121. package/src/components/Pagination/Pagination.tsx +248 -0
  122. package/src/components/Pagination/Pagination.utils.test.ts +20 -0
  123. package/src/components/Pagination/Pagination.utils.ts +37 -0
  124. package/src/components/Pagination/index.ts +2 -0
  125. package/src/components/SafeAreaView/SafeAreaView.props.ts +20 -0
  126. package/src/components/SafeAreaView/SafeAreaView.tsx +173 -0
  127. package/src/components/SafeAreaView/index.ts +2 -0
  128. package/src/components/Select/Select.props.ts +4 -0
  129. package/src/components/Select/Select.tsx +35 -28
  130. package/src/components/Select/SelectOption.tsx +2 -0
  131. package/src/components/Table/Table.context.tsx +23 -0
  132. package/src/components/Table/Table.docs.mdx +239 -0
  133. package/src/components/Table/Table.figma.tsx +65 -0
  134. package/src/components/Table/Table.props.ts +65 -0
  135. package/src/components/Table/Table.stories.tsx +399 -0
  136. package/src/components/Table/Table.tsx +127 -0
  137. package/src/components/Table/Table.utils.test.ts +82 -0
  138. package/src/components/Table/Table.utils.ts +72 -0
  139. package/src/components/Table/TableBody.tsx +25 -0
  140. package/src/components/Table/TableCell.tsx +67 -0
  141. package/src/components/Table/TableHeader.tsx +41 -0
  142. package/src/components/Table/TableHeaderCell.tsx +136 -0
  143. package/src/components/Table/TablePagination.tsx +10 -0
  144. package/src/components/Table/TableRow.tsx +42 -0
  145. package/src/components/Table/index.ts +16 -0
  146. package/src/components/Timeline/Timeline.docs.mdx +177 -0
  147. package/src/components/Timeline/Timeline.figma.tsx +89 -0
  148. package/src/components/Timeline/Timeline.props.ts +51 -0
  149. package/src/components/Timeline/Timeline.stories.tsx +102 -0
  150. package/src/components/Timeline/Timeline.tsx +48 -0
  151. package/src/components/Timeline/TimelineItem.tsx +293 -0
  152. package/src/components/Timeline/index.ts +9 -0
  153. package/src/components/VerificationInput/VerificationInput.tsx +3 -0
  154. package/src/components/index.ts +5 -0
  155. package/src/tokens/components/dark/timeline.ts +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilitywarehouse/hearth-react-native",
3
- "version": "0.27.2",
3
+ "version": "0.28.0-testid-fix-1",
4
4
  "description": "Utility Warehouse React Native UI library",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -56,11 +56,11 @@
56
56
  "vite": "^7.1.3",
57
57
  "vite-plugin-svgr": "^4.5.0",
58
58
  "vitest": "^3.2.4",
59
- "@utilitywarehouse/hearth-fonts": "^0.0.4",
60
- "@utilitywarehouse/hearth-react-icons": "^0.8.0",
61
59
  "@utilitywarehouse/hearth-react-native-icons": "^0.8.0",
60
+ "@utilitywarehouse/hearth-fonts": "^0.0.4",
62
61
  "@utilitywarehouse/hearth-svg-assets": "^0.5.0",
63
- "@utilitywarehouse/hearth-tokens": "^0.2.4"
62
+ "@utilitywarehouse/hearth-tokens": "^0.2.4",
63
+ "@utilitywarehouse/hearth-react-icons": "^0.8.0"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "@gorhom/bottom-sheet": "^5.0.0",
@@ -1,4 +1,4 @@
1
- import { Meta, StoryObj } from '@storybook/react-vite';
1
+ import { Meta, StoryObj } from '@storybook/react-native';
2
2
  import * as Icons from '@utilitywarehouse/hearth-react-native-icons';
3
3
  import { AddSmallIcon, ChevronRightSmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
4
4
  import { Platform } from 'react-native';
@@ -92,7 +92,7 @@ type Story = StoryObj<typeof meta>;
92
92
 
93
93
  export const Playground: Story = {
94
94
  /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
95
- render: ({ icon: _icon, children: _, ...args }) => {
95
+ render: ({ icon: _icon, children: _, ...args }: StoryObj<typeof meta.args>) => {
96
96
  // @ts-expect-error - This is a playground
97
97
  const icon = _icon === 'none' ? undefined : Icons[_icon];
98
98
  return <Button {...args} icon={icon} />;
@@ -104,7 +104,7 @@ export const Variants: Story = {
104
104
  controls: { exclude: ['variant'] },
105
105
  },
106
106
  /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
107
- render: ({ icon: _icon, children: _, ...args }) => {
107
+ render: ({ icon: _icon, children: _, ...args }: StoryObj<typeof meta.args>) => {
108
108
  // @ts-expect-error - This is a playground
109
109
  const icon = _icon === 'none' ? undefined : Icons[_icon];
110
110
  return (
@@ -123,11 +123,9 @@ export const Variants: Story = {
123
123
  {args.colorScheme !== 'highlight' && (
124
124
  <>
125
125
  <VariantTitle title="Outline" invert={args.inverted}>
126
- {/* @ts-expect-error - story loop types don't match */}
127
126
  <Button {...args} variant="outline" icon={icon} />
128
127
  </VariantTitle>
129
128
  <VariantTitle title="Ghost" invert={args.inverted}>
130
- {/* @ts-expect-error - story loop types don't match */}
131
129
  <Button {...args} variant="ghost" icon={icon} />
132
130
  </VariantTitle>
133
131
  </>
@@ -138,6 +136,44 @@ export const Variants: Story = {
138
136
  },
139
137
  };
140
138
 
139
+ export const PaddingNone: Story = {
140
+ parameters: {
141
+ controls: {
142
+ include: ['text', 'size', 'inverted', 'icon', 'iconPosition'],
143
+ },
144
+ },
145
+ /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
146
+ render: ({ icon: _icon, children: _, ...args }: StoryObj<typeof meta.args>) => {
147
+ // @ts-expect-error - This is a playground
148
+ const icon = _icon === 'none' ? undefined : Icons[_icon];
149
+
150
+ return (
151
+ <Flex direction="column" spacing="lg">
152
+ <VariantTitle title="Default Padding" invert={args.inverted}>
153
+ <Flex direction="row" align="center" spacing="none">
154
+ <Box backgroundColor="brand" width="100" height="100" />
155
+ <Button
156
+ {...args}
157
+ colorScheme="functional"
158
+ variant="ghost"
159
+ icon={icon}
160
+ paddingNone={false}
161
+ />
162
+ <Box backgroundColor="brand" width="100" height="100" />
163
+ </Flex>
164
+ </VariantTitle>
165
+ <VariantTitle title="No Padding (paddingNone)" invert={args.inverted}>
166
+ <Flex direction="row" align="center" spacing="none">
167
+ <Box backgroundColor="brand" width="100" height="100" />
168
+ <Button {...args} colorScheme="functional" variant="ghost" icon={icon} paddingNone />
169
+ <Box backgroundColor="brand" width="100" height="100" />
170
+ </Flex>
171
+ </VariantTitle>
172
+ </Flex>
173
+ );
174
+ },
175
+ };
176
+
141
177
  type ColorScheme = ButtonProps['colorScheme'];
142
178
  type Variant = ButtonProps['variant'];
143
179
 
@@ -145,7 +181,7 @@ export const KitchenSink: Story = {
145
181
  parameters: {
146
182
  controls: { include: ['text', 'size', 'inverted'] },
147
183
  },
148
- render: ({ text, inverted, size }) => {
184
+ render: ({ text, inverted, size }: StoryObj<typeof meta.args>) => {
149
185
  const schemes: Array<ColorScheme> = ['highlight', 'destructive', 'affirmative', 'functional'];
150
186
  const variants: Array<Variant> = ['emphasis', 'solid', 'outline', 'ghost'];
151
187
  return (
@@ -174,7 +210,7 @@ export const KitchenSink: Story = {
174
210
  .map(variant => (
175
211
  <Box key={variant} mb="100">
176
212
  <Box mb="100">
177
- <DetailText size="lg" color={inverted ? 'warmWhite50' : 'grey1000'}>
213
+ <DetailText size="lg" inverted={inverted}>
178
214
  {scheme} - {variant}
179
215
  </DetailText>
180
216
  </Box>
@@ -132,6 +132,14 @@ const styles = StyleSheet.create(theme => ({
132
132
  paddingHorizontal: 0,
133
133
  },
134
134
  },
135
+ {
136
+ size: 'md',
137
+ paddingNone: true,
138
+ variant: 'ghost',
139
+ styles: {
140
+ paddingHorizontal: 0,
141
+ },
142
+ },
135
143
  // Variant Color Schemes
136
144
  // Emphasis
137
145
  // Emphasis Yellow
@@ -0,0 +1,26 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ export interface ComboboxSelection {
4
+ label: string;
5
+ value: string;
6
+ }
7
+
8
+ export interface ComboboxContextValue {
9
+ close: () => void;
10
+ search: string;
11
+ selectedValue?: string | null;
12
+ selectOption: (option: ComboboxSelection) => void;
13
+ setSearch: (value: string) => void;
14
+ }
15
+
16
+ export const ComboboxContext = createContext<ComboboxContextValue | undefined>(undefined);
17
+
18
+ export const useComboboxContext = () => {
19
+ const context = useContext(ComboboxContext);
20
+
21
+ if (!context) {
22
+ throw new Error('useComboboxContext must be used within a Combobox');
23
+ }
24
+
25
+ return context;
26
+ };
@@ -0,0 +1,277 @@
1
+ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks';
2
+ import { BottomSheetFlatList, Box, Center, Combobox, ComboboxOption } from '../../';
3
+ import { BackToTopButton, UsageWrap, ViewFigmaButton } from '../../../docs/components';
4
+ import * as Stories from './Combobox.stories';
5
+
6
+ <Meta title="Forms / Combobox" />
7
+
8
+ <BackToTopButton />
9
+
10
+ <ViewFigmaButton url="https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=9359-2923&t=c7xg5X0N2EL0t87h-4" />
11
+
12
+ # Combobox
13
+
14
+ The `Combobox` component lets people search and select from a list inside a bottom sheet. It keeps the trigger text and the bottom sheet search input in sync, and can render either the built-in options list or fully custom sheet content.
15
+
16
+ - [Playground](#playground)
17
+ - [Usage](#usage)
18
+ - [Props](#props)
19
+ - [Examples](#examples)
20
+ - [Accessibility](#accessibility)
21
+
22
+ ## Playground
23
+
24
+ <Canvas of={Stories.Playground} />
25
+
26
+ <Controls of={Stories.Playground} />
27
+
28
+ ## Usage
29
+
30
+ <UsageWrap>
31
+ <Center>
32
+ <Combobox
33
+ label="Country"
34
+ placeholder="Search for a country"
35
+ searchPlaceholder="Search for a country"
36
+ options={[
37
+ { label: 'United Kingdom', value: 'uk' },
38
+ { label: 'United States', value: 'us' },
39
+ { label: 'Canada', value: 'ca' },
40
+ ]}
41
+ value="uk"
42
+ />
43
+ </Center>
44
+ </UsageWrap>
45
+
46
+ ```tsx
47
+ import { Combobox } from '@utilitywarehouse/hearth-react-native';
48
+ import { useState } from 'react';
49
+
50
+ const countries = [
51
+ { label: 'United Kingdom', value: 'uk' },
52
+ { label: 'United States', value: 'us' },
53
+ { label: 'Canada', value: 'ca' },
54
+ ];
55
+
56
+ const MyComponent = () => {
57
+ const [value, setValue] = useState<string | null>(null);
58
+
59
+ return (
60
+ <Combobox
61
+ label="Country"
62
+ placeholder="Search for a country"
63
+ searchPlaceholder="Search for a country"
64
+ options={countries}
65
+ value={value}
66
+ onValueChange={setValue}
67
+ />
68
+ );
69
+ };
70
+ ```
71
+
72
+ ## Props
73
+
74
+ The `Combobox` component inherits all of the React Native [`View` props](https://reactnative.dev/docs/view).
75
+
76
+ | Prop | Type | Default | Description |
77
+ | -------------------- | ----------------------------------- | -------------------- | ---------------------------------------------------------------------------- |
78
+ | `options` | `ComboboxOptionItemProps[]` | `[]` | Array of options for the built-in bottom sheet list. |
79
+ | `value` | `string \| null` | `-` | Currently selected value. |
80
+ | `onValueChange` | `(value: string \| null) => void` | `-` | Callback when the selected value changes. |
81
+ | `inputValue` | `string` | `-` | Controlled search value for the trigger and bottom sheet input. |
82
+ | `onInputValueChange` | `(value: string) => void` | `-` | Callback when the search value changes. |
83
+ | `placeholder` | `string` | `'Search'` | Placeholder shown in the trigger when empty. |
84
+ | `searchPlaceholder` | `string` | `'Search'` | Placeholder shown in the bottom sheet search input. |
85
+ | `menuHeading` | `string` | `-` | Optional heading shown at the top of the bottom sheet. |
86
+ | `children` | `ReactNode \| (props) => ReactNode` | `-` | Custom bottom sheet content, or a render function for custom list rendering. |
87
+ | `loading` | `boolean` | `false` | Displays a loading spinner in the trigger and sheet input. |
88
+ | `noOptionsFoundText` | `string` | `'No options found'` | Empty state text for the built-in options list. |
89
+ | `getValueLabel` | `(value) => string` | `-` | Resolve a label for selected values that are not present in `options`. |
90
+ | `filterOption` | `(option, search) => boolean` | `-` | Override the default label and keyword filtering logic. |
91
+
92
+ ## Examples
93
+
94
+ ### Static Searchable List
95
+
96
+ <Canvas of={Stories.StaticSearchableList} />
97
+
98
+ ```tsx
99
+ import { Combobox } from '@utilitywarehouse/hearth-react-native';
100
+ import { useState } from 'react';
101
+
102
+ const countries = [
103
+ { label: 'United Kingdom', value: 'uk', keywords: ['britain', 'england'] },
104
+ { label: 'United States', value: 'us', keywords: ['america', 'usa'] },
105
+ { label: 'Canada', value: 'ca' },
106
+ { label: 'Australia', value: 'au' },
107
+ { label: 'France', value: 'fr' },
108
+ ];
109
+
110
+ const MyComponent = () => {
111
+ const [value, setValue] = useState<string | null>(null);
112
+
113
+ return (
114
+ <Combobox
115
+ label="Country"
116
+ helperText="Search a fixed list of countries"
117
+ placeholder="Search for a country"
118
+ searchPlaceholder="Search for a country"
119
+ menuHeading="Choose a country"
120
+ options={countries}
121
+ value={value}
122
+ onValueChange={setValue}
123
+ />
124
+ );
125
+ };
126
+ ```
127
+
128
+ ### Dynamic Items
129
+
130
+ <Canvas of={Stories.DynamicItems} />
131
+
132
+ ```tsx
133
+ import { Combobox } from '@utilitywarehouse/hearth-react-native';
134
+ import { useEffect, useState } from 'react';
135
+
136
+ const allProducts = [
137
+ { label: 'Broadband', value: 'broadband' },
138
+ { label: 'Energy', value: 'energy' },
139
+ { label: 'Mobile', value: 'mobile' },
140
+ ];
141
+
142
+ const MyComponent = () => {
143
+ const [value, setValue] = useState<string | null>(null);
144
+ const [inputValue, setInputValue] = useState('');
145
+ const [loading, setLoading] = useState(false);
146
+ const [options, setOptions] = useState(allProducts);
147
+
148
+ useEffect(() => {
149
+ setLoading(true);
150
+
151
+ const timeout = setTimeout(() => {
152
+ const query = inputValue.trim().toLowerCase();
153
+ setOptions(allProducts.filter(option => option.label.toLowerCase().includes(query)));
154
+ setLoading(false);
155
+ }, 300);
156
+
157
+ return () => clearTimeout(timeout);
158
+ }, [inputValue]);
159
+
160
+ return (
161
+ <Combobox
162
+ label="Products"
163
+ options={options}
164
+ value={value}
165
+ onValueChange={setValue}
166
+ inputValue={inputValue}
167
+ onInputValueChange={setInputValue}
168
+ loading={loading}
169
+ getValueLabel={selectedValue =>
170
+ allProducts.find(option => option.value === selectedValue)?.label ?? ''
171
+ }
172
+ />
173
+ );
174
+ };
175
+ ```
176
+
177
+ ### Custom Bottom Sheet Flat List
178
+
179
+ <Canvas of={Stories.CustomFlatList} />
180
+
181
+ ```tsx
182
+ import {
183
+ BottomSheetFlatList,
184
+ Combobox,
185
+ ComboboxOption,
186
+ } from '@utilitywarehouse/hearth-react-native';
187
+
188
+ const cities = [
189
+ { label: 'London', value: 'london' },
190
+ { label: 'Liverpool', value: 'liverpool' },
191
+ { label: 'Leeds', value: 'leeds' },
192
+ ];
193
+
194
+ const MyComponent = () => {
195
+ return (
196
+ <Combobox label="Cities" placeholder="Search a city">
197
+ {({ search }) => {
198
+ const query = search.trim().toLowerCase();
199
+ const filteredCities = cities.filter(option => option.label.toLowerCase().includes(query));
200
+
201
+ return (
202
+ <BottomSheetFlatList
203
+ data={filteredCities}
204
+ keyExtractor={item => item.value}
205
+ renderItem={({ item }) => <ComboboxOption label={item.label} value={item.value} />}
206
+ />
207
+ );
208
+ }}
209
+ </Combobox>
210
+ );
211
+ };
212
+ ```
213
+
214
+ ### Inline Custom Content
215
+
216
+ <UsageWrap>
217
+ <Center>
218
+ <Combobox label="Inline options" placeholder="Search for a country">
219
+ <Box>
220
+ <ComboboxOption label="United Kingdom" value="uk" />
221
+ <ComboboxOption label="United States" value="us" />
222
+ <ComboboxOption label="Canada" value="ca" />
223
+ </Box>
224
+ </Combobox>
225
+ </Center>
226
+ </UsageWrap>
227
+
228
+ ```tsx
229
+ import { Box, Combobox, ComboboxOption } from '@utilitywarehouse/hearth-react-native';
230
+ import { useState } from 'react';
231
+
232
+ const countries = [
233
+ { label: 'United Kingdom', value: 'uk' },
234
+ { label: 'United States', value: 'us' },
235
+ { label: 'Canada', value: 'ca' },
236
+ ];
237
+
238
+ const MyComponent = () => {
239
+ const [value, setValue] = useState<string | null>(null);
240
+
241
+ return (
242
+ <Combobox
243
+ label="Inline options"
244
+ helperText="Compose your own sheet content using ComboboxOption"
245
+ placeholder="Search a country"
246
+ searchPlaceholder="Search a country"
247
+ value={value}
248
+ onValueChange={setValue}
249
+ >
250
+ <Box>
251
+ {countries.map(option => (
252
+ <ComboboxOption key={option.value} label={option.label} value={option.value} />
253
+ ))}
254
+ </Box>
255
+ </Combobox>
256
+ );
257
+ };
258
+ ```
259
+
260
+ ## Accessibility
261
+
262
+ The `Combobox` is implemented as a button that opens a bottom sheet. By default:
263
+
264
+ - The trigger uses an accessible label derived from the `label` prop (and, where appropriate, the current value or `placeholder`) and is exposed with `accessibilityRole="button"`.
265
+ - When the bottom sheet is open, the trigger reflects its state via `accessibilityState={{ expanded: true }}`; when the sheet is closed, it reports `expanded: false`. Screen readers should announce this state change as “expanded” / “collapsed”.
266
+ - The clear control (shown when a value is selected and clearing is allowed) is exposed as a separate button with an accessibility label such as “Clear selection”, so that screen reader users understand its purpose.
267
+
268
+ When you rely on the built‑in options list via the `options` prop, each option is rendered with appropriate roles and labels so that it can be discovered and activated with assistive technology.
269
+
270
+ When you provide fully custom sheet content instead of using `ComboboxOption`:
271
+
272
+ - Make sure interactive elements inside the sheet have the correct `accessibilityRole` (for example, `button` or `checkbox`) and meaningful `accessibilityLabel`s.
273
+ - Ensure that the primary search field inside the sheet remains focusable and clearly labelled with `searchPlaceholder` or a custom accessible label.
274
+ - Provide clear labelling or headings for grouped content, and ensure focus order inside the sheet is logical.
275
+ - Keep a visible and accessible way to dismiss the sheet (for example, a close button with an appropriate accessibility label).
276
+
277
+ These practices help ensure that the combobox remains understandable and operable for screen reader and keyboard users, regardless of whether you use the built‑in options list or custom content.
@@ -0,0 +1,60 @@
1
+ import figma from '@figma/code-connect';
2
+ import { useState } from 'react';
3
+ import { Combobox } from '../';
4
+
5
+ figma.connect(
6
+ Combobox,
7
+ 'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=9359%3A2923',
8
+ {
9
+ props: {
10
+ label: figma.string('Label'),
11
+ placeholder: figma.enum('Value type', {
12
+ Empty: figma.string('Value'),
13
+ }),
14
+ disabled: figma.enum('Variant', {
15
+ Disabled: true,
16
+ }),
17
+ validationStatus: figma.enum('Variant', {
18
+ Invalid: 'invalid',
19
+ }),
20
+ invalidText: figma.enum('Variant', {
21
+ Invalid: figma.string('Validation'),
22
+ }),
23
+ helperText: figma.boolean('Helper text?', {
24
+ true: figma.string('Helper text'),
25
+ }),
26
+ required: figma.boolean('Optional?', {
27
+ true: false,
28
+ }),
29
+ loading: figma.enum('Variant', {
30
+ Loading: true,
31
+ }),
32
+ value: figma.enum('Value type', {
33
+ Filled: figma.string('Value'),
34
+ }),
35
+ },
36
+ example: props => {
37
+ const [value, setValue] = useState<string | null>(props.value ? '1' : null);
38
+
39
+ return (
40
+ <Combobox
41
+ label={props.label}
42
+ placeholder={props.placeholder}
43
+ disabled={props.disabled}
44
+ validationStatus={props.validationStatus}
45
+ invalidText={props.invalidText}
46
+ helperText={props.helperText}
47
+ required={props.required}
48
+ loading={props.loading}
49
+ options={[
50
+ { label: 'Option 1', value: '1' },
51
+ { label: 'Option 2', value: '2' },
52
+ { label: 'Option 3', value: '3' },
53
+ ]}
54
+ value={value}
55
+ onValueChange={setValue}
56
+ />
57
+ );
58
+ },
59
+ }
60
+ );
@@ -0,0 +1,187 @@
1
+ import { BottomSheetFlatListProps } from '@gorhom/bottom-sheet/lib/typescript/components/bottomSheetScrollable/types';
2
+ import { ComponentType, ReactNode } from 'react';
3
+ import { ViewProps } from 'react-native';
4
+ import { BottomSheetProps } from '../BottomSheet';
5
+ import { ComboboxSelection } from './Combobox.context';
6
+
7
+ type ValidationStatus = 'initial' | 'valid' | 'invalid';
8
+
9
+ export interface ComboboxOptionItemProps {
10
+ /**
11
+ * Label shown for this option.
12
+ */
13
+ label: string;
14
+ /**
15
+ * Value returned when this option is selected.
16
+ */
17
+ value: string;
18
+ /**
19
+ * Optional icon shown before the option label.
20
+ */
21
+ leadingIcon?: ComponentType;
22
+ /**
23
+ * Optional icon shown after the option label.
24
+ */
25
+ trailingIcon?: ComponentType;
26
+ /**
27
+ * Additional searchable terms for this option.
28
+ */
29
+ keywords?: string[];
30
+ /**
31
+ * Whether this option is disabled.
32
+ */
33
+ disabled?: boolean;
34
+ }
35
+
36
+ export interface ComboboxRenderContentProps {
37
+ /**
38
+ * Current search value used by both the trigger and bottom sheet input.
39
+ */
40
+ search: string;
41
+ /**
42
+ * Update the search value.
43
+ */
44
+ setSearch: (value: string) => void;
45
+ /**
46
+ * Close the bottom sheet.
47
+ */
48
+ close: () => void;
49
+ /**
50
+ * Select an option, update the trigger text and close the bottom sheet.
51
+ */
52
+ selectOption: (option: ComboboxSelection) => void;
53
+ /**
54
+ * Currently selected value.
55
+ */
56
+ selectedValue?: string | null;
57
+ }
58
+
59
+ export interface ComboboxOptionProps extends ComboboxOptionItemProps {
60
+ /**
61
+ * Whether this option is selected.
62
+ */
63
+ selected?: boolean;
64
+ /**
65
+ * Custom press handler for this option.
66
+ */
67
+ onPress?: (value: string) => void;
68
+ }
69
+
70
+ type ComboboxChildren = ReactNode | ((props: ComboboxRenderContentProps) => ReactNode);
71
+
72
+ interface ComboboxProps extends Omit<ViewProps, 'children'> {
73
+ /**
74
+ * Array of options to render using the default bottom sheet list.
75
+ */
76
+ options?: ComboboxOptionItemProps[];
77
+ /**
78
+ * Currently selected value.
79
+ */
80
+ value?: string | null;
81
+ /**
82
+ * Callback fired when the selected value changes.
83
+ */
84
+ onValueChange?: (value: string | null) => void;
85
+ /**
86
+ * Controlled search value used by the trigger and bottom sheet input.
87
+ */
88
+ inputValue?: string;
89
+ /**
90
+ * Callback fired when the search value changes.
91
+ */
92
+ onInputValueChange?: (value: string) => void;
93
+ /**
94
+ * Label shown above the combobox.
95
+ */
96
+ label?: string;
97
+ /**
98
+ * The variant of the label text.
99
+ *
100
+ * @default 'body'
101
+ */
102
+ labelVariant?: 'heading' | 'body';
103
+ /**
104
+ * Helper text shown below the label.
105
+ */
106
+ helperText?: string;
107
+ /**
108
+ * Optional helper icon.
109
+ */
110
+ helperIcon?: ComponentType;
111
+ /**
112
+ * Text shown for invalid state.
113
+ */
114
+ invalidText?: string;
115
+ /**
116
+ * Text shown for valid state.
117
+ */
118
+ validText?: string;
119
+ /**
120
+ * Placeholder shown when the combobox is empty.
121
+ *
122
+ * @default 'Search'
123
+ */
124
+ placeholder?: string;
125
+ /**
126
+ * Placeholder shown in the bottom sheet search input.
127
+ *
128
+ * @default 'Search'
129
+ */
130
+ searchPlaceholder?: string;
131
+ /**
132
+ * Whether the combobox is disabled.
133
+ */
134
+ disabled?: boolean;
135
+ /**
136
+ * Validation status for the combobox.
137
+ *
138
+ * @default 'initial'
139
+ */
140
+ validationStatus?: ValidationStatus;
141
+ /**
142
+ * Whether the combobox is required.
143
+ *
144
+ * @default true
145
+ */
146
+ required?: boolean;
147
+ /**
148
+ * Heading displayed above the bottom sheet search input.
149
+ */
150
+ menuHeading?: string;
151
+ /**
152
+ * Custom bottom sheet content, or a render function for fully custom list rendering.
153
+ */
154
+ children?: ComboboxChildren;
155
+ /**
156
+ * Props passed to the BottomSheetModal.
157
+ */
158
+ bottomSheetProps?: Partial<BottomSheetProps>;
159
+ /**
160
+ * Text shown when no options match the current search.
161
+ *
162
+ * @default 'No options found'
163
+ */
164
+ noOptionsFoundText?: string;
165
+ /**
166
+ * Props passed to the default BottomSheetFlatList renderer.
167
+ */
168
+ listProps?: Partial<BottomSheetFlatListProps<ComboboxOptionItemProps>>;
169
+ /**
170
+ * Whether the combobox should show a loading spinner.
171
+ */
172
+ loading?: boolean;
173
+ /**
174
+ * Whether the combobox is readonly.
175
+ */
176
+ readonly?: boolean;
177
+ /**
178
+ * Resolve a label for a selected value when the current options array does not contain it.
179
+ */
180
+ getValueLabel?: (value: string | null | undefined) => string;
181
+ /**
182
+ * Override the default filtering behaviour for the default options list.
183
+ */
184
+ filterOption?: (option: ComboboxOptionItemProps, search: string) => boolean;
185
+ }
186
+
187
+ export default ComboboxProps;