@jobber/components-native 0.113.3 → 0.113.4

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.
@@ -1,8 +1,12 @@
1
1
  # Select
2
2
 
3
- Select presents a defined list of options to choose from. It is a prop-driven,
4
- labelled dropdown wrapped in a field structure that handles the label,
5
- description, and error messaging for you.
3
+ ## Summary
4
+
5
+ Select presents a defined list of options and lets the user pick a single value.
6
+ Use it when the choice is one of a known, reasonably short list that lives
7
+ inside a form. For longer lists or free-text search, use
8
+ [Autocomplete](/components/Autocomplete); for triggering an action rather than
9
+ picking a value, use [Menu](/components/Menu).
6
10
 
7
11
  ```tsx
8
12
  import React, { useState } from "react";
@@ -21,21 +25,30 @@ export function SelectBasicExample() {
21
25
  }
22
26
  ```
23
27
 
24
- ## Design & usage guidelines
28
+ ## Anatomy
25
29
 
26
- Use Select when a user needs to pick a single value from a known, reasonably
27
- short list of options. Each option is declared with `Select.Item`, where the
28
- `value` is submitted and the children are shown as the label.
30
+ | Part | Description |
31
+ | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
32
+ | Trigger | The field the user clicks to open the list |
33
+ | Label | Names the field. Sits inside the trigger when empty, floats to a mini-label once a value is chosen. Doubles as the empty-state hint |
34
+ | Value | The selected option, shown inside the trigger |
35
+ | Dropdown | The floating panel of options on large screens |
36
+ | Bottom sheet | The panel of options on small screens (≤490px). Automatic |
37
+ | Item | An individual option in the list |
38
+ | Group | Optional — wraps related items under a shared heading |
39
+ | Group label | Optional — the heading shown above a group |
40
+ | Separator | Optional — a divider between items outside of groups |
41
+ | Description | Optional — helper text shown beneath the field |
42
+ | Error message | Optional — replaces the description while an error is present |
29
43
 
30
- Select is controlled: pass the current `value` and an `onValueChange` handler to
31
- keep your state in sync.
44
+ ## Behaviour
32
45
 
33
- ## Empty state
46
+ #### Label as placeholder
34
47
 
35
- The `label` doubles as the placeholder: while no value is selected it sits
36
- centered inside the field, then rises to a floating mini-label once the user
37
- picks an option. There is no separate `placeholder` prop — the label fills that
38
- role.
48
+ The `label` names the field and doubles as the empty state. While no value is
49
+ selected it sits centered inside the trigger; once the user picks an option it
50
+ floats up as a mini-label. There is no separate `placeholder` prop — the label
51
+ fills that role.
39
52
 
40
53
  ```tsx
41
54
  import React, { useState } from "react";
@@ -54,89 +67,62 @@ export function SelectEmptyExample() {
54
67
  }
55
68
  ```
56
69
 
57
- ## Description
70
+ #### Selected value display
58
71
 
59
- Use `description` to add supporting help text beneath the field.
72
+ The closed trigger shows the selected `value`, capitalized (e.g. `"active"` →
73
+ `"Active"`). It does **not** read the item's children. When the display label
74
+ differs from the capitalized value, pass `renderValue` to control what the
75
+ trigger shows — otherwise the trigger will silently drift from the list.
60
76
 
