diff --git a/.changeset/hot-llamas-fry.md b/.changeset/hot-llamas-fry.md
new file mode 100644
index 0000000000..cdcf98b38d
--- /dev/null
+++ b/.changeset/hot-llamas-fry.md
@@ -0,0 +1,6 @@
+---
+"@ultraviolet/form": patch
+---
+
+- Minor fixes to `` component
+- Fixed issue with `` component preventing the input to be edited
diff --git a/.changeset/shy-parrots-matter.md b/.changeset/shy-parrots-matter.md
new file mode 100644
index 0000000000..a65b0b0895
--- /dev/null
+++ b/.changeset/shy-parrots-matter.md
@@ -0,0 +1,16 @@
+---
+"@ultraviolet/ui": patch
+---
+
+Fix `` prop `onChange` will now take function with event.
+
+```tsx
+// Before
+onChange={value => value}
+
+// After
+onChangeValue={value => value}
+onChange={event => event.target.value}
+```
+
+This will also fix `` issues such as editing the input value.
diff --git a/packages/form/src/components/TextInputFieldV2/index.tsx b/packages/form/src/components/TextInputFieldV2/index.tsx
index 9b38078289..eefa3d8506 100644
--- a/packages/form/src/components/TextInputFieldV2/index.tsx
+++ b/packages/form/src/components/TextInputFieldV2/index.tsx
@@ -1,6 +1,6 @@
import { TextInputV2 } from '@ultraviolet/ui'
import type { ComponentProps } from 'react'
-import type { FieldPath, FieldValues, Path, PathValue } from 'react-hook-form'
+import type { FieldPath, FieldValues } from 'react-hook-form'
import { useController } from 'react-hook-form'
import { useErrors } from '../../providers'
import type { BaseFieldProps } from '../../types'
@@ -10,10 +10,7 @@ type TextInputFieldProps<
TFieldValues extends FieldValues,
TFieldName extends FieldPath,
> = BaseFieldProps &
- Omit<
- ComponentProps,
- 'value' | 'error' | 'name' | 'onChange'
- > & {
+ Omit, 'value' | 'error' | 'name'> & {
regex?: (RegExp | RegExp[])[]
}
@@ -116,7 +113,7 @@ export const TextInputField = <
}}
onChange={event => {
field.onChange(event)
- onChange?.(event as PathValue>)
+ onChange?.(event)
}}
onFocus={event => {
onFocus?.(event)
@@ -128,7 +125,7 @@ export const TextInputField = <
tabIndex={tabIndex}
tooltip={tooltip}
type={type}
- value={field.value}
+ value={field.value === undefined ? '' : field.value}
id={id}
prefix={prefix}
suffix={suffix}
diff --git a/packages/ui/src/components/DateInput/__stories__/Localized.stories.tsx b/packages/ui/src/components/DateInput/__stories__/Localized.stories.tsx
index f878077bcc..d65c1651a3 100644
--- a/packages/ui/src/components/DateInput/__stories__/Localized.stories.tsx
+++ b/packages/ui/src/components/DateInput/__stories__/Localized.stories.tsx
@@ -12,7 +12,13 @@ const locales = [
export const Localized = (props: ComponentProps) =>
locales.map(({ label, locale }) => (
- {}} label={label} locale={locale} />
+ {}}
+ label={label}
+ locale={locale}
+ />
))
Localized.args = Template.args
diff --git a/packages/ui/src/components/DateInput/__stories__/Size.stories.tsx b/packages/ui/src/components/DateInput/__stories__/Size.stories.tsx
index 2fad76eac0..69acf72812 100644
--- a/packages/ui/src/components/DateInput/__stories__/Size.stories.tsx
+++ b/packages/ui/src/components/DateInput/__stories__/Size.stories.tsx
@@ -5,7 +5,7 @@ import { Stack } from '../../Stack'
export const Size: StoryFn = args => (
{(['small', 'medium', 'large'] as const).map(size => (
-
+
))}
)
diff --git a/packages/ui/src/components/DateInput/index.tsx b/packages/ui/src/components/DateInput/index.tsx
index c58a1eba56..09c650ac92 100644
--- a/packages/ui/src/components/DateInput/index.tsx
+++ b/packages/ui/src/components/DateInput/index.tsx
@@ -303,7 +303,7 @@ export const DateInput = ({
id={localId}
label={label}
labelDescription={labelDescription}
- value={valueFormat || ''}
+ value={valueFormat}
disabled={disabled}
size={size}
suffix={
diff --git a/packages/ui/src/components/SearchInput/index.tsx b/packages/ui/src/components/SearchInput/index.tsx
index 75180fe59c..a0eb4c4a16 100644
--- a/packages/ui/src/components/SearchInput/index.tsx
+++ b/packages/ui/src/components/SearchInput/index.tsx
@@ -287,7 +287,7 @@ export const SearchInput = forwardRef(
label={label}
placeholder={placeholder}
loading={loading}
- onChange={onSearchCallback}
+ onChange={event => onSearchCallback(event.target.value)}
clearable
disabled={disabled}
className={className}
diff --git a/packages/ui/src/components/SelectInputV2/SearchBarDropdown.tsx b/packages/ui/src/components/SelectInputV2/SearchBarDropdown.tsx
index a31196ce7a..5f5de52583 100644
--- a/packages/ui/src/components/SelectInputV2/SearchBarDropdown.tsx
+++ b/packages/ui/src/components/SelectInputV2/SearchBarDropdown.tsx
@@ -143,7 +143,7 @@ export const SearchBarDropdown = ({
return (
handleChange(event)}
+ onChange={event => handleChange(event.target.value)}
placeholder={placeholder}
onFocus={() => setSearchBarActive(true)}
onBlur={() => setSearchBarActive(false)}
diff --git a/packages/ui/src/components/TextInputV2/__stories__/ControlledVSUncontrolled.stories.tsx b/packages/ui/src/components/TextInputV2/__stories__/ControlledVSUncontrolled.stories.tsx
new file mode 100644
index 0000000000..e2b9ff5b94
--- /dev/null
+++ b/packages/ui/src/components/TextInputV2/__stories__/ControlledVSUncontrolled.stories.tsx
@@ -0,0 +1,38 @@
+import type { StoryFn } from '@storybook/react'
+import { useState } from 'react'
+import { TextInputV2 } from '..'
+import { Stack } from '../../Stack'
+import { Text } from '../../Text'
+
+export const ControlledVSUncontrolled: StoryFn = props => {
+ const [value, setValue] = useState('content')
+
+ return (
+
+
+
+ setValue(event.target.value)}
+ {...props}
+ />
+
+ We can get the value from the input, which is:{' '}
+
+ {value}
+
+
+
+
+ )
+}
+
+ControlledVSUncontrolled.parameters = {
+ docs: {
+ description: {
+ story:
+ 'The component can be controlled or uncontrolled.\n\n The difference is that in the controlled version, the `value` and `onChange` is passed as a prop and the component does not manage its own state.\n\n In the uncontrolled version, the component manages its own state. For more information check [React documentation](https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components).',
+ },
+ },
+}
diff --git a/packages/ui/src/components/TextInputV2/__stories__/Examples.stories.tsx b/packages/ui/src/components/TextInputV2/__stories__/Examples.stories.tsx
index 9dcd3c8a4b..fd92caa742 100644
--- a/packages/ui/src/components/TextInputV2/__stories__/Examples.stories.tsx
+++ b/packages/ui/src/components/TextInputV2/__stories__/Examples.stories.tsx
@@ -16,14 +16,14 @@ export const Examples: StoryFn = args => {
label="With prefix"
prefix="https://"
value={value}
- onChange={setValue}
+ onChange={event => setValue(event.target.value)}
/>
setValue(event.target.value)}
onRandomize={() => setValue(randomName())}
/>
= args => {
label="Password input with random hook"
prefix="https://"
value={value}
- onChange={setValue}
+ onChange={event => setValue(event.target.value)}
onRandomize={() => setValue(randomName())}
type="password"
/>
@@ -41,7 +41,7 @@ export const Examples: StoryFn = args => {
prefix="https://"
suffix=".com"
value={value}
- onChange={setValue}
+ onChange={event => setValue(event.target.value)}
onRandomize={() => setValue(randomName())}
success="Field has been updated!"
loading
@@ -53,7 +53,7 @@ export const Examples: StoryFn = args => {
disabled
helper="Notice to fill the field"
value={value}
- onChange={setValue}
+ onChange={event => setValue(event.target.value)}
onRandomize={() => setValue(randomName())}
loading
/>
@@ -69,7 +69,7 @@ export const Examples: StoryFn = args => {
}
value={value}
- onChange={setValue}
+ onChange={event => setValue(event.target.value)}
onRandomize={() => setValue(randomName())}
loading
/>
diff --git a/packages/ui/src/components/TextInputV2/__stories__/OnRandomize.stories.tsx b/packages/ui/src/components/TextInputV2/__stories__/OnRandomize.stories.tsx
index a71d566ac2..465cd73e5b 100644
--- a/packages/ui/src/components/TextInputV2/__stories__/OnRandomize.stories.tsx
+++ b/packages/ui/src/components/TextInputV2/__stories__/OnRandomize.stories.tsx
@@ -9,7 +9,7 @@ export const OnRandomize: StoryFn = ({ ...args }) => {
setValue(event.target.value)}
onRandomize={() => {
setValue(`randomValue-${Math.round(Math.random() * 1000)}`)
}}
diff --git a/packages/ui/src/components/TextInputV2/__stories__/Size.stories.tsx b/packages/ui/src/components/TextInputV2/__stories__/Size.stories.tsx
index d71e240d93..9940cf3fcd 100644
--- a/packages/ui/src/components/TextInputV2/__stories__/Size.stories.tsx
+++ b/packages/ui/src/components/TextInputV2/__stories__/Size.stories.tsx
@@ -15,10 +15,11 @@ export const Size: StoryFn = args => {
).map(size => (
setValue(event.target.value)}
placeholder="Placeholder"
/>
))}
diff --git a/packages/ui/src/components/TextInputV2/__stories__/Template.stories.tsx b/packages/ui/src/components/TextInputV2/__stories__/Template.stories.tsx
index 79b1e72c0f..f07d6c54bb 100644
--- a/packages/ui/src/components/TextInputV2/__stories__/Template.stories.tsx
+++ b/packages/ui/src/components/TextInputV2/__stories__/Template.stories.tsx
@@ -5,7 +5,13 @@ import { TextInputV2 } from '..'
export const Template: StoryFn = ({ ...args }) => {
const [value, setValue] = useState(args.value)
- return
+ return (
+ setValue(event.target.value)}
+ />
+ )
}
Template.args = {
diff --git a/packages/ui/src/components/TextInputV2/__stories__/index.stories.tsx b/packages/ui/src/components/TextInputV2/__stories__/index.stories.tsx
index 61ff4c20da..ad6c7d56f6 100644
--- a/packages/ui/src/components/TextInputV2/__stories__/index.stories.tsx
+++ b/packages/ui/src/components/TextInputV2/__stories__/index.stories.tsx
@@ -19,4 +19,5 @@ export { ReadOnly } from './ReadOnly.stories'
export { Loading } from './Loading.stories'
export { Success } from './Success.stories'
export { Error } from './Error.stories'
+export { ControlledVSUncontrolled } from './ControlledVSUncontrolled.stories'
export { Examples } from './Examples.stories'
diff --git a/packages/ui/src/components/TextInputV2/__tests__/index.test.tsx b/packages/ui/src/components/TextInputV2/__tests__/index.test.tsx
index 04e9a66748..b4d41dd050 100644
--- a/packages/ui/src/components/TextInputV2/__tests__/index.test.tsx
+++ b/packages/ui/src/components/TextInputV2/__tests__/index.test.tsx
@@ -12,16 +12,22 @@ describe('TextInputV2', () => {
test('should control the value', () => {
const onChange = vi.fn()
+ const onChangeValue = vi.fn()
renderWithTheme(
- ,
+ ,
)
const textarea = screen.getByLabelText('Test')
expect(textarea.value).toBe('test')
- // userEvent.type do not work here at the moment
fireEvent.change(textarea, { target: { value: 'another value' } })
- expect(onChange).toHaveBeenCalledWith('another value')
+ expect(onChange).toHaveBeenCalled()
+ expect(onChangeValue).toHaveBeenCalledWith('another value')
})
test('should be clearable', async () => {
@@ -35,7 +41,10 @@ describe('TextInputV2', () => {
expect(textarea.value).toBe('test')
const clearableButton = screen.getByLabelText('clear value')
await userEvent.click(clearableButton)
- expect(onChange).toHaveBeenCalledWith('')
+ expect(onChange).toHaveBeenCalledWith({
+ target: { value: '' },
+ currentTarget: { value: '' },
+ })
})
test('should render correctly when input is disabled', () =>
diff --git a/packages/ui/src/components/TextInputV2/index.tsx b/packages/ui/src/components/TextInputV2/index.tsx
index 154d71abf1..cff4d7e675 100644
--- a/packages/ui/src/components/TextInputV2/index.tsx
+++ b/packages/ui/src/components/TextInputV2/index.tsx
@@ -1,7 +1,20 @@
import styled from '@emotion/styled'
import { Icon } from '@ultraviolet/icons'
-import type { InputHTMLAttributes, ReactNode } from 'react'
-import { forwardRef, useId, useMemo, useState } from 'react'
+import type {
+ ChangeEvent,
+ ChangeEventHandler,
+ InputHTMLAttributes,
+ ReactNode,
+} from 'react'
+import {
+ forwardRef,
+ useCallback,
+ useId,
+ useImperativeHandle,
+ useMemo,
+ useRef,
+ useState,
+} from 'react'
import { Button } from '../Button'
import { Loader } from '../Loader'
import { Stack } from '../Stack'
@@ -129,7 +142,6 @@ type TextInputProps = {
minLength?: number
maxLength?: number
onRandomize?: () => void
- onChange?: (newValue: string) => void
prefix?: ReactNode
size?: TextInputSize
success?: string | boolean
@@ -137,6 +149,8 @@ type TextInputProps = {
tooltip?: string
type?: 'text' | 'password' | 'url' | 'email'
value?: string
+ defaultValue?: string
+ onChangeValue?: (value: string) => void
} & Pick<
InputHTMLAttributes,
| 'onFocus'
@@ -156,10 +170,13 @@ type TextInputProps = {
| 'role'
| 'aria-live'
| 'aria-atomic'
+ | 'onChange'
>
/**
- * This component offers an extended input HTML
+ * This component offers an extended input HTML. The component can be controlled or uncontrolled.
+ * To control the component, you need to pass the value and the `onChange` function.
+ * If you don't pass the `onChange` function, the component will be uncontrolled and you can set the default value using `defaultValue`
*/
export const TextInputV2 = forwardRef(
(
@@ -169,6 +186,7 @@ export const TextInputV2 = forwardRef(
tabIndex,
value,
onChange,
+ onChangeValue,
placeholder,
disabled = false,
readOnly = false,
@@ -200,11 +218,14 @@ export const TextInputV2 = forwardRef(
role,
'aria-live': ariaLive,
'aria-atomic': ariaAtomic,
+ defaultValue,
},
ref,
) => {
const localId = useId()
const [hasFocus, setHasFocus] = useState(false)
+ const inputRef = useRef(null)
+ useImperativeHandle(ref, () => inputRef.current as HTMLInputElement)
const [isPasswordVisible, setIsPasswordVisible] = useState(false)
const computedType =
@@ -222,6 +243,14 @@ export const TextInputV2 = forwardRef(
return 'neutral'
}, [error, success])
+ const onChangeCallback: ChangeEventHandler = useCallback(
+ event => {
+ onChange?.(event)
+ onChangeValue?.(event.target.value)
+ },
+ [onChange, onChangeValue],
+ )
+
const computedClearable = clearable && !!value
return (
@@ -246,7 +275,7 @@ export const TextInputV2 = forwardRef(
{label}
{required ? (
-
+
) : null}
) : null}
@@ -286,11 +315,10 @@ export const TextInputV2 = forwardRef(
tabIndex={tabIndex}
autoFocus={autoFocus}
disabled={disabled}
- ref={ref}
- value={value === null || value === undefined ? '' : value}
- onChange={event => {
- onChange?.(event.currentTarget.value)
- }}
+ ref={inputRef}
+ value={value}
+ defaultValue={defaultValue}
+ onChange={onChangeCallback}
data-size={size}
placeholder={placeholder}
data-testid={dataTestId}
@@ -322,7 +350,13 @@ export const TextInputV2 = forwardRef(
size={size === 'small' ? 'xsmall' : 'small'}
icon="close"
onClick={() => {
- onChange?.('')
+ if (inputRef?.current) {
+ inputRef.current.value = ''
+ onChangeCallback({
+ target: { value: '' },
+ currentTarget: { value: '' },
+ } as ChangeEvent)
+ }
}}
sentiment="neutral"
/>