Skip to content

Commit

Permalink
fix: use react change events + remove text field wrapper (#295)
Browse files Browse the repository at this point in the history
  • Loading branch information
PHILLIPS71 authored Jan 10, 2025
1 parent 3382d06 commit deb52e1
Show file tree
Hide file tree
Showing 9 changed files with 75 additions and 128 deletions.
22 changes: 14 additions & 8 deletions packages/react/src/components/form/use-form-group.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,23 @@ import type { LabelAria } from 'react-aria'
import React from 'react'
import { form } from '@giantnodes/theme'

import type { ChangeHandler } from '~/utilities/types'
import { create } from '~/utilities/create-context'

export type FeedbackType = 'success' | 'info' | 'warning' | 'error'

type UseFormGroupProps = LabelAria & {
ref?: React.RefObject<HTMLInputElement | HTMLLabelElement>
type UseFormElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement

type UseFormGroupProps<T extends UseFormElement = UseFormElement> = LabelAria & {
ref?: React.RefObject<T>
name?: string
color?: FormVariantProps['color']
onChange?: ChangeHandler
onBlur?: ChangeHandler
onChange?: React.ChangeEventHandler<T>
onBlur?: React.FocusEventHandler<T>
}

type FormGroupContextType = ReturnType<typeof useFormGroupValue>
type FormGroupContextType<T extends UseFormElement = UseFormElement> = ReturnType<typeof useFormGroupValue<T>>

export const useFormGroupValue = (props: UseFormGroupProps) => {
export const useFormGroupValue = <T extends UseFormElement = UseFormElement>(props: UseFormGroupProps<T>) => {
const { ref, name, fieldProps, labelProps, onChange, onBlur } = props

const [feedback, setFeedback] = React.useState<FeedbackType | null>(null)
Expand Down Expand Up @@ -56,8 +57,13 @@ export const useFormGroupValue = (props: UseFormGroupProps) => {
}
}

export const [FormGroupContext, useFormGroup] = create<FormGroupContextType | undefined>({
export const [FormGroupContext, useFormGroupBase] = create<FormGroupContextType | undefined>({
name: 'FormGroupContext',
strict: false,
errorMessage: 'useFormGroup: `context` is undefined. Seems you forgot to wrap component within <Form.Group />',
})

export const useFormGroup = <T extends UseFormElement = UseFormElement>() => {
const context = useFormGroupBase()
return context as FormGroupContextType<T> | undefined
}
59 changes: 31 additions & 28 deletions packages/react/src/components/input/InputPhone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

import type { InputVariantProps } from '@giantnodes/theme'
import type { CountryCode } from 'libphonenumber-js'
import type { InputProps, TextFieldProps } from 'react-aria-components'
import type { InputProps } from 'react-aria-components'
import React from 'react'
import { getExampleNumber, parsePhoneNumber } from 'libphonenumber-js/min'
import examples from 'libphonenumber-js/mobile/examples'
import { Input, TextField } from 'react-aria-components'
import { Input } from 'react-aria-components'

import type * as Polymorphic from '~/utilities/polymorphic'
import { useFormGroup } from '~/components/form/use-form-group.hook'
Expand All @@ -17,7 +17,7 @@ import { cn } from '~/utilities'
const __ELEMENT_TYPE__ = 'input'

type ComponentOwnProps = InputVariantProps &
Omit<TextFieldProps, 'children'> & {
Omit<InputProps, 'size'> & {
country: CountryCode
onTemplateChange?: (template: string) => void
}
Expand All @@ -34,12 +34,13 @@ type ComponentType = (<TElement extends React.ElementType = typeof __ELEMENT_TYP

const Component: ComponentType = React.forwardRef<React.ReactElement<ComponentOwnProps>, ComponentOwnProps>(
<TElement extends React.ElementType>(props: ComponentProps<TElement>, ref: Polymorphic.Ref<TElement>) => {
const { as, className, country, color, size, shape, variant, onTemplateChange, ...rest } = props
const { className, country, color, size, shape, variant, onChange, onBlur, onTemplateChange, ...rest } = props

const Element = as ?? TextField
const group = useFormGroup()
const group = useFormGroup<HTMLInputElement>()
const context = useInput()

const [value, setValue] = React.useState<string>('')

const { slots } = useInputValue({
color: color ?? group?.status ?? context?.color,
size: size ?? context?.size,
Expand Down Expand Up @@ -101,32 +102,36 @@ const Component: ComponentType = React.forwardRef<React.ReactElement<ComponentOw
[template]
)

const onChange = React.useCallback(
(value: string) =>
group?.onChange?.({
target: { value: format(value) },
type: 'change',
}),
[format, group]
)
const onInputChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const formatted = format(event.target.value)

const component = React.useMemo<TextFieldProps>(
() => ({
name: group?.name,
onChange: onChange,
onBlur: group?.onBlur,
className: slots.field(),
...group?.fieldProps,
}),
[group?.fieldProps, group?.name, group?.onBlur, onChange, slots]
const synthetic = {
...event,
target: {
...event.target,
value: formatted,
},
}

setValue(formatted)

return (group?.onChange ?? onChange)?.(synthetic)
},
[format, group?.onChange, onChange]
)

const input = React.useMemo<InputProps>(
const component = React.useMemo<InputProps>(
() => ({
name: group?.name,
onChange: onInputChange,
onBlur: group?.onBlur ?? onBlur,
className: slots.input({ className: cn(className) }),
value,
...group?.fieldProps,
...rest,
}),
[className, rest, slots]
[className, group?.fieldProps, group?.name, group?.onBlur, onBlur, onInputChange, rest, slots, value]
)

React.useEffect(() => {
Expand All @@ -139,9 +144,7 @@ const Component: ComponentType = React.forwardRef<React.ReactElement<ComponentOw
<CountryFlag country={country} />
</Addon>

<Element {...component}>
<Input {...input} ref={(group?.ref as React.RefObject<HTMLInputElement> | undefined) ?? ref} />
</Element>
<Input {...component} ref={group?.ref ?? ref} />
</>
)
}
Expand Down
37 changes: 10 additions & 27 deletions packages/react/src/components/input/InputText.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
'use client'

import type { InputVariantProps } from '@giantnodes/theme'
import type { InputProps, TextFieldProps } from 'react-aria-components'
import type { InputProps } from 'react-aria-components'
import React from 'react'
import { Input, TextField } from 'react-aria-components'
import { Input } from 'react-aria-components'

import type * as Polymorphic from '~/utilities/polymorphic'
import { useFormGroup } from '~/components/form/use-form-group.hook'
Expand All @@ -13,7 +13,7 @@ import { cn } from '~/utilities'
const __ELEMENT_TYPE__ = 'input'

type ComponentOwnProps = InputVariantProps &
Omit<TextFieldProps, 'children'> & {
Omit<InputProps, 'size'> & {
directory?: boolean
}

Expand All @@ -29,11 +29,9 @@ type ComponentType = (<TElement extends React.ElementType = typeof __ELEMENT_TYP

const Component: ComponentType = React.forwardRef<React.ReactElement<ComponentOwnProps>, ComponentOwnProps>(
<TElement extends React.ElementType>(props: ComponentProps<TElement>, ref: Polymorphic.Ref<TElement>) => {
const { as, className, color, size, shape, variant, directory, ...rest } = props
const { className, color, size, shape, variant, directory, ...rest } = props

const Element = as ?? TextField

const group = useFormGroup()
const group = useFormGroup<HTMLInputElement>()
const context = useInput()

const { slots } = useInputValue({
Expand All @@ -43,36 +41,21 @@ const Component: ComponentType = React.forwardRef<React.ReactElement<ComponentOw
variant: variant ?? context?.variant,
})

const component = React.useMemo<TextFieldProps>(
const component = React.useMemo<InputProps>(
() => ({
name: group?.name,
onChange: (value: string) =>
group?.onChange?.({
target: { value },
type: 'change',
}),
onChange: group?.onChange,
onBlur: group?.onBlur,
className: slots.field(),
...group?.fieldProps,
}),
[group, slots]
)

const input = React.useMemo<InputProps>(
() => ({
className: slots.input({ className: cn(className) }),
directory: directory ? 'true' : undefined,
webkitdirectory: directory ? 'true' : undefined,
...group?.fieldProps,
...rest,
}),
[className, directory, rest, slots]
[className, directory, group?.fieldProps, group?.name, group?.onBlur, group?.onChange, rest, slots]
)

return (
<Element {...component}>
<Input {...input} ref={(group?.ref as React.RefObject<HTMLInputElement> | undefined) ?? ref} />
</Element>
)
return <Input {...component} ref={group?.ref ?? ref} />
}
)

Expand Down
37 changes: 10 additions & 27 deletions packages/react/src/components/input/InputTextArea.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
'use client'

import type { InputVariantProps } from '@giantnodes/theme'
import type { TextAreaProps, TextFieldProps } from 'react-aria-components'
import type { TextAreaProps } from 'react-aria-components'
import React from 'react'
import { TextArea, TextField } from 'react-aria-components'
import { TextArea } from 'react-aria-components'

import type * as Polymorphic from '~/utilities/polymorphic'
import { useFormGroup } from '~/components/form/use-form-group.hook'
Expand All @@ -26,48 +26,31 @@ type ComponentType = (<TElement extends React.ElementType = typeof __ELEMENT_TYP

const Component: ComponentType = React.forwardRef<React.ReactElement<ComponentOwnProps>, ComponentOwnProps>(
<TElement extends React.ElementType>(props: ComponentProps<TElement>, ref: Polymorphic.Ref<TElement>) => {
const { as, className, color, size, shape, variant, ...rest } = props

const Element = as ?? TextField
const { className, color, size, shape, variant, ...rest } = props

const group = useFormGroup<HTMLTextAreaElement>()
const context = useInput()

const { slots } = useInputValue({
color: color ?? context?.color,
size: size ?? context?.size,
shape: shape ?? context?.shape,
variant: variant ?? context?.variant,
})

const group = useFormGroup()

const component = React.useMemo<TextFieldProps>(
const input = React.useMemo<TextAreaProps>(
() => ({
name: group?.name,
onChange: (value: string) =>
group?.onChange?.({
target: { value },
type: 'change',
}),
onChange: group?.onChange,
onBlur: group?.onBlur,
className: slots.field(),
...group?.fieldProps,
}),
[group, slots]
)

const input = React.useMemo<TextAreaProps>(
() => ({
className: slots.input({ className: cn(className) }),
...group?.fieldProps,
...rest,
}),
[className, rest, slots]
[className, group?.fieldProps, group?.name, group?.onBlur, group?.onChange, rest, slots]
)

return (
<Element {...component}>
<TextArea {...input} ref={(group?.ref as React.RefObject<HTMLTextAreaElement> | undefined) ?? ref} />
</Element>
)
return <TextArea {...input} ref={group?.ref ?? ref} />
}
)

Expand Down
17 changes: 3 additions & 14 deletions packages/react/src/components/select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ const Component: ComponentType = React.forwardRef(

const Element = as ?? Select

const group = useFormGroup()
const group = useFormGroup<HTMLSelectElement>()

const context = useSelectValue({
ref: (group?.ref as React.RefObject<HTMLInputElement> | undefined) ?? ref,
ref: group?.ref ?? ref,
name: group?.name,
behavior,
mode,
Expand All @@ -87,22 +87,11 @@ const Component: ComponentType = React.forwardRef(
placeholder,
onChange: group?.onChange,
onBlur: group?.onBlur,
onSelectionChange: context.onSelect,
className: context.slots.select({ className: className?.toString() }),
...group?.fieldProps,
...rest,
}),
[
className,
context.onSelect,
context.slots,
group?.fieldProps,
group?.name,
group?.onBlur,
group?.onChange,
placeholder,
rest,
]
[className, context.slots, group?.fieldProps, group?.name, group?.onBlur, group?.onChange, placeholder, rest]
)

const button = React.useMemo<ButtonProps>(
Expand Down
25 changes: 4 additions & 21 deletions packages/react/src/components/select/use-select.hook.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,30 @@
'use client'

import type { SelectVariantProps } from '@giantnodes/theme'
import type { Key, SelectionMode, SelectProps } from 'react-aria-components'
import type { SelectionMode, SelectProps } from 'react-aria-components'
import React from 'react'
import { select } from '@giantnodes/theme'

import type { ChangeHandler } from '~/utilities/types'
import { create } from '~/utilities/create-context'

type UseSelectProps<TElement extends object> = SelectVariantProps &
Pick<SelectProps<TElement>, 'onSelectionChange'> & {
ref?: React.RefObject<HTMLInputElement>
ref?: React.RefObject<TElement>
name?: string
behavior?: 'toggle' | 'replace'
mode?: SelectionMode
onChange?: ChangeHandler
onChange?: React.ChangeEventHandler<TElement>
}

type SelectContextType = ReturnType<typeof useSelectValue>

export const useSelectValue = <TElement extends object>(props: UseSelectProps<TElement>) => {
const { ref, name, size, status, onChange, onSelectionChange } = props
const { size, status } = props

const slots = React.useMemo(() => select({ size, status }), [size, status])

const onSelect = (key: Key) => {
onSelectionChange?.(key)

if (onChange && typeof onChange === 'function' && ref?.current) {
const event = {
target: {
value: key.toString(),
name,
},
}

onChange(event)
}
}

return {
slots,
onSelect,
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/utilities/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type React from 'react'

export type Component<
TElementType extends React.ElementType = 'div',
TProps = unknown,
Expand All @@ -10,5 +12,3 @@ export type Override<T, R> = Omit<T, keyof R> & R
export type ComponentWithoutAs<TElement extends React.ElementType> = Omit<Component<TElement>, 'as'>

export type Selection = 'all' | Set<string | number>

export type ChangeHandler = (event: { target: unknown; type?: unknown }) => void | boolean
Loading

0 comments on commit deb52e1

Please sign in to comment.