61
77
  ```tsx
62
78
  import React, { useState } from "react";
63
79
  import { Select } from "@jobber/components/Select";
64
80
 
65
- export function SelectDescriptionExample() {
66
- const [value, setValue] = useState<string | undefined>();
67
-
68
- return (
69
- <Select
70
- label="Status"
71
- description="This controls who can see the record."
72
- value={value}
73
- onValueChange={setValue}
74
- >
75
- <Select.Item value="active">Active</Select.Item>
76
- <Select.Item value="archived">Archived</Select.Item>
77
- <Select.Item value="draft">Draft</Select.Item>
78
- </Select>
79
- );
80
- }
81
- ```
82
-
83
- ## States
84
-
85
- ### Error
86
-
87
- Pass an `error` message to mark the field invalid and show the message beneath
88
- the field. The error replaces the description while it is present. Use `invalid`
89
- instead to apply the invalid styling without a message — for example when a form
90
- library such as React Hook Form renders the error text itself.
91
-
92
- ```tsx
93
- import React, { useState } from "react";
94
- import { Select } from "@jobber/components/Select";
81
+ const PRIORITY_LABELS: Record<string, string> = {
82
+ low: "Low priority",
83
+ medium: "Medium priority",
84
+ high: "High priority",
85
+ };
95
86
 
96
- export function SelectErrorExample() {
97
- const [value, setValue] = useState<string | undefined>();
87
+ export function SelectRenderValueExample() {
88
+ const [value, setValue] = useState<string | undefined>("high");
98
89
 
99
90
  return (
100
91
  <Select
101
- label="Status"
102
- description="This controls who can see the record."
103
- error="Please choose a status."
92
+ label="Priority"
104
93
  value={value}
105
94
  onValueChange={setValue}
95
+ renderValue={selected => PRIORITY_LABELS[selected]}
106
96
  >
107
- <Select.Item value="active">Active</Select.Item>
108
- <Select.Item value="archived">Archived</Select.Item>
109
- <Select.Item value="draft">Draft</Select.Item>
97
+ <Select.Item value="low">Low</Select.Item>
98
+ <Select.Item value="medium">Medium</Select.Item>
99
+ <Select.Item value="high">High</Select.Item>
110
100
  </Select>
111
101
  );
112
102
  }
113
103
  ```
114
104
 
115
- ### Disabled
105
+ #### Small screens
116
106
 
117
- Set `disabled` to prevent interaction with the field.
107
+ On viewports ≤490px the options open as a bottom sheet instead of an anchored
108
+ dropdown, matching the pattern used by [Menu](/components/Menu) and
109
+ [Dialog](/components/Dialog). This is automatic — the same authored child tree
110
+ renders on both. Resize the preview narrow to see the sheet.
118
111
 
