@jobber/components 8.25.1 → 8.25.2

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.
@@ -901,8 +901,9 @@ function useInputNumberExperimentalContext(consumer) {
901
901
  }
902
902
  return context;
903
903
  }
904
- function InputNumberExperimentalWrapperInternal(props, ref) {
905
- const { align, autoComplete, children, className, disabled, format, id: idProp, inline, inputMode, invalid, loading, max, maxLength, min, name, onBlur, onEnter, onFocus, onKeyDown, onKeyUp, onValueChange, onValueCommitted, readOnly, showMiniLabel = true, size = "default", step = 1, style, value, } = props;
904
+ function InputNumberExperimentalWrapper(_a) {
905
+ var { ref } = _a, props = tslib_es6.__rest(_a, ["ref"]);
906
+ const { align, autoComplete, children, className, disabled, format, id: idProp, inline, inputMode, invalid, loading, max, min, name, onBlur, onEnter, onFocus, onKeyDown, onKeyUp, onValueChange, onValueCommitted, readOnly, showMiniLabel = true, size = "default", step = 1, style, value, } = props;
906
907
  const generatedId = React.useId();
907
908
  const id = idProp !== null && idProp !== void 0 ? idProp : generatedId;
908
909
  const innerInputRef = React.useRef(null);
@@ -938,7 +939,6 @@ function InputNumberExperimentalWrapperInternal(props, ref) {
938
939
  inputMode,
939
940
  loading,
940
941
  max,
941
- maxLength,
942
942
  min,
943
943
  onBlur,
944
944
  onFocus,
@@ -961,7 +961,6 @@ function InputNumberExperimentalWrapperInternal(props, ref) {
961
961
  inputMode,
962
962
  loading,
963
963
  max,
964
- maxLength,
965
964
  min,
966
965
  onBlur,
967
966
  onFocus,
@@ -999,7 +998,7 @@ function InputNumberExperimentalInput({ children, className, style, }) {
999
998
  className: classnames(styles.inputWrapper, !ctx.showMiniLabel && styles.hideLabel, ctx.size !== "default" && styles[ctx.size], ctx.disabled && styles.disabled),
1000
999
  }, { className, style });
1001
1000
  return (React.createElement("div", Object.assign({}, rootProps),
1002
- React.createElement(NumberFieldInput.NumberFieldInput, { "aria-busy": ctx.loading || undefined, autoComplete: resolveAutoComplete(ctx.autoComplete), className: styles.input, inputMode: ctx.inputMode, maxLength: ctx.maxLength, onBlur: ctx.onBlur, onFocus: ctx.onFocus, onKeyDown: ctx.onKeyDown, onKeyUp: ctx.onKeyUp, placeholder: " ", ref: ctx.innerInputRef }),
1001
+ React.createElement(NumberFieldInput.NumberFieldInput, { "aria-busy": ctx.loading || undefined, autoComplete: resolveAutoComplete(ctx.autoComplete), className: styles.input, inputMode: ctx.inputMode, onBlur: ctx.onBlur, onFocus: ctx.onFocus, onKeyDown: ctx.onKeyDown, onKeyUp: ctx.onKeyUp, placeholder: " ", ref: ctx.innerInputRef }),
1003
1002
  children));
1004
1003
  }
1005
1004
  function InputNumberExperimentalLabel({ children, className, style, }) {
@@ -1099,10 +1098,11 @@ function resolveAutoComplete(autoComplete) {
1099
1098
  * consumer would write by hand with `<InputNumberExperimental.Wrapper>` and the
1100
1099
  * parts — to customize a single piece, copy this tree and swap that part.
1101
1100
  */
1102
- function InputNumberExperimentalInternal(props, ref) {
1103
- var _a;
1101
+ function InputNumberExperimental(_a) {
1102
+ var _b;
1103
+ var { ref } = _a, props = tslib_es6.__rest(_a, ["ref"]);
1104
1104
  const { label, description, error, prefix, suffix } = props, config = tslib_es6.__rest(props, ["label", "description", "error", "prefix", "suffix"]);
1105
- const size = (_a = config.size) !== null && _a !== void 0 ? _a : "default";
1105
+ const size = (_b = config.size) !== null && _b !== void 0 ? _b : "default";
1106
1106
  return (React.createElement(InputNumberExperimentalWrapper, Object.assign({}, config, { invalid: config.invalid || Boolean(error), ref: ref }),
1107
1107
  React.createElement(InputNumberExperimentalGroup, null,
1108
1108
  prefix && renderDefaultAffix("prefix", prefix, size),
@@ -1114,10 +1114,8 @@ function InputNumberExperimentalInternal(props, ref) {
1114
1114
  React.createElement(InputNumberExperimentalDescription, null, description),
1115
1115
  React.createElement(InputNumberExperimentalError, null, error)))));
1116
1116
  }
1117
- const InputNumberExperimentalWrapper = React.forwardRef(InputNumberExperimentalWrapperInternal);
1118
1117
  InputNumberExperimentalWrapper.displayName = "InputNumberExperimental.Wrapper";
1119
- const InputNumberExperimentalBase = React.forwardRef(InputNumberExperimentalInternal);
1120
- InputNumberExperimentalBase.displayName = "InputNumberExperimental";
1118
+ InputNumberExperimental.displayName = "InputNumberExperimental";
1121
1119
  InputNumberExperimentalGroup.displayName = "InputNumberExperimental.Group";
1122
1120
  InputNumberExperimentalFooter.displayName = "InputNumberExperimental.Footer";
1123
1121
  InputNumberExperimentalInput.displayName = "InputNumberExperimental.Input";
@@ -1133,19 +1131,17 @@ InputNumberExperimentalDecrement.displayName =
1133
1131
  "InputNumberExperimental.Decrement";
1134
1132
  InputNumberExperimentalAffixCompound.displayName =
1135
1133
  "InputNumberExperimental.Affix";
1136
- const InputNumberExperimental = Object.assign(InputNumberExperimentalBase, {
1137
- Wrapper: InputNumberExperimentalWrapper,
1138
- Group: InputNumberExperimentalGroup,
1139
- Footer: InputNumberExperimentalFooter,
1140
- Input: InputNumberExperimentalInput,
1141
- Label: InputNumberExperimentalLabel,
1142
- Description: InputNumberExperimentalDescription,
1143
- Error: InputNumberExperimentalError,
1144
- Loading: InputNumberExperimentalLoading,
1145
- Stepper: InputNumberExperimentalStepper,
1146
- Increment: InputNumberExperimentalIncrement,
1147
- Decrement: InputNumberExperimentalDecrement,
1148
- Affix: InputNumberExperimentalAffixCompound,
1149
- });
1134
+ InputNumberExperimental.Wrapper = InputNumberExperimentalWrapper;
1135
+ InputNumberExperimental.Group = InputNumberExperimentalGroup;
1136
+ InputNumberExperimental.Footer = InputNumberExperimentalFooter;
1137
+ InputNumberExperimental.Input = InputNumberExperimentalInput;
1138
+ InputNumberExperimental.Label = InputNumberExperimentalLabel;
1139
+ InputNumberExperimental.Description = InputNumberExperimentalDescription;
1140
+ InputNumberExperimental.Error = InputNumberExperimentalError;
1141
+ InputNumberExperimental.Loading = InputNumberExperimentalLoading;
1142
+ InputNumberExperimental.Stepper = InputNumberExperimentalStepper;
1143
+ InputNumberExperimental.Increment = InputNumberExperimentalIncrement;
1144
+ InputNumberExperimental.Decrement = InputNumberExperimentalDecrement;
1145
+ InputNumberExperimental.Affix = InputNumberExperimentalAffixCompound;
1150
1146
 
1151
1147
  exports.InputNumberExperimental = InputNumberExperimental;
@@ -1,6 +1,6 @@
1
1
  import { _ as __rest } from './tslib.es6-es.js';
2
2
  import * as React from 'react';
3
- import React__default, { createContext, forwardRef, useId, useRef, useImperativeHandle, useCallback, useMemo, useContext } from 'react';
3
+ import React__default, { createContext, useId, useRef, useImperativeHandle, useCallback, useMemo, useContext } from 'react';
4
4
  import classnames from 'classnames';
5
5
  import { A as ActivityIndicator } from './ActivityIndicator-es.js';
6
6
  import { B as Button } from './Button-es.js';
@@ -881,8 +881,9 @@ function useInputNumberExperimentalContext(consumer) {
881
881
  }
882
882
  return context;
883
883
  }
884
- function InputNumberExperimentalWrapperInternal(props, ref) {
885
- const { align, autoComplete, children, className, disabled, format, id: idProp, inline, inputMode, invalid, loading, max, maxLength, min, name, onBlur, onEnter, onFocus, onKeyDown, onKeyUp, onValueChange, onValueCommitted, readOnly, showMiniLabel = true, size = "default", step = 1, style, value, } = props;
884
+ function InputNumberExperimentalWrapper(_a) {
885
+ var { ref } = _a, props = __rest(_a, ["ref"]);
886
+ const { align, autoComplete, children, className, disabled, format, id: idProp, inline, inputMode, invalid, loading, max, min, name, onBlur, onEnter, onFocus, onKeyDown, onKeyUp, onValueChange, onValueCommitted, readOnly, showMiniLabel = true, size = "default", step = 1, style, value, } = props;
886
887
  const generatedId = useId();
887
888
  const id = idProp !== null && idProp !== void 0 ? idProp : generatedId;
888
889
  const innerInputRef = useRef(null);
@@ -918,7 +919,6 @@ function InputNumberExperimentalWrapperInternal(props, ref) {
918
919
  inputMode,
919
920
  loading,
920
921
  max,
921
- maxLength,
922
922
  min,
923
923
  onBlur,
924
924
  onFocus,
@@ -941,7 +941,6 @@ function InputNumberExperimentalWrapperInternal(props, ref) {
941
941
  inputMode,
942
942
  loading,
943
943
  max,
944
- maxLength,
945
944
  min,
946
945
  onBlur,
947
946
  onFocus,
@@ -979,7 +978,7 @@ function InputNumberExperimentalInput({ children, className, style, }) {
979
978
  className: classnames(styles.inputWrapper, !ctx.showMiniLabel && styles.hideLabel, ctx.size !== "default" && styles[ctx.size], ctx.disabled && styles.disabled),
980
979
  }, { className, style });
981
980
  return (React__default.createElement("div", Object.assign({}, rootProps),
982
- React__default.createElement(NumberFieldInput, { "aria-busy": ctx.loading || undefined, autoComplete: resolveAutoComplete(ctx.autoComplete), className: styles.input, inputMode: ctx.inputMode, maxLength: ctx.maxLength, onBlur: ctx.onBlur, onFocus: ctx.onFocus, onKeyDown: ctx.onKeyDown, onKeyUp: ctx.onKeyUp, placeholder: " ", ref: ctx.innerInputRef }),
981
+ React__default.createElement(NumberFieldInput, { "aria-busy": ctx.loading || undefined, autoComplete: resolveAutoComplete(ctx.autoComplete), className: styles.input, inputMode: ctx.inputMode, onBlur: ctx.onBlur, onFocus: ctx.onFocus, onKeyDown: ctx.onKeyDown, onKeyUp: ctx.onKeyUp, placeholder: " ", ref: ctx.innerInputRef }),
983
982
  children));
984
983
  }
985
984
  function InputNumberExperimentalLabel({ children, className, style, }) {
@@ -1079,10 +1078,11 @@ function resolveAutoComplete(autoComplete) {
1079
1078
  * consumer would write by hand with `<InputNumberExperimental.Wrapper>` and the
1080
1079
  * parts — to customize a single piece, copy this tree and swap that part.
1081
1080
  */
1082
- function InputNumberExperimentalInternal(props, ref) {
1083
- var _a;
1081
+ function InputNumberExperimental(_a) {
1082
+ var _b;
1083
+ var { ref } = _a, props = __rest(_a, ["ref"]);
1084
1084
  const { label, description, error, prefix, suffix } = props, config = __rest(props, ["label", "description", "error", "prefix", "suffix"]);
1085
- const size = (_a = config.size) !== null && _a !== void 0 ? _a : "default";
1085
+ const size = (_b = config.size) !== null && _b !== void 0 ? _b : "default";
1086
1086
  return (React__default.createElement(InputNumberExperimentalWrapper, Object.assign({}, config, { invalid: config.invalid || Boolean(error), ref: ref }),
1087
1087
  React__default.createElement(InputNumberExperimentalGroup, null,
1088
1088
  prefix && renderDefaultAffix("prefix", prefix, size),
@@ -1094,10 +1094,8 @@ function InputNumberExperimentalInternal(props, ref) {
1094
1094
  React__default.createElement(InputNumberExperimentalDescription, null, description),
1095
1095
  React__default.createElement(InputNumberExperimentalError, null, error)))));
1096
1096
  }
1097
- const InputNumberExperimentalWrapper = forwardRef(InputNumberExperimentalWrapperInternal);
1098
1097
  InputNumberExperimentalWrapper.displayName = "InputNumberExperimental.Wrapper";
1099
- const InputNumberExperimentalBase = forwardRef(InputNumberExperimentalInternal);
1100
- InputNumberExperimentalBase.displayName = "InputNumberExperimental";
1098
+ InputNumberExperimental.displayName = "InputNumberExperimental";
1101
1099
  InputNumberExperimentalGroup.displayName = "InputNumberExperimental.Group";
1102
1100
  InputNumberExperimentalFooter.displayName = "InputNumberExperimental.Footer";
1103
1101
  InputNumberExperimentalInput.displayName = "InputNumberExperimental.Input";
@@ -1113,19 +1111,17 @@ InputNumberExperimentalDecrement.displayName =
1113
1111
  "InputNumberExperimental.Decrement";
1114
1112
  InputNumberExperimentalAffixCompound.displayName =
1115
1113
  "InputNumberExperimental.Affix";
1116
- const InputNumberExperimental = Object.assign(InputNumberExperimentalBase, {
1117
- Wrapper: InputNumberExperimentalWrapper,
1118
- Group: InputNumberExperimentalGroup,
1119
- Footer: InputNumberExperimentalFooter,
1120
- Input: InputNumberExperimentalInput,
1121
- Label: InputNumberExperimentalLabel,
1122
- Description: InputNumberExperimentalDescription,
1123
- Error: InputNumberExperimentalError,
1124
- Loading: InputNumberExperimentalLoading,
1125
- Stepper: InputNumberExperimentalStepper,
1126
- Increment: InputNumberExperimentalIncrement,
1127
- Decrement: InputNumberExperimentalDecrement,
1128
- Affix: InputNumberExperimentalAffixCompound,
1129
- });
1114
+ InputNumberExperimental.Wrapper = InputNumberExperimentalWrapper;
1115
+ InputNumberExperimental.Group = InputNumberExperimentalGroup;
1116
+ InputNumberExperimental.Footer = InputNumberExperimentalFooter;
1117
+ InputNumberExperimental.Input = InputNumberExperimentalInput;
1118
+ InputNumberExperimental.Label = InputNumberExperimentalLabel;
1119
+ InputNumberExperimental.Description = InputNumberExperimentalDescription;
1120
+ InputNumberExperimental.Error = InputNumberExperimentalError;
1121
+ InputNumberExperimental.Loading = InputNumberExperimentalLoading;
1122
+ InputNumberExperimental.Stepper = InputNumberExperimentalStepper;
1123
+ InputNumberExperimental.Increment = InputNumberExperimentalIncrement;
1124
+ InputNumberExperimental.Decrement = InputNumberExperimentalDecrement;
1125
+ InputNumberExperimental.Affix = InputNumberExperimentalAffixCompound;
1130
1126
 
1131
1127
  export { InputNumberExperimental as I };
@@ -0,0 +1,519 @@
1
+ # InputNumberExperimental
2
+
3
+ ## Summary
4
+
5
+ `InputNumberExperimental` collects a single numeric value in a form. Reach for
6
+ it when the value benefits from being nudged up or down — quantities, prices,
7
+ durations, or counts.
8
+
9
+ Most fields need only the prop-driven usage below. For the rare layout the props
10
+ can't express, the same field can be composed from parts — see the **Implement**
11
+ tab.
12
+
13
+ ### When to use
14
+
15
+ * The value is a number the user increments or decrements (quantity, price,
16
+ days, repetitions).
17
+ * A stepper, min/max bounds, or number formatting would help the user.
18
+
19
+ ### When not to use
20
+
21
+ * The value is a sequence of digits that is never calculated with — phone
22
+ numbers, credit-card numbers, or postal codes. A stepper adds no value there;
23
+ use [InputText](../InputText/InputText.md) instead.
24
+
25
+ ## Basic usage
26
+
27
+ Pass `label`, a controlled `value`, and `onValueCommitted`. Bounds (`min` /
28
+ `max`) and `step` are optional.
29
+
30
+ ```tsx
31
+ import React, { useState } from "react";
32
+ import type { InputNumberExperimentalProps } from "@jobber/components/primitives";
33
+ import { InputNumberExperimental } from "@jobber/components/primitives";
34
+
35
+ export function InputNumberExperimentalBasicExample(
36
+ props: Partial<InputNumberExperimentalProps>,
37
+ ) {
38
+ const [value, setValue] = useState<number | null>(3);
39
+
40
+ return (
41
+ <InputNumberExperimental
42
+ label="Quantity"
43
+ min={0}
44
+ max={100}
45
+ {...props}
46
+ value={value}
47
+ onValueCommitted={setValue}
48
+ />
49
+ );
50
+ }
51
+ ```
52
+
53
+ ## Options
54
+
55
+ ### Prefixes and suffixes
56
+
57
+ Use `prefix` / `suffix` to add a unit or symbol. A suffix can be a label, an
58
+ icon, or a clickable icon that runs an action (give it an `ariaLabel`).
59
+
60
+ ```tsx
61
+ import React, { useState } from "react";
62
+ import { InputNumberExperimental } from "@jobber/components/primitives";
63
+ import { Content } from "@jobber/components/Content";
64
+
65
+ export function InputNumberExperimentalAffixesExample() {
66
+ const [price, setPrice] = useState<number | null>(42);
67
+ const [days, setDays] = useState<number | null>(7);
68
+ const [reps, setReps] = useState<number | null>(3);
69
+
70
+ return (
71
+ <Content>
72
+ <InputNumberExperimental
73
+ label="Price"
74
+ prefix={{ label: "$" }}
75
+ suffix={{ label: "USD" }}
76
+ value={price}
77
+ onValueCommitted={setPrice}
78
+ />
79
+
80
+ <InputNumberExperimental
81
+ label="Follow-up in"
82
+ suffix={{ icon: "calendar", label: "days" }}
83
+ value={days}
84
+ onValueCommitted={setDays}
85
+ />
86
+
87
+ <InputNumberExperimental
88
+ label="Repetitions"
89
+ suffix={{
90
+ icon: "remove",
91
+ ariaLabel: "Clear value",
92
+ onClick: () => setReps(null),
93
+ }}
94
+ value={reps}
95
+ onValueCommitted={setReps}
96
+ />
97
+ </Content>
98
+ );
99
+ }
100
+ ```
101
+
102
+ ### Sizes
103
+
104
+ Three sizes are available. `default` fits almost every form; use `small` only in
105
+ tight spaces and `large` only in especially spacious layouts.
106
+
107
+ ```tsx
108
+ import React, { useState } from "react";
109
+ import { InputNumberExperimental } from "@jobber/components/primitives";
110
+ import { Content } from "@jobber/components/Content";
111
+
112
+ export function InputNumberExperimentalSizesExample() {
113
+ const [small, setSmall] = useState<number | null>(42);
114
+ const [base, setBase] = useState<number | null>(42);
115
+ const [large, setLarge] = useState<number | null>(42);
116
+
117
+ return (
118
+ <Content>
119
+ <InputNumberExperimental
120
+ label="Small"
121
+ size="small"
122
+ suffix={{ label: "items" }}
123
+ value={small}
124
+ onValueCommitted={setSmall}
125
+ />
126
+ <InputNumberExperimental
127
+ label="Default"
128
+ size="default"
129
+ suffix={{ label: "items" }}
130
+ value={base}
131
+ onValueCommitted={setBase}
132
+ />
133
+ <InputNumberExperimental
134
+ label="Large"
135
+ size="large"
136
+ suffix={{ label: "items" }}
137
+ value={large}
138
+ onValueCommitted={setLarge}
139
+ />
140
+ </Content>
141
+ );
142
+ }
143
+ ```
144
+
145
+ ### Formatting
146
+
147
+ `format` takes any `Intl.NumberFormatOptions` and controls only how the value is
148
+ displayed; the committed value stays a plain number. See the **Implement** tab
149
+ for how percent and currency values map to the underlying number.
150
+
151
+ ```tsx
152
+ import React, { useState } from "react";
153
+ import { InputNumberExperimental } from "@jobber/components/primitives";
154
+ import { Content } from "@jobber/components/Content";
155
+
156
+ export function InputNumberExperimentalFormattingExample() {
157
+ const [currency, setCurrency] = useState<number | null>(1234.5);
158
+ const [percent, setPercent] = useState<number | null>(0.5);
159
+ const [decimal, setDecimal] = useState<number | null>(11.13);
160
+
161
+ return (
162
+ <Content>
163
+ <InputNumberExperimental
164
+ label="Currency"
165
+ description='{ style: "currency", currency: "USD" }'
166
+ format={{ style: "currency", currency: "USD" }}
167
+ value={currency}
168
+ onValueCommitted={setCurrency}
169
+ />
170
+ <InputNumberExperimental
171
+ label="Percent"
172
+ description='{ style: "percent" } — value is a ratio: 0.5 → 50%'
173
+ format={{ style: "percent", maximumFractionDigits: 2 }}
174
+ value={percent}
175
+ onValueCommitted={setPercent}
176
+ />
177
+ <InputNumberExperimental
178
+ label="Decimal"
179
+ description="{ maximumFractionDigits: 2 }"
180
+ format={{ maximumFractionDigits: 2 }}
181
+ value={decimal}
182
+ onValueCommitted={setDecimal}
183
+ />
184
+ </Content>
185
+ );
186
+ }
187
+ ```
188
+
189
+ ### Loading
190
+
191
+ `loading` shows a non-blocking indicator in the stepper's slot for background
192
+ work (for example, saving). The field stays editable and the stepper is hidden
193
+ while loading.
194
+
195
+ ```tsx
196
+ import React, { useState } from "react";
197
+ import { InputNumberExperimental } from "@jobber/components/primitives";
198
+
199
+ export function InputNumberExperimentalLoadingExample() {
200
+ const [value, setValue] = useState<number | null>(42);
201
+
202
+ return (
203
+ <InputNumberExperimental
204
+ loading
205
+ label="Quantity"
206
+ suffix={{ label: "items" }}
207
+ value={value}
208
+ onValueCommitted={setValue}
209
+ />
210
+ );
211
+ }
212
+ ```
213
+
214
+ ## Content guidelines
215
+
216
+ ### Label the unit, don't repeat it
217
+
218
+ Put the unit in the label or an affix, not both.
219
+
220
+ | ✅ Do | ❌ Don't |
221
+ | ------------------------------- | --------------------------------------- |
222
+ | Label "Weight", suffix "kg" | Label "Weight (kg)", suffix "kg" |
223
+ | Label "Duration", suffix "days" | Label "Duration in days", suffix "days" |
224
+
225
+ ### Keep labels short and sentence-cased
226
+
227
+ | ✅ Do | ❌ Don't |
228
+ | -------- | ----------------------- |
229
+ | Quantity | Enter the quantity here |
230
+ | Discount | DISCOUNT % |
231
+
232
+ ## Do's and Don'ts
233
+
234
+ * ✅ Use for values the user increments or decrements
235
+ * ✅ Set `min` / `max` when the value has real bounds
236
+ * ✅ Use `format` for currency, percent, and decimals rather than formatting the
237
+ value yourself
238
+ * ❌ Don't use it for digit sequences that are never calculated with (phone,
239
+ credit card)
240
+ * ❌ Don't disable the field to communicate an error — show an `error` message
241
+ instead
242
+
243
+ ## Accessibility
244
+
245
+ The field is a native number input, so it is reachable and operable by keyboard
246
+ and assistive technology.
247
+
248
+ | Key | Behavior |
249
+ | ---------------- | ------------------------------- |
250
+ | Tab | Moves focus to the field |
251
+ | Up / Down arrows | Increment / decrement by `step` |
252
+ | Enter | Commits the current value |
253
+ | Type | Replaces the value |
254
+
255
+ Give a clickable affix a clear `ariaLabel` describing its action (for example,
256
+ "Clear value").
257
+
258
+ ## Related components
259
+
260
+ * For digit sequences that are not calculated with (phone, credit card), use
261
+ [InputText](../InputText/InputText.md).
262
+ * For dates, use [InputDate](../InputDate/InputDate.md).
263
+
264
+
265
+ ## Anatomy
266
+
267
+ The prop-driven `<InputNumberExperimental>` composes a set of parts. You only
268
+ need these when the props can't express a layout; otherwise reach for the props
269
+ shown on the **Design** tab.
270
+
271
+ | Part | Description |
272
+ | ----------------------- | -------------------------------------------------------------------- |
273
+ | `Wrapper` | Owns the field configuration and state; provides it to the parts |
274
+ | `Group` | The bordered field row |
275
+ | `Input` | The input area; holds the `Label` and the `Stepper` / `Loading` slot |
276
+ | `Label` | Floating field label |
277
+ | `Stepper` | The increment / decrement button pair |
278
+ | `Increment` `Decrement` | The individual stepper buttons |
279
+ | `Affix` | Prefix / suffix content (label, icon, or clickable icon) |
280
+ | `Loading` | Non-blocking loading indicator slot |
281
+ | `Footer` | Below-field row that holds `Description` and `Error` |
282
+ | `Description` | Helper text below the field |
283
+ | `Error` | Styled error message below the field |
284
+
285
+ ## Composition
286
+
287
+ The prop-driven component is sugar: it renders exactly the tree you would write
288
+ by hand with `<InputNumberExperimental.Wrapper>` and the parts. To customize a
289
+ single piece, compose the tree yourself and swap that one part — the other parts
290
+ keep their defaults. The sugar does not merge consumer-provided parts into its
291
+ render, so there is no per-slot precedence to reason about.
292
+
293
+ `Wrapper` owns the field state and shares it with the parts through context, so
294
+ every part must be rendered inside a `Wrapper` (a part used outside one throws).
295
+
296
+ The example below replaces the default stepper icons with `+` / `−` and leaves
297
+ everything else as the default:
298
+
299
+ ```tsx
300
+ import React, { useState } from "react";
301
+ import { InputNumberExperimental } from "@jobber/components/primitives";
302
+
303
+ export function InputNumberExperimentalCompositionExample() {
304
+ const [value, setValue] = useState<number | null>(3);
305
+
306
+ return (
307
+ <InputNumberExperimental.Wrapper value={value} onValueCommitted={setValue}>
308
+ <InputNumberExperimental.Group>
309
+ <InputNumberExperimental.Input>
310
+ <InputNumberExperimental.Label>
311
+ Quantity
312
+ </InputNumberExperimental.Label>
313
+ <InputNumberExperimental.Stepper>
314
+ <InputNumberExperimental.Increment ariaLabel="Increase Quantity">
315
+ +
316
+ </InputNumberExperimental.Increment>
317
+ <InputNumberExperimental.Decrement ariaLabel="Decrease Quantity">
318
+
319
+ </InputNumberExperimental.Decrement>
320
+ </InputNumberExperimental.Stepper>
321
+ </InputNumberExperimental.Input>
322
+ </InputNumberExperimental.Group>
323
+ </InputNumberExperimental.Wrapper>
324
+ );
325
+ }
326
+ ```
327
+
328
+ ## Controlled usage
329
+
330
+ The field is controlled: pass `value` (a `number`, or `null` for empty) and read
331
+ changes back through one of two callbacks.
332
+
333
+ | Callback | Fires | Use for |
334
+ | ------------------ | ----------------------------------------------------------- | ---------------------------------- |
335
+ | `onValueChange` | On every parsed change (typing, paste, stepper, arrow step) | Live-updating UI as the user types |
336
+ | `onValueCommitted` | When the user commits (blur, Enter, stepper, arrow step) | Saving / validating a final value |
337
+
338
+ Both emit `null` when the field is empty. Prefer `onValueCommitted` for
339
+ persistence so you are not writing on every keystroke.
340
+
341
+ ## Formatting semantics
342
+
343
+ `format` is forwarded to Base UI's `NumberField` `format` and accepts any
344
+ `Intl.NumberFormatOptions`. It changes the display only; the committed value is
345
+ always a plain number. Two things to know:
346
+
347
+ * **Percent** (`{ style: "percent" }`) treats the value as a ratio: `0.5`
348
+ renders `50%`, and the stepper moves in ratio units. If you want the value to
349
+ be the number itself (`50` → `50%`), use `{ style: "unit", unit: "percent" }`.
350
+ * With no `format`, typed decimals are preserved and grouping is off
351
+ (`useGrouping: false`), so large numbers render without thousands separators
352
+ unless you opt in.
353
+
354
+
355
+ ## Props
356
+
357
+ ### Web
358
+
359
+ #### InputNumberExperimental
360
+
361
+ | Prop | Type | Required | Default | Description |
362
+ |------|------|----------|---------|-------------|
363
+ | `align` | `"center" | "right"` | No | — | |
364
+ | `autoComplete` | `InputNumberExperimentalAutoComplete` | No | — | |
365
+ | `className` | `string` | No | — | |
366
+ | `description` | `ReactNode` | No | — | |
367
+ | `disabled` | `boolean` | No | — | |
368
+ | `error` | `string` | No | — | Renders a styled error message below the field. |
369
+ | `format` | `NumberFormatOptions` | No | — | Number formatting for the displayed value, forwarded to Base UI `NumberField`'s native `format`. When omitted, typed ... |
370
+ | `id` | `string` | No | — | |
371
+ | `inline` | `boolean` | No | — | Shrink-wrap the field to its content (auto width). |
372
+ | `inputMode` | `"decimal" | "numeric"` | No | — | |
373
+ | `invalid` | `boolean` | No | — | Style the error border without showing an error message. |
374
+ | `label` | `string` | No | — | Floating field label. |
375
+ | `loading` | `boolean` | No | — | Shows a non-blocking loading indicator in the stepper's slot. The field stays editable (use `readOnly`/`disabled` to ... |
376
+ | `max` | `number` | No | — | |
377
+ | `min` | `number` | No | — | |
378
+ | `name` | `string` | No | — | |
379
+ | `onBlur` | `(event?: FocusEvent<HTMLInputElement, Element>) => void` | No | — | |
380
+ | `onEnter` | `(event: KeyboardEvent<HTMLInputElement>) => void` | No | — | Fires when Enter is pressed without modifier keys (Shift/Ctrl/Meta). |
381
+ | `onFocus` | `(event?: FocusEvent<HTMLInputElement, Element>) => void` | No | — | |
382
+ | `onKeyDown` | `(event: KeyboardEvent<HTMLInputElement>) => void` | No | — | |
383
+ | `onKeyUp` | `(event: KeyboardEvent<HTMLInputElement>) => void` | No | — | |
384
+ | `onValueChange` | `(newValue: number) => void` | No | — | Fires on every parsed value change (typing, paste, stepper, arrow step). Emits `null` when the field is empty. |
385
+ | `onValueCommitted` | `(newValue: number) => void` | No | — | Fires when the user commits a value (blur, Enter, stepper, arrow step). Emits `null` when committed empty. For per-ke... |
386
+ | `prefix` | `InputNumberExperimentalAffix` | No | — | |
387
+ | `readOnly` | `boolean` | No | — | |
388
+ | `ref` | `Ref<InputNumberExperimentalRef>` | No | — | |
389
+ | `showMiniLabel` | `boolean` | No | — | Default `true`. When `false`, the floating label is hidden. |
390
+ | `size` | `InputNumberExperimentalSize` | No | — | |
391
+ | `step` | `number` | No | — | Amount the stepper buttons and ArrowUp/ArrowDown keys change the value by. Default `1`. |
392
+ | `style` | `CSSProperties` | No | — | |
393
+ | `suffix` | `InputNumberExperimentalSuffixProp` | No | — | |
394
+ | `value` | `number` | No | — | Controlled value. `number` sets the value; `null` (or `undefined`) is an empty field. |
395
+
396
+ #### InputNumberExperimental.Affix
397
+
398
+ | Prop | Type | Required | Default | Description |
399
+ |------|------|----------|---------|-------------|
400
+ | `variation` | `"prefix" | "suffix"` | Yes | — | |
401
+ | `ariaLabel` | `string` | No | — | |
402
+ | `children` | `ReactNode` | No | — | Arbitrary affix content, beyond the built-in `label`/`icon`. |
403
+ | `className` | `string` | No | — | |
404
+ | `icon` | `IconNames` | No | — | |
405
+ | `label` | `string` | No | — | |
406
+ | `onClick` | `() => void` | No | — | |
407
+ | `style` | `CSSProperties` | No | — | |
408
+
409
+ #### InputNumberExperimental.Decrement
410
+
411
+ | Prop | Type | Required | Default | Description |
412
+ |------|------|----------|---------|-------------|
413
+ | `ariaLabel` | `string` | No | — | Accessible label for the button. |
414
+ | `children` | `ReactNode` | No | — | Icon content. Falls back to the default Atlantis stepper icon. |
415
+ | `className` | `string` | No | — | |
416
+ | `style` | `CSSProperties` | No | — | |
417
+
418
+ #### InputNumberExperimental.Description
419
+
420
+ | Prop | Type | Required | Default | Description |
421
+ |------|------|----------|---------|-------------|
422
+ | `className` | `string` | No | — | |
423
+ | `style` | `CSSProperties` | No | — | |
424
+
425
+ #### InputNumberExperimental.Error
426
+
427
+ | Prop | Type | Required | Default | Description |
428
+ |------|------|----------|---------|-------------|
429
+ | `className` | `string` | No | — | |
430
+ | `style` | `CSSProperties` | No | — | |
431
+
432
+ #### InputNumberExperimental.Footer
433
+
434
+ | Prop | Type | Required | Default | Description |
435
+ |------|------|----------|---------|-------------|
436
+ | `className` | `string` | No | — | |
437
+ | `style` | `CSSProperties` | No | — | |
438
+
439
+ #### InputNumberExperimental.Group
440
+
441
+ | Prop | Type | Required | Default | Description |
442
+ |------|------|----------|---------|-------------|
443
+ | `className` | `string` | No | — | |
444
+ | `style` | `CSSProperties` | No | — | |
445
+
446
+ #### InputNumberExperimental.Increment
447
+
448
+ | Prop | Type | Required | Default | Description |
449
+ |------|------|----------|---------|-------------|
450
+ | `ariaLabel` | `string` | No | — | Accessible label for the button. |
451
+ | `children` | `ReactNode` | No | — | Icon content. Falls back to the default Atlantis stepper icon. |
452
+ | `className` | `string` | No | — | |
453
+ | `style` | `CSSProperties` | No | — | |
454
+
455
+ #### InputNumberExperimental.Input
456
+
457
+ | Prop | Type | Required | Default | Description |
458
+ |------|------|----------|---------|-------------|
459
+ | `children` | `ReactNode` | No | — | Content rendered inside the input area (e.g. `.Label`, `.Stepper`). |
460
+ | `className` | `string` | No | — | |
461
+ | `style` | `CSSProperties` | No | — | |
462
+
463
+ #### InputNumberExperimental.Label
464
+
465
+ | Prop | Type | Required | Default | Description |
466
+ |------|------|----------|---------|-------------|
467
+ | `className` | `string` | No | — | |
468
+ | `style` | `CSSProperties` | No | — | |
469
+
470
+ #### InputNumberExperimental.Loading
471
+
472
+ | Prop | Type | Required | Default | Description |
473
+ |------|------|----------|---------|-------------|
474
+ | `children` | `ReactNode` | No | — | Indicator content. Falls back to the default `ActivityIndicator`. |
475
+ | `className` | `string` | No | — | |
476
+ | `style` | `CSSProperties` | No | — | |
477
+
478
+ #### InputNumberExperimental.Stepper
479
+
480
+ | Prop | Type | Required | Default | Description |
481
+ |------|------|----------|---------|-------------|
482
+ | `children` | `ReactNode` | No | — | Stepper buttons. Falls back to the default increment/decrement pair. |
483
+ | `className` | `string` | No | — | |
484
+ | `decrementLabel` | `string` | No | — | Accessible label for the decrement button. Defaults to `Decrease value`. |
485
+ | `incrementLabel` | `string` | No | — | Accessible label for the increment button. Defaults to `Increase value`. |
486
+ | `style` | `CSSProperties` | No | — | |
487
+
488
+ #### InputNumberExperimental.Wrapper
489
+
490
+ | Prop | Type | Required | Default | Description |
491
+ |------|------|----------|---------|-------------|
492
+ | `align` | `"center" | "right"` | No | — | |
493
+ | `autoComplete` | `InputNumberExperimentalAutoComplete` | No | — | |
494
+ | `children` | `ReactNode` | No | — | Composed parts (`.Group`, `.Footer`, and the parts within them). |
495
+ | `className` | `string` | No | — | |
496
+ | `disabled` | `boolean` | No | — | |
497
+ | `format` | `NumberFormatOptions` | No | — | Number formatting for the displayed value, forwarded to Base UI `NumberField`'s native `format`. When omitted, typed ... |
498
+ | `id` | `string` | No | — | |
499
+ | `inline` | `boolean` | No | — | Shrink-wrap the field to its content (auto width). |
500
+ | `inputMode` | `"decimal" | "numeric"` | No | — | |
501
+ | `invalid` | `boolean` | No | — | Style the error border without showing an error message. |
502
+ | `loading` | `boolean` | No | — | Shows a non-blocking loading indicator in the stepper's slot. The field stays editable (use `readOnly`/`disabled` to ... |
503
+ | `max` | `number` | No | — | |
504
+ | `min` | `number` | No | — | |
505
+ | `name` | `string` | No | — | |
506
+ | `onBlur` | `(event?: FocusEvent<HTMLInputElement, Element>) => void` | No | — | |
507
+ | `onEnter` | `(event: KeyboardEvent<HTMLInputElement>) => void` | No | — | Fires when Enter is pressed without modifier keys (Shift/Ctrl/Meta). |
508
+ | `onFocus` | `(event?: FocusEvent<HTMLInputElement, Element>) => void` | No | — | |
509
+ | `onKeyDown` | `(event: KeyboardEvent<HTMLInputElement>) => void` | No | — | |
510
+ | `onKeyUp` | `(event: KeyboardEvent<HTMLInputElement>) => void` | No | — | |
511
+ | `onValueChange` | `(newValue: number) => void` | No | — | Fires on every parsed value change (typing, paste, stepper, arrow step). Emits `null` when the field is empty. |
512
+ | `onValueCommitted` | `(newValue: number) => void` | No | — | Fires when the user commits a value (blur, Enter, stepper, arrow step). Emits `null` when committed empty. For per-ke... |
513
+ | `readOnly` | `boolean` | No | — | |
514
+ | `ref` | `Ref<InputNumberExperimentalRef>` | No | — | |
515
+ | `showMiniLabel` | `boolean` | No | — | Default `true`. When `false`, the floating label is hidden. |
516
+ | `size` | `InputNumberExperimentalSize` | No | — | |
517
+ | `step` | `number` | No | — | Amount the stepper buttons and ArrowUp/ArrowDown keys change the value by. Default `1`. |
518
+ | `style` | `CSSProperties` | No | — | |
519
+ | `value` | `number` | No | — | Controlled value. `number` sets the value; `null` (or `undefined`) is an empty field. |
@@ -55,6 +55,7 @@
55
55
  [InputFile](./InputFile/InputFile.md)
56
56
  [InputGroup](./InputGroup/InputGroup.md)
57
57
  [InputNumber](./InputNumber/InputNumber.md)
58
+ [InputNumberExperimental](./InputNumberExperimental/InputNumberExperimental.md)
58
59
  [InputPhoneNumber](./InputPhoneNumber/InputPhoneNumber.md)
59
60
  [InputText](./InputText/InputText.md)
60
61
  [InputTime](./InputTime/InputTime.md)
@@ -1,5 +1,12 @@
1
+ import type { Ref } from "react";
1
2
  import React from "react";
2
3
  import type { InputNumberExperimentalAffixCompoundProps, InputNumberExperimentalDescriptionProps, InputNumberExperimentalErrorProps, InputNumberExperimentalFooterProps, InputNumberExperimentalGroupProps, InputNumberExperimentalInputProps, InputNumberExperimentalLabelProps, InputNumberExperimentalLoadingProps, InputNumberExperimentalProps, InputNumberExperimentalRef, InputNumberExperimentalStepButtonProps, InputNumberExperimentalStepperProps, InputNumberExperimentalWrapperProps } from "./types";
4
+ declare function InputNumberExperimentalWrapper({ ref, ...props }: InputNumberExperimentalWrapperProps & {
5
+ readonly ref?: Ref<InputNumberExperimentalRef>;
6
+ }): React.JSX.Element;
7
+ declare namespace InputNumberExperimentalWrapper {
8
+ var displayName: string;
9
+ }
3
10
  declare function InputNumberExperimentalGroup({ children, className, style, }: InputNumberExperimentalGroupProps): React.JSX.Element;
4
11
  declare namespace InputNumberExperimentalGroup {
5
12
  var displayName: string;
@@ -44,18 +51,27 @@ declare function InputNumberExperimentalAffixCompound({ variation, label, icon,
44
51
  declare namespace InputNumberExperimentalAffixCompound {
45
52
  var displayName: string;
46
53
  }
47
- export declare const InputNumberExperimental: React.ForwardRefExoticComponent<InputNumberExperimentalProps & React.RefAttributes<InputNumberExperimentalRef>> & {
48
- Wrapper: React.ForwardRefExoticComponent<InputNumberExperimentalWrapperProps & React.RefAttributes<InputNumberExperimentalRef>>;
49
- Group: typeof InputNumberExperimentalGroup;
50
- Footer: typeof InputNumberExperimentalFooter;
51
- Input: typeof InputNumberExperimentalInput;
52
- Label: typeof InputNumberExperimentalLabel;
53
- Description: typeof InputNumberExperimentalDescription;
54
- Error: typeof InputNumberExperimentalError;
55
- Loading: typeof InputNumberExperimentalLoading;
56
- Stepper: typeof InputNumberExperimentalStepper;
57
- Increment: typeof InputNumberExperimentalIncrement;
58
- Decrement: typeof InputNumberExperimentalDecrement;
59
- Affix: typeof InputNumberExperimentalAffixCompound;
60
- };
61
- export type { InputNumberExperimentalAffix, InputNumberExperimentalProps, InputNumberExperimentalRef, InputNumberExperimentalSuffix, InputNumberExperimentalSuffixProp, InputNumberExperimentalWrapperProps, } from "./types";
54
+ /**
55
+ * Sugar layer: composes the parts from props. This is the same composition a
56
+ * consumer would write by hand with `<InputNumberExperimental.Wrapper>` and the
57
+ * parts — to customize a single piece, copy this tree and swap that part.
58
+ */
59
+ declare function InputNumberExperimental({ ref, ...props }: InputNumberExperimentalProps & {
60
+ readonly ref?: Ref<InputNumberExperimentalRef>;
61
+ }): React.JSX.Element;
62
+ declare namespace InputNumberExperimental {
63
+ var displayName: string;
64
+ var Wrapper: typeof InputNumberExperimentalWrapper;
65
+ var Group: typeof InputNumberExperimentalGroup;
66
+ var Footer: typeof InputNumberExperimentalFooter;
67
+ var Input: typeof InputNumberExperimentalInput;
68
+ var Label: typeof InputNumberExperimentalLabel;
69
+ var Description: typeof InputNumberExperimentalDescription;
70
+ var Error: typeof InputNumberExperimentalError;
71
+ var Loading: typeof InputNumberExperimentalLoading;
72
+ var Stepper: typeof InputNumberExperimentalStepper;
73
+ var Increment: typeof InputNumberExperimentalIncrement;
74
+ var Decrement: typeof InputNumberExperimentalDecrement;
75
+ var Affix: typeof InputNumberExperimentalAffixCompound;
76
+ }
77
+ export { InputNumberExperimental };
@@ -63,7 +63,6 @@ export interface InputNumberExperimentalWrapperProps extends InputNumberExperime
63
63
  */
64
64
  readonly loading?: boolean;
65
65
  readonly max?: number;
66
- readonly maxLength?: number;
67
66
  readonly min?: number;
68
67
  readonly name?: string;
69
68
  readonly onBlur?: (event?: FocusEvent<HTMLInputElement>) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components",
3
- "version": "8.25.1",
3
+ "version": "8.25.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -545,5 +545,5 @@
545
545
  "> 1%",
546
546
  "IE 10"
547
547
  ],
548
- "gitHead": "1ad8858948625e5d646ba969560948576dc5e721"
548
+ "gitHead": "6ad9bdaf20469f010e018c70c5c2a1e4f061dbfe"
549
549
  }