diff --git a/app/components/form/fields/RadioField.tsx b/app/components/form/fields/RadioField.tsx index 04e3b99ce6..d69a4a5b9c 100644 --- a/app/components/form/fields/RadioField.tsx +++ b/app/components/form/fields/RadioField.tsx @@ -6,7 +6,7 @@ * Copyright Oxide Computer Company */ import cn from 'classnames' -import { useId, type default as React } from 'react' +import { useId } from 'react' import { useController, type Control, diff --git a/app/table/QueryTable.tsx b/app/table/QueryTable.tsx index 74a464d000..bf25f9c361 100644 --- a/app/table/QueryTable.tsx +++ b/app/table/QueryTable.tsx @@ -7,7 +7,7 @@ */ import { useQuery } from '@tanstack/react-query' import { getCoreRowModel, useReactTable, type ColumnDef } from '@tanstack/react-table' -import { useEffect, useMemo, useRef, type default as React } from 'react' +import { useEffect, useMemo, useRef } from 'react' import { ensurePrefetched, type PaginatedQuery, type ResultsPage } from '@oxide/api' diff --git a/app/ui/lib/AuthCodeInput.tsx b/app/ui/lib/AuthCodeInput.tsx index 94828d93a4..2c10c1e162 100644 --- a/app/ui/lib/AuthCodeInput.tsx +++ b/app/ui/lib/AuthCodeInput.tsx @@ -14,13 +14,7 @@ * license that can be found in the LICENSE file or at * https://opensource.org/licenses/MIT. */ -import { - forwardRef, - useEffect, - useImperativeHandle, - useRef, - type default as React, -} from 'react' +import { useEffect, useRef } from 'react' import { KEYS } from '~/ui/util/keys' import { invariant } from '~/util/invariant' @@ -38,11 +32,6 @@ export type AuthCodeProps = { dashAfterIdxs?: number[] } -export type AuthCodeRef = { - focus: () => void - clear: () => void -} - const INPUT_PATTERN = '[a-zA-Z]{1}' // the reason these helpers are here is we to skip the dashes when we're looking @@ -77,157 +66,133 @@ const Dash = () => ( ) // See https://github.com/drac94/react-auth-code-input -export const AuthCodeInput = forwardRef( - ( - { - ariaLabel, - autoFocus = true, - containerClassName, - disabled, - inputClassName, - length = 6, - placeholder, - onChange, - dashAfterIdxs = [], - }, - ref - ) => { - invariant(!isNaN(length) || length > 0, 'Length must be a number greater than 0') - invariant( - dashAfterIdxs.every((i) => 0 <= i && i < length - 1), - '"Dash after" indices must mark spots between inputs, i.e., 0 <= i < length - 1' - ) - - const inputsRef = useRef>([]) - - useImperativeHandle(ref, () => ({ - focus: () => { - if (inputsRef.current) { - inputsRef.current[0].focus() - } - }, - clear: () => { - if (inputsRef.current) { - for (const input of inputsRef.current) { - input.value = '' - } - inputsRef.current[0].focus() - } - sendResult() - }, - })) - - useEffect(() => { - if (autoFocus) { - inputsRef.current[0].focus() - } - }, [autoFocus]) - - const sendResult = () => { - // user_code is always uppercase - // https://github.com/oxidecomputer/omicron/blob/c63fe1658674186d974e3287afdce09b07912afd/nexus/db-model/src/device_auth.rs#L72-L77 - const res = inputsRef.current - .map((input) => input.value) - .join('') - .toUpperCase() - onChange?.(res) +export function AuthCodeInput({ + ariaLabel, + autoFocus = true, + containerClassName, + disabled, + inputClassName, + length = 6, + placeholder, + onChange, + dashAfterIdxs = [], +}: AuthCodeProps) { + invariant(!isNaN(length) || length > 0, 'Length must be a number greater than 0') + invariant( + dashAfterIdxs.every((i) => 0 <= i && i < length - 1), + '"Dash after" indices must mark spots between inputs, i.e., 0 <= i < length - 1' + ) + + const inputsRef = useRef>([]) + + useEffect(() => { + if (autoFocus) { + inputsRef.current[0].focus() } + }, [autoFocus]) + + const sendResult = () => { + // user_code is always uppercase + // https://github.com/oxidecomputer/omicron/blob/c63fe1658674186d974e3287afdce09b07912afd/nexus/db-model/src/device_auth.rs#L72-L77 + const res = inputsRef.current + .map((input) => input.value) + .join('') + .toUpperCase() + onChange?.(res) + } - const handleOnChange = (e: React.ChangeEvent) => { - const { value } = e.target - const nextInput = getNextInputSibling(e.target) - if (value.length > 1) { - e.target.value = value.charAt(0) + const handleOnChange = (e: React.ChangeEvent) => { + const { value } = e.target + const nextInput = getNextInputSibling(e.target) + if (value.length > 1) { + e.target.value = value.charAt(0) + if (nextInput) { + nextInput.focus() + } + } else { + if (value.match(INPUT_PATTERN)) { if (nextInput) { nextInput.focus() } } else { - if (value.match(INPUT_PATTERN)) { - if (nextInput) { - nextInput.focus() - } - } else { - e.target.value = '' - } + e.target.value = '' } - sendResult() } + sendResult() + } - const handleOnKeyDown = (e: React.KeyboardEvent) => { - const target = e.target as HTMLInputElement - if (e.key === KEYS.backspace) { - if (target.value === '') { - const prevInput = getPrevInputSibling(target) - if (prevInput !== null) { - prevInput.value = '' - prevInput.focus() - e.preventDefault() - } - } else { - target.value = '' + const handleOnKeyDown = (e: React.KeyboardEvent) => { + const target = e.target as HTMLInputElement + if (e.key === KEYS.backspace) { + if (target.value === '') { + const prevInput = getPrevInputSibling(target) + if (prevInput !== null) { + prevInput.value = '' + prevInput.focus() + e.preventDefault() } - sendResult() + } else { + target.value = '' } + sendResult() } + } - const handleOnFocus = (e: React.FocusEvent) => { - e.target.select() - } + const handleOnFocus = (e: React.FocusEvent) => { + e.target.select() + } - const handleOnPaste = (e: React.ClipboardEvent) => { - const pastedValue = e.clipboardData.getData('Text') - - let currentInput = 0 - - for (let i = 0; i < pastedValue.length; i++) { - const pastedCharacter = pastedValue.charAt(i) - const currentValue = inputsRef.current[currentInput].value - if (pastedCharacter.match(INPUT_PATTERN) && !currentValue) { - const input = inputsRef.current[currentInput] - input.value = pastedCharacter - const nextInput = getNextInputSibling(input) - if (nextInput !== null) { - nextInput.focus() - currentInput++ - } - } - } - sendResult() + const handleOnPaste = (e: React.ClipboardEvent) => { + const pastedValue = e.clipboardData.getData('Text') - e.preventDefault() - } + let currentInput = 0 - const inputs = [] - for (let i = 0; i < length; i++) { - inputs.push( - { - inputsRef.current[i] = el - }} - maxLength={1} - className={inputClassName} - autoComplete="off" - aria-label={ - ariaLabel ? `${ariaLabel}. Character ${i + 1}.` : `Character ${i + 1}.` - } - disabled={disabled} - placeholder={placeholder} - /> - ) - - if (dashAfterIdxs.includes(i)) { - inputs.push() + for (let i = 0; i < pastedValue.length; i++) { + const pastedCharacter = pastedValue.charAt(i) + const currentValue = inputsRef.current[currentInput].value + if (pastedCharacter.match(INPUT_PATTERN) && !currentValue) { + const input = inputsRef.current[currentInput] + input.value = pastedCharacter + const nextInput = getNextInputSibling(input) + if (nextInput !== null) { + nextInput.focus() + currentInput++ + } } } + sendResult() - return
{inputs}
+ e.preventDefault() } -) + + const inputs = [] + for (let i = 0; i < length; i++) { + inputs.push( + { + inputsRef.current[i] = el + }} + maxLength={1} + className={inputClassName} + autoComplete="off" + aria-label={ariaLabel ? `${ariaLabel}. Character ${i + 1}.` : `Character ${i + 1}.`} + disabled={disabled} + placeholder={placeholder} + /> + ) + + if (dashAfterIdxs.includes(i)) { + inputs.push() + } + } + + return
{inputs}
+} diff --git a/app/ui/lib/DialogOverlay.tsx b/app/ui/lib/DialogOverlay.tsx index 4fbe391925..e0dfd12ce0 100644 --- a/app/ui/lib/DialogOverlay.tsx +++ b/app/ui/lib/DialogOverlay.tsx @@ -7,9 +7,13 @@ */ import * as m from 'motion/react-m' -import { forwardRef } from 'react' +import { type Ref } from 'react' -export const DialogOverlay = forwardRef((_, ref) => ( +type Props = { + ref?: Ref +} + +export const DialogOverlay = ({ ref }: Props) => ( ((_, ref) => ( exit={{ opacity: 0 }} transition={{ duration: 0.15, ease: 'easeOut' }} /> -)) +) diff --git a/app/ui/lib/DropdownMenu.tsx b/app/ui/lib/DropdownMenu.tsx index f70954c30c..d4e15ee0f9 100644 --- a/app/ui/lib/DropdownMenu.tsx +++ b/app/ui/lib/DropdownMenu.tsx @@ -14,7 +14,7 @@ import { type MenuItemsProps, } from '@headlessui/react' import cn from 'classnames' -import { forwardRef, type ForwardedRef, type ReactNode } from 'react' +import { type ReactNode, type Ref } from 'react' import { Link } from 'react-router' export const Root = Menu @@ -60,26 +60,24 @@ export function LinkItem({ className, to, children }: LinkItemProps) { ) } -type ButtonRef = ForwardedRef type ItemProps = { className?: string onSelect?: () => void children: ReactNode disabled?: boolean + ref?: Ref } // need to forward ref because of tooltips on disabled menu buttons -export const Item = forwardRef( - ({ className, onSelect, children, disabled }: ItemProps, ref: ButtonRef) => ( - - - - ) +export const Item = ({ className, onSelect, children, disabled, ref }: ItemProps) => ( + + + ) diff --git a/app/ui/lib/FileInput.tsx b/app/ui/lib/FileInput.tsx index 09b348cd64..38302be29f 100644 --- a/app/ui/lib/FileInput.tsx +++ b/app/ui/lib/FileInput.tsx @@ -8,12 +8,12 @@ import cn from 'classnames' import { filesize } from 'filesize' import { - forwardRef, useRef, useState, type ChangeEvent, type ComponentProps, type MouseEvent, + type Ref, } from 'react' import { mergeRefs } from 'react-merge-refs' @@ -24,102 +24,106 @@ import { Truncate } from '~/ui/lib/Truncate' export type FileInputProps = Omit, 'type' | 'onChange'> & { onChange: (f: File | null) => void error?: boolean + ref?: Ref } // Wrapping a file input in a `