Skip to content

Commit 2ec0873

Browse files
authored
chore: get rid of forwardRef thanks to React 19 (#2715)
* get rid of forwardRef * remove ref from AuthCodeInput It was only needed if you wanted to override the default autofocus behavior. * get the rest
1 parent 69a171c commit 2ec0873

File tree

10 files changed

+277
-321
lines changed

10 files changed

+277
-321
lines changed

app/components/form/fields/RadioField.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* Copyright Oxide Computer Company
77
*/
88
import cn from 'classnames'
9-
import { useId, type default as React } from 'react'
9+
import { useId } from 'react'
1010
import {
1111
useController,
1212
type Control,

app/table/QueryTable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88
import { useQuery } from '@tanstack/react-query'
99
import { getCoreRowModel, useReactTable, type ColumnDef } from '@tanstack/react-table'
10-
import { useEffect, useMemo, useRef, type default as React } from 'react'
10+
import { useEffect, useMemo, useRef } from 'react'
1111

1212
import { ensurePrefetched, type PaginatedQuery, type ResultsPage } from '@oxide/api'
1313

app/ui/lib/AuthCodeInput.tsx

Lines changed: 111 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,7 @@
1414
* license that can be found in the LICENSE file or at
1515
* https://opensource.org/licenses/MIT.
1616
*/
17-
import {
18-
forwardRef,
19-
useEffect,
20-
useImperativeHandle,
21-
useRef,
22-
type default as React,
23-
} from 'react'
17+
import { useEffect, useRef } from 'react'
2418

2519
import { KEYS } from '~/ui/util/keys'
2620
import { invariant } from '~/util/invariant'
@@ -38,11 +32,6 @@ export type AuthCodeProps = {
3832
dashAfterIdxs?: number[]
3933
}
4034

41-
export type AuthCodeRef = {
42-
focus: () => void
43-
clear: () => void
44-
}
45-
4635
const INPUT_PATTERN = '[a-zA-Z]{1}'
4736

4837
// the reason these helpers are here is we to skip the dashes when we're looking
@@ -77,157 +66,133 @@ const Dash = () => (
7766
)
7867

7968
// See https://github.com/drac94/react-auth-code-input
80-
export const AuthCodeInput = forwardRef<AuthCodeRef, AuthCodeProps>(
81-
(
82-
{
83-
ariaLabel,
84-
autoFocus = true,
85-
containerClassName,
86-
disabled,
87-
inputClassName,
88-
length = 6,
89-
placeholder,
90-
onChange,
91-
dashAfterIdxs = [],
92-
},
93-
ref
94-
) => {
95-
invariant(!isNaN(length) || length > 0, 'Length must be a number greater than 0')
96-
invariant(
97-
dashAfterIdxs.every((i) => 0 <= i && i < length - 1),
98-
'"Dash after" indices must mark spots between inputs, i.e., 0 <= i < length - 1'
99-
)
100-
101-
const inputsRef = useRef<Array<HTMLInputElement>>([])
102-
103-
useImperativeHandle(ref, () => ({
104-
focus: () => {
105-
if (inputsRef.current) {
106-
inputsRef.current[0].focus()
107-
}
108-
},
109-
clear: () => {
110-
if (inputsRef.current) {
111-
for (const input of inputsRef.current) {
112-
input.value = ''
113-
}
114-
inputsRef.current[0].focus()
115-
}
116-
sendResult()
117-
},
118-
}))
119-
120-
useEffect(() => {
121-
if (autoFocus) {
122-
inputsRef.current[0].focus()
123-
}
124-
}, [autoFocus])
125-
126-
const sendResult = () => {
127-
// user_code is always uppercase
128-
// https://github.com/oxidecomputer/omicron/blob/c63fe1658674186d974e3287afdce09b07912afd/nexus/db-model/src/device_auth.rs#L72-L77
129-
const res = inputsRef.current
130-
.map((input) => input.value)
131-
.join('')
132-
.toUpperCase()
133-
onChange?.(res)
69+
export function AuthCodeInput({
70+
ariaLabel,
71+
autoFocus = true,
72+
containerClassName,
73+
disabled,
74+
inputClassName,
75+
length = 6,
76+
placeholder,
77+
onChange,
78+
dashAfterIdxs = [],
79+
}: AuthCodeProps) {
80+
invariant(!isNaN(length) || length > 0, 'Length must be a number greater than 0')
81+
invariant(
82+
dashAfterIdxs.every((i) => 0 <= i && i < length - 1),
83+
'"Dash after" indices must mark spots between inputs, i.e., 0 <= i < length - 1'
84+
)
85+
86+
const inputsRef = useRef<Array<HTMLInputElement>>([])
87+
88+
useEffect(() => {
89+
if (autoFocus) {
90+
inputsRef.current[0].focus()
13491
}
92+
}, [autoFocus])
93+
94+
const sendResult = () => {
95+
// user_code is always uppercase
96+
// https://github.com/oxidecomputer/omicron/blob/c63fe1658674186d974e3287afdce09b07912afd/nexus/db-model/src/device_auth.rs#L72-L77
97+
const res = inputsRef.current
98+
.map((input) => input.value)
99+
.join('')
100+
.toUpperCase()
101+
onChange?.(res)
102+
}
135103

136-
const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
137-
const { value } = e.target
138-
const nextInput = getNextInputSibling(e.target)
139-
if (value.length > 1) {
140-
e.target.value = value.charAt(0)
104+
const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
105+
const { value } = e.target
106+
const nextInput = getNextInputSibling(e.target)
107+
if (value.length > 1) {
108+
e.target.value = value.charAt(0)
109+
if (nextInput) {
110+
nextInput.focus()
111+
}
112+
} else {
113+
if (value.match(INPUT_PATTERN)) {
141114
if (nextInput) {
142115
nextInput.focus()
143116
}
144117
} else {
145-
if (value.match(INPUT_PATTERN)) {
146-
if (nextInput) {
147-
nextInput.focus()
148-
}
149-
} else {
150-
e.target.value = ''
151-
}
118+
e.target.value = ''
152119
}
153-
sendResult()
154120
}
121+
sendResult()
122+
}
155123

156-
const handleOnKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
157-
const target = e.target as HTMLInputElement
158-
if (e.key === KEYS.backspace) {
159-
if (target.value === '') {
160-
const prevInput = getPrevInputSibling(target)
161-
if (prevInput !== null) {
162-
prevInput.value = ''
163-
prevInput.focus()
164-
e.preventDefault()
165-
}
166-
} else {
167-
target.value = ''
124+
const handleOnKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
125+
const target = e.target as HTMLInputElement
126+
if (e.key === KEYS.backspace) {
127+
if (target.value === '') {
128+
const prevInput = getPrevInputSibling(target)
129+
if (prevInput !== null) {
130+
prevInput.value = ''
131+
prevInput.focus()
132+
e.preventDefault()
168133
}
169-
sendResult()
134+
} else {
135+
target.value = ''
170136
}
137+
sendResult()
171138
}
139+
}
172140

173-
const handleOnFocus = (e: React.FocusEvent<HTMLInputElement>) => {
174-
e.target.select()
175-
}
141+
const handleOnFocus = (e: React.FocusEvent<HTMLInputElement>) => {
142+
e.target.select()
143+
}
176144

177-
const handleOnPaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
178-
const pastedValue = e.clipboardData.getData('Text')
179-
180-
let currentInput = 0
181-
182-
for (let i = 0; i < pastedValue.length; i++) {
183-
const pastedCharacter = pastedValue.charAt(i)
184-
const currentValue = inputsRef.current[currentInput].value
185-
if (pastedCharacter.match(INPUT_PATTERN) && !currentValue) {
186-
const input = inputsRef.current[currentInput]
187-
input.value = pastedCharacter
188-
const nextInput = getNextInputSibling(input)
189-
if (nextInput !== null) {
190-
nextInput.focus()
191-
currentInput++
192-
}
193-
}
194-
}
195-
sendResult()
145+
const handleOnPaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
146+
const pastedValue = e.clipboardData.getData('Text')
196147

197-
e.preventDefault()
198-
}
148+
let currentInput = 0
199149

200-
const inputs = []
201-
for (let i = 0; i < length; i++) {
202-
inputs.push(
203-
<input
204-
key={i}
205-
type="text"
206-
inputMode="text"
207-
onChange={handleOnChange}
208-
onKeyDown={handleOnKeyDown}
209-
onFocus={handleOnFocus}
210-
onPaste={handleOnPaste}
211-
pattern={INPUT_PATTERN}
212-
ref={(el: HTMLInputElement) => {
213-
inputsRef.current[i] = el
214-
}}
215-
maxLength={1}
216-
className={inputClassName}
217-
autoComplete="off"
218-
aria-label={
219-
ariaLabel ? `${ariaLabel}. Character ${i + 1}.` : `Character ${i + 1}.`
220-
}
221-
disabled={disabled}
222-
placeholder={placeholder}
223-
/>
224-
)
225-
226-
if (dashAfterIdxs.includes(i)) {
227-
inputs.push(<Dash key={`${i}-dash`} />)
150+
for (let i = 0; i < pastedValue.length; i++) {
151+
const pastedCharacter = pastedValue.charAt(i)
152+
const currentValue = inputsRef.current[currentInput].value
153+
if (pastedCharacter.match(INPUT_PATTERN) && !currentValue) {
154+
const input = inputsRef.current[currentInput]
155+
input.value = pastedCharacter
156+
const nextInput = getNextInputSibling(input)
157+
if (nextInput !== null) {
158+
nextInput.focus()
159+
currentInput++
160+
}
228161
}
229162
}
163+
sendResult()
230164

231-
return <div className={containerClassName}>{inputs}</div>
165+
e.preventDefault()
232166
}
233-
)
167+
168+
const inputs = []
169+
for (let i = 0; i < length; i++) {
170+
inputs.push(
171+
<input
172+
key={i}
173+
type="text"
174+
inputMode="text"
175+
onChange={handleOnChange}
176+
onKeyDown={handleOnKeyDown}
177+
onFocus={handleOnFocus}
178+
onPaste={handleOnPaste}
179+
pattern={INPUT_PATTERN}
180+
ref={(el: HTMLInputElement) => {
181+
inputsRef.current[i] = el
182+
}}
183+
maxLength={1}
184+
className={inputClassName}
185+
autoComplete="off"
186+
aria-label={ariaLabel ? `${ariaLabel}. Character ${i + 1}.` : `Character ${i + 1}.`}
187+
disabled={disabled}
188+
placeholder={placeholder}
189+
/>
190+
)
191+
192+
if (dashAfterIdxs.includes(i)) {
193+
inputs.push(<Dash key={`${i}-dash`} />)
194+
}
195+
}
196+
197+
return <div className={containerClassName}>{inputs}</div>
198+
}

app/ui/lib/DialogOverlay.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@
77
*/
88

99
import * as m from 'motion/react-m'
10-
import { forwardRef } from 'react'
10+
import { type Ref } from 'react'
1111

12-
export const DialogOverlay = forwardRef<HTMLDivElement>((_, ref) => (
12+
type Props = {
13+
ref?: Ref<HTMLDivElement>
14+
}
15+
16+
export const DialogOverlay = ({ ref }: Props) => (
1317
<m.div
1418
ref={ref}
1519
aria-hidden
@@ -19,4 +23,4 @@ export const DialogOverlay = forwardRef<HTMLDivElement>((_, ref) => (
1923
exit={{ opacity: 0 }}
2024
transition={{ duration: 0.15, ease: 'easeOut' }}
2125
/>
22-
))
26+
)

app/ui/lib/DropdownMenu.tsx

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
type MenuItemsProps,
1515
} from '@headlessui/react'
1616
import cn from 'classnames'
17-
import { forwardRef, type ForwardedRef, type ReactNode } from 'react'
17+
import { type ReactNode, type Ref } from 'react'
1818
import { Link } from 'react-router'
1919

2020
export const Root = Menu
@@ -60,26 +60,24 @@ export function LinkItem({ className, to, children }: LinkItemProps) {
6060
)
6161
}
6262

63-
type ButtonRef = ForwardedRef<HTMLButtonElement>
6463
type ItemProps = {
6564
className?: string
6665
onSelect?: () => void
6766
children: ReactNode
6867
disabled?: boolean
68+
ref?: Ref<HTMLButtonElement>
6969
}
7070

7171
// need to forward ref because of tooltips on disabled menu buttons
72-
export const Item = forwardRef(
73-
({ className, onSelect, children, disabled }: ItemProps, ref: ButtonRef) => (
74-
<MenuItem disabled={disabled}>
75-
<button
76-
type="button"
77-
className={cn('DropdownMenuItem ox-menu-item', className)}
78-
ref={ref}
79-
onClick={onSelect}
80-
>
81-
{children}
82-
</button>
83-
</MenuItem>
84-
)
72+
export const Item = ({ className, onSelect, children, disabled, ref }: ItemProps) => (
73+
<MenuItem disabled={disabled}>
74+
<button
75+
type="button"
76+
className={cn('DropdownMenuItem ox-menu-item', className)}
77+
ref={ref}
78+
onClick={onSelect}
79+
>
80+
{children}
81+
</button>
82+
</MenuItem>
8583
)

0 commit comments

Comments
 (0)