119
- ```tsx
120
- import React from "react";
121
- import { Select } from "@jobber/components/Select";
112
+ #### Controlled only
122
113
 
123
- export function SelectDisabledExample() {
124
- return (
125
- <Select label="Status" disabled value="active">
126
- <Select.Item value="active">Active</Select.Item>
127
- <Select.Item value="archived">Archived</Select.Item>
128
- <Select.Item value="draft">Draft</Select.Item>
129
- </Select>
130
- );
131
- }
132
- ```
114
+ Select is controlled: always pass `value` and `onValueChange`. There is no
115
+ uncontrolled `defaultValue`. The change prop is named `onValueChange` (not
116
+ `onChange`) to match the Base UI foundation and to encode the payload — the
117
+ value, not an event — in its name. See [Content guidelines](#content-guidelines)
118
+ for how this affects form-library wiring.
133
119
 
134
- ## Grouping
120
+ #### Grouping
135
121
 
136
122
  Organize related options under section headers with `Select.Group` and
137
123
  `Select.GroupLabel`. Adjacent groups are divided automatically, so you don't
138
- need to add a separator between them. `Select.Separator` is available for adding
139
- a divider elsewhere in the list.
124
+ need a `Select.Separator` between them — reach for `Select.Separator` only to
125
+ divide items that sit outside groups.
140
126
 
141
127
  ```tsx
142
128
  import React, { useState } from "react";
@@ -162,47 +148,85 @@ export function SelectGroupedExample() {
162
148
  }
163
149
  ```
164
150
 
165
- ## Custom selected value
151
+ #### States
166
152
 
167
- The closed trigger shows the selected value capitalized by default. Pass
168
- `renderValue` to display something different from the option's label — here the
169
- list shows short labels (`Low`, `Medium`, `High`) while the trigger shows the
170
- full `"High priority"`. The open list always shows each `Select.Item`'s
171
- children.
153
+ * **Description** pass `description` to add supporting help text beneath the
154
+ field.
155
+ * **Error** pass an `error` message to mark the field invalid and show the
156
+ message beneath the field. The error replaces the description while it is
157
+ present.
158
+ * **Invalid without a message** — use `invalid` on its own to apply the invalid
159
+ styling without a message, for example when a form library renders the error
160
+ text itself.
161
+ * **Disabled** — set `disabled` to prevent interaction with the field.
172
162
 
173
163
  ```tsx
174
164
  import React, { useState } from "react";
175
165
  import { Select } from "@jobber/components/Select";
176
166
 
177
- const PRIORITY_LABELS: Record<string, string> = {
178
- low: "Low priority",
179
- medium: "Medium priority",
180
- high: "High priority",
181
- };
167
+ export function SelectDescriptionExample() {
168
+ const [value, setValue] = useState<string | undefined>();
182
169
 
183
- export function SelectRenderValueExample() {
184
- const [value, setValue] = useState<string | undefined>("high");
170
+ return (
171
+ <Select
172
+ label="Status"
173
+ description="This controls who can see the record."
174
+ value={value}
175
+ onValueChange={setValue}
176
+ >
177
+ <Select.Item value="active">Active</Select.Item>
178
+ <Select.Item value="archived">Archived</Select.Item>
179
+ <Select.Item value="draft">Draft</Select.Item>
180
+ </Select>
181
+ );
182
+ }
183
+ ```
184
+
185
+ ```tsx
186
+ import React, { useState } from "react";
187
+ import { Select } from "@jobber/components/Select";
188
+
189
+ export function SelectErrorExample() {
190
+ const [value, setValue] = useState<string | undefined>();
185
191
 
186
192
  return (
187
193
  <Select
188
- label="Priority"
194
+ label="Status"
195
+ description="This controls who can see the record."
196
+ error="Please choose a status."
189
197
  value={value}
190
198
  onValueChange={setValue}
191
- renderValue={selected => PRIORITY_LABELS[selected]}
192
199
  >
193
- <Select.Item value="low">Low</Select.Item>
194
- <Select.Item value="medium">Medium</Select.Item>
195
- <Select.Item value="high">High</Select.Item>
200
+ <Select.Item value="active">Active</Select.Item>
201
+ <Select.Item value="archived">Archived</Select.Item>
202
+ <Select.Item value="draft">Draft</Select.Item>
196
203
  </Select>
197
204
  );
198
205
  }
199
206
  ```
200
207
 
201
- ## Sizes
208
+ ```tsx
209
+ import React from "react";
210
+ import { Select } from "@jobber/components/Select";
211
+
212
+ export function SelectDisabledExample() {
213
+ return (
214
+ <Select label="Status" disabled value="active">
215
+ <Select.Item value="active">Active</Select.Item>
216
+ <Select.Item value="archived">Archived</Select.Item>
217
+ <Select.Item value="draft">Draft</Select.Item>
218
+ </Select>
219
+ );
220
+ }
221
+ ```
222
+
223
+ ## Variants
224
+
225
+ ### Sizes
202
226
 
203
- Use the `size` prop to render a `small` control for tighter layouts. The
204
- floating mini-label is hidden at the `small` size, so the compact control keeps
205
- a single-line height.
227
+ Use `size="small"` for tighter layouts. The floating mini-label is hidden at the
228
+ small size, so the compact control keeps a single-line height. Prefer the
229
+ default (large) size in forms so the label remains visible after selection.
206
230
 
207
231
  ```tsx
208
232
  import React, { useState } from "react";
@@ -221,10 +245,11 @@ export function SelectSizeExample() {
221
245
  }
222
246
  ```
223
247
 
224
- ## Inline
248
+ ### Inline
225
249
 
226
250
  Set `inline` to embed the Select within a line of text. The description and
227
- error messaging are suppressed in inline mode.
251
+ error messaging are suppressed in inline mode, so pair it with validation that
252
+ lives elsewhere in the surrounding sentence or paragraph.
228
253
 
229
254
  ```tsx
230
255
  import React, { useState } from "react";
@@ -247,48 +272,170 @@ export function SelectInlineExample() {
247
272
  }
248
273
  ```
249
274
 
250
- ## Mobile
275
+ ## Content guidelines
251
276
 
252
- On small (touch-sized) web screens viewport widths of `490px` or less the
253
- options open as a bottom sheet instead of an anchored dropdown, matching the
254
- mobile pattern used by `Menu` and `Dialog`. This is automatic and requires no
255
- props; selection, keyboard, and focus behaviour are unchanged. Resize the
256
- preview narrow (or open the examples above on a phone) to see the sheet.
277
+ Option labels are scanned quickly while the list is open and then read back as
278
+ the chosen value once it's closed so they have to work in both places. And
279
+ because the `label` prop doubles as the empty-state hint, it has to work as a
280
+ field name and as an invitation to pick.
257
281
 
282
+ #### Sentence case
258
283
 
259
- ## Content
284
+ Option labels and group labels are sentence-cased. Capitalize only the first
285
+ letter unless there is a proper noun (a person's name, a brand). Jobber features
286
+ like jobs, quotes, and invoices are not proper nouns.
260
287
 
261
- ### Options and labels
288
+ | Do | ❌ Don't |
289
+ | ----------------- | ----------------- |
290
+ | Credit/debit card | Credit/Debit Card |
291
+ | Bank transfer | BANK TRANSFER |
292
+ | Authorize.net | authorize.net |
293
+ | Jasmine Williams | jasmine williams |
262
294
 
263
- Declare each option with `Select.Item`. The `value` prop is what `onValueChange`
264
- reports and what is submitted with a form; the children are the human-readable
265
- label shown in the open list.
295
+ #### Noun-first labels
266
296
 
267
- ### The selected value display
297
+ Select options represent things the user is picking, not actions they're taking.
298
+ Lead with the noun. If a label reads like an imperative verb phrase, reach for
299
+ [Menu](/components/Menu) or a set of buttons instead.
268
300
 
269
- The closed trigger shows the selected **value**, capitalized by default (e.g.
270
- `"active"` `"Active"`). It does not read the option's children — so when the
271
- label differs from the capitalized value, pass `renderValue` to control what the
272
- trigger shows:
301
+ | Do | Don't |
302
+ | ------- | -------------------- |
303
+ | Cash | Paid by cash |
304
+ | Draft | Save as draft |
305
+ | Overdue | Show overdue only |
306
+ | Active | Set status to active |
307
+
308
+ #### Keep labels short and parallel
309
+
310
+ Aim for 1–3 words. Every item in the same list should follow the same
311
+ grammatical shape.
273
312
 
274
- ```tsx
275
- <Select
276
- label="Status"
277
- value={value}
278
- onValueChange={setValue}
279
- renderValue={value => STATUS_LABELS[value]}
280
- >
281
- {/* Select.Item options */}
282
- </Select>
283
- ```
313
+ | ✅ Do | ❌ Don't |
314
+ | ---------------------------------- | ---------------------------------------------------- |
315
+ | Small / Medium / Large | Small / 12px / Large |
316
+ | Draft / Sent / Paid | Draft / Sent to client / Payment received |
317
+ | Bank transfer / Credit card / Cash | Bank transfer / Charge to credit card / Cash on hand |
318
+
319
+ #### The `label` prop names the field and is the empty state
320
+
321
+ The `label` is both the field heading and, before a value is picked, the
322
+ placeholder inside the trigger. Use a noun or short noun phrase (1–3 words).
323
+ Don't include the word "Select" or "Choose"; the field itself is the invitation.
324
+
325
+ | ✅ Do | ❌ Don't |
326
+ | -------------- | --------------------------- |
327
+ | Payment method | Select a payment method |
328
+ | Status | Choose a status |
329
+ | Team member | Please assign a team member |
330
+
331
+ #### Don't repeat the label in every option
332
+
333
+ The `label` already names the category. Repeating it in each option adds noise
334
+ and eats horizontal space in the trigger. With a `label` of "Payment method":
335
+
336
+ | ✅ Do | ❌ Don't |
337
+ | ---------- | --------------------- |
338
+ | Cash | Cash payment method |
339
+ | Cheque | Cheque payment method |
340
+ | E-transfer | Payment by e-transfer |
341
+
342
+ #### Group labels describe, don't instruct
343
+
344
+ Keep group labels to 1–2 words that name the category. Don't phrase them as
345
+ prompts to the user.
346
+
347
+ | ✅ Do | ❌ Don't |
348
+ | --------------- | ------------------------- |
349
+ | Jobber Payments | Payments through Jobber |
350
+ | Manual entry | Record a payment manually |
351
+ | Active | Currently active statuses |
352
+
353
+ #### Use `renderValue` when the trigger should differ from the item
354
+
355
+ By default the trigger shows the selected `value` capitalized. Pass
356
+ `renderValue` when the item label carries detail the trigger doesn't need, when
357
+ the underlying value is a raw enum, or when items include an icon or adornment.
358
+ The item shows what the user is choosing between; the trigger
359
+ shows what they chose.
360
+
361
+ | Scenario | Item label | `renderValue` returns |
362
+ | ------------------------------- | ----------------------------- | --------------------- |
363
+ | Item has qualifying detail | `High — needs response today` | `High` |
364
+ | Item includes an icon adornment | `🔒 Private` | `Private` |
365
+
366
+ ## Do's and Don'ts
367
+
368
+ #### Do:
369
+
370
+ * ✅ Use sentence case for option and group labels
371
+ * ✅ Pass `renderValue` whenever the item label differs from the capitalized
372
+ `value`
373
+ * ✅ Wire `value`, `onValueChange`, `onBlur`, and `ref` explicitly when using a
374
+ form library
375
+ * ✅ Group options with `Select.Group` + `Select.GroupLabel` when they fall into
376
+ two or more categories
377
+ * ✅ Reach for [Autocomplete](/components/Autocomplete) instead when the list is
378
+ long or benefits from typeahead
379
+
380
+ #### Don't:
284
381
 
285
- ### Grouping
382
+ * ❌ Don't spread a React Hook Form `field` object into Select — `onValueChange`
383
+ won't wire from `field.onChange` and the field will silently stop updating
384
+ * ❌ Don't include "Select" or "Choose" in the `label` — the field itself is the
385
+ invitation
386
+ * ❌ Don't mix noun options and verb options in the same list; if the items are
387
+ actions, use [Menu](/components/Menu) instead
388
+ * ❌ Don't repeat the field's label as a prefix in every option
389
+ * ❌ Don't assume the trigger shows the item's children — it shows the `value`,
390
+ capitalized, unless you pass `renderValue`
391
+
392
+ ## Accessibility
393
+
394
+ Select is built on Base UI's `Field` and `Select` primitives, which handle label
395
+ wiring, ARIA state, and the interaction model.
396
+
397
+ #### Keyboard navigation
398
+
399
+ | Key | Behaviour |
400
+ | ------------------ | ------------------------------------------------------------------- |
401
+ | Tab | Moves focus to the trigger |
402
+ | Enter or Space | Opens the dropdown / bottom sheet |
403
+ | Up and Down arrows | Move through the list. Wrap at the ends |
404
+ | Home / End | Jump to the first or last option |
405
+ | Enter | Selects the highlighted option and closes |
406
+ | Esc | Closes without selecting |
407
+ | Type-to-select | Typing letters jumps to the next option starting with those letters |
408
+
409
+ #### Screen readers
410
+
411
+ The `label` is wired to the trigger via `Field.Label`, so screen readers
412
+ announce the field's purpose along with the selected value. When `error` or
413
+ `invalid` is set, the field is announced as invalid; the `error` message is
414
+ associated with the field so it is read after the label.
415
+
416
+ #### Focus management
417
+
418
+ Select exposes an imperative `ref.focus()` handle that moves focus to the
419
+ trigger. Use it to implement focus-on-error in a form — for example, focusing
420
+ the first invalid Select when the SP tries to submit.
421
+
422
+ #### Touch targets
423
+
424
+ On small screens (≤490px) the options open as a bottom sheet, so each item gets
425
+ the vertical space needed to hit standard touch-target sizing. No per-item
426
+ configuration required.
427
+
428
+ ## Related components
429
+
430
+ * Use [LegacySelect](/components/LegacySelect) if you are working in an app that
431
+ has not yet migrated off the previous native `<select>`-based implementation.
432
+ * Use [Autocomplete](/components/Autocomplete) when the list is long, needs
433
+ typeahead search, or when the user might type a value that isn't in the list.
434
+ * Use [Menu](/components/Menu) when the choice triggers an action rather than
435
+ picking a value that persists in the form.
436
+ * Use [RadioGroup](/components/RadioGroup) when the set of options is small
437
+ (roughly five or fewer) and it helps the SP to see all of them at once.
286
438
 
287
- * `Select.Group`: wraps a set of related options.
288
- * `Select.GroupLabel`: the section header text for a group.
289
- * `Select.Separator`: an optional visual divider. Adjacent `Select.Group`s are
290
- already divided automatically, so reach for this only to divide options
291
- elsewhere in the list.
292
439
 
293
440
  ## Component customization
294
441
 
@@ -298,16 +445,7 @@ than the full underlying API. The subcomponents (`Select.Item`, `Select.Group`,
298
445
  per-option styling; reach out to UXF if you need behaviour beyond what the props
299
446
  provide.
300
447
 
301
- ### Controlled usage
302
-
303
- Select is controlled only. Always pass `value` and `onValueChange`; there is no
304
- uncontrolled `defaultValue`. The `id` and `name` are generated for you and wired
305
- to the label automatically.
306
-
307
- The change handler is named `onValueChange` to stay isomorphic with Base UI, and
308
- to encode the payload (the value, not an event) in its name.
309
-
310
- ### Using with a form library
448
+ ## Using with a form library
311
449
 
312
450
  Select is form-library-agnostic — it exposes plain controlled props, so any
313
451
  system (React Hook Form, Formik, TanStack Form, or plain `useState`) drives it
@@ -364,11 +502,6 @@ const toValueField = ({ onChange, ...field }) => ({
364
502
  > prop and `Form`-driven focus-on-error do not reach Select. Drive validation
365
503
  > through the `error` / `invalid` props for now.
366
504
 
367
- ## Related components
368
-
369
- * [LegacySelect](/components/LegacySelect): the previous native `<select>`-based
370
- implementation.
371
-
372
505
 
373
506
  ## Props
374
507
 
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components-native",
3
- "version": "0.113.3",
3
+ "version": "0.113.4",
4
4
  "license": "MIT",
5
5
  "description": "React Native implementation of Atlantis",
6
6
  "repository": {
@@ -124,5 +124,5 @@
124
124
  "react-native-screens": ">=4.18.0",
125
125
  "react-native-svg": ">=12.0.0"
126
126
  },
127
- "gitHead": "895ad886700e1a7e11927702a0e5f49cb554c4e3"
127
+ "gitHead": "b151fce2d669e8b85cea91becf7d9aacee827c3e"
128
128
  }
@@ -58,7 +58,7 @@ function getIconColorVariation(variation, type, disabled) {
58
58
  return "disabled";
59
59
  }
60
60
  if (type === "primary" && variation !== "cancel") {
61
- return "white";
61
+ return "surface"; // This should be a text color token instead, but the action label also uses this
62
62
  }
63
63
  switch (variation) {
64
64
  case "learning":