Skip to content

Commit ff7cdb2

Browse files
authored
Use NumberFields in more places; enhance NumberField component (#1926)
* Use NumberFields in more places; enhance NumberField component * Apply default minimum of 0 on NumberFields * Refactor onChange logic in TextFields, permit 'text' and 'password' types
1 parent efa3978 commit ff7cdb2

File tree

6 files changed

+15
-47
lines changed

6 files changed

+15
-47
lines changed

app/components/form/fields/DiskSizeField.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ export function DiskSizeField<
3838
return (
3939
<NumberField
4040
units="GiB"
41-
type="number"
4241
required={required}
4342
name={name}
4443
min={minSize}

app/components/form/fields/NumberField.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ export function NumberField<
5252
* Primarily exists for `NumberField`, but we occasionally also need a plain field
5353
* without a label on it.
5454
*
55-
* Note that `id` is an allowed prop, unlike in `TextField`, where it is always
55+
* Note that `id` is an allowed prop, unlike in `NumberField`, where it is always
5656
* generated from `name`. This is because we need to pass the generated ID in
57-
* from there to here. For the case where `TextFieldInner` is used
57+
* from there to here. For the case where `NumberFieldInner` is used
5858
* independently, we also generate an ID for use only if none is passed in.
5959
*/
6060
export const NumberFieldInner = <
@@ -69,6 +69,8 @@ export const NumberFieldInner = <
6969
required,
7070
id: idProp,
7171
disabled,
72+
max,
73+
min = 0,
7274
}: TextFieldProps<TFieldValues, TName>) => {
7375
const generatedId = useId()
7476
const id = idProp || generatedId
@@ -89,6 +91,8 @@ export const NumberFieldInner = <
8991
})}
9092
aria-describedby={tooltipText ? `${id}-label-tip` : undefined}
9193
isDisabled={disabled}
94+
maxValue={max ? Number(max) : undefined}
95+
minValue={min !== undefined ? Number(min) : undefined}
9296
{...field}
9397
formatOptions={{
9498
useGrouping: false,

app/components/form/fields/TextField.tsx

Lines changed: 4 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export interface TextFieldProps<
3333
> extends UITextFieldProps {
3434
name: TName
3535
/** HTML type attribute, defaults to text */
36-
type?: string
36+
type?: 'text' | 'password'
3737
/** Will default to name if not provided */
3838
label?: string
3939
/**
@@ -92,17 +92,9 @@ export function TextField<
9292
)
9393
}
9494

95-
function numberToInputValue(value: number) {
96-
// could add `|| value === 0`, but that means when the value is 0, we always
97-
// show an empty string, which is weird, and doubly bad because then the
98-
// browser apparently fails to validate it against minimum (if one is
99-
// provided). I found it let me submit instance create with 0 CPUs.
100-
return isNaN(value) ? '' : value.toString()
101-
}
102-
10395
/**
10496
* Primarily exists for `TextField`, but we occasionally also need a plain field
105-
* without a label on it. Note special handling of `type="number"`.
97+
* without a label on it.
10698
*
10799
* Note that `id` is an allowed prop, unlike in `TextField`, where it is always
108100
* generated from `name`. This is because we need to pass the generated ID in
@@ -131,7 +123,7 @@ export const TextFieldInner = <
131123
name={name}
132124
control={control}
133125
rules={{ required, validate }}
134-
render={({ field: { onChange, value, ...fieldRest }, fieldState: { error } }) => {
126+
render={({ field: { onChange, ...fieldRest }, fieldState: { error } }) => {
135127
return (
136128
<>
137129
<UITextField
@@ -143,32 +135,9 @@ export const TextFieldInner = <
143135
[`${id}-help-text`]: !!tooltipText,
144136
})}
145137
aria-describedby={tooltipText ? `${id}-label-tip` : undefined}
146-
// note special handling for number fields, which produce a number
147-
// for the calling code despite the actual input value necessarily
148-
// being a string.
149138
onChange={(e) => {
150-
if (transform) {
151-
onChange(transform(e.target.value))
152-
return
153-
}
154-
if (type === 'number') {
155-
if (e.target.value.trim() === '') {
156-
onChange(0)
157-
} else if (!isNaN(e.target.valueAsNumber)) {
158-
onChange(e.target.valueAsNumber)
159-
}
160-
// otherwise ignore the input. this means, for example, typing
161-
// letters does nothing. If we instead said take anything
162-
// that's NaN and fall back to 0, typing a letter would reset
163-
// the field to 0, which is silly. Browsers are supposed to
164-
// ignore non-numeric input for you anyway, but Firefox does
165-
// not.
166-
return
167-
}
168-
169-
onChange(e.target.value)
139+
onChange(transform ? transform(e.target.value) : e.target.value)
170140
}}
171-
value={type === 'number' ? numberToInputValue(value) : value}
172141
{...fieldRest}
173142
{...props}
174143
/>

app/forms/instance-create.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
ImageSelectField,
4747
NameField,
4848
NetworkInterfaceField,
49+
NumberField,
4950
RadioFieldDyn,
5051
SshKeysField,
5152
TextField,
@@ -290,8 +291,7 @@ export function CreateInstanceForm() {
290291
</Tabs.Content>
291292

292293
<Tabs.Content value="custom">
293-
<TextField
294-
type="number"
294+
<NumberField
295295
required
296296
label="CPUs"
297297
name="ncpus"
@@ -308,9 +308,8 @@ export function CreateInstanceForm() {
308308
}}
309309
disabled={isSubmitting}
310310
/>
311-
<TextField
311+
<NumberField
312312
units="GiB"
313-
type="number"
314313
required
315314
label="Memory"
316315
name="memory"

app/forms/silo-create.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ export function CreateSiloSideModalForm() {
108108
label="CPU quota"
109109
name="quotas.cpus"
110110
required
111-
type="number"
112111
units="nCPUs"
113112
validate={validateQuota}
114113
/>
@@ -117,7 +116,6 @@ export function CreateSiloSideModalForm() {
117116
label="Memory quota"
118117
name="quotas.memory"
119118
required
120-
type="number"
121119
units="GiB"
122120
validate={validateQuota}
123121
/>
@@ -126,7 +124,6 @@ export function CreateSiloSideModalForm() {
126124
label="Storage quota"
127125
name="quotas.storage"
128126
required
129-
type="number"
130127
units="GiB"
131128
validate={validateQuota}
132129
/>

app/test/e2e/instance-create.e2e.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ test('can create an instance with custom hardware', async ({ page }) => {
109109

110110
// Fill in custom specs
111111
await page.getByRole('tab', { name: 'Custom' }).click()
112-
await page.fill('input[name=ncpus]', '29')
113-
await page.fill('input[name=memory]', '53')
112+
await page.getByRole('textbox', { name: 'CPUs' }).fill('29')
113+
await page.getByRole('textbox', { name: 'Memory (GiB)' }).fill('53')
114114

115115
await page.getByRole('textbox', { name: 'Disk name' }).fill('my-boot-disk')
116116
const diskSizeInput = page.getByRole('textbox', { name: 'Disk size (GiB)' })

0 commit comments

Comments
 (0)