Fix displayValue syncing problem (#1755)
* ensure `syncInputValue` is updated correctly * WIP * WIP * Don’t resync on open * Fix react value syncing update * Add comment * Port new setup over to Vue * Remove `inputPropsRef` We hardly knew ye * Remove repro * Cleanup * Update changelog Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
@@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Don’t close dialog when drag ends outside dialog ([#1667](https://github.com/tailwindlabs/headlessui/pull/1667))
|
||||
- Fix outside clicks to close dialog when nested, unopened dialogs are present ([#1667](https://github.com/tailwindlabs/headlessui/pull/1667))
|
||||
- Close `Menu` component when using `tab` key ([#1673](https://github.com/tailwindlabs/headlessui/pull/1673))
|
||||
- Resync input when display value changes ([#1679](https://github.com/tailwindlabs/headlessui/pull/1679))
|
||||
- Resync input when display value changes ([#1679](https://github.com/tailwindlabs/headlessui/pull/1679), [#1755](https://github.com/tailwindlabs/headlessui/pull/1755))
|
||||
- Ensure controlled `Tabs` don't change automagically ([#1680](https://github.com/tailwindlabs/headlessui/pull/1680))
|
||||
- Don't scroll lock when a Transition + Dialog is mounted but hidden ([#1681](https://github.com/tailwindlabs/headlessui/pull/1681))
|
||||
- Improve outside click on Safari iOS ([#1712](https://github.com/tailwindlabs/headlessui/pull/1712))
|
||||
|
||||
@@ -427,38 +427,24 @@ describe('Rendering', () => {
|
||||
suppressConsoleLogs(async () => {
|
||||
function Example() {
|
||||
let [value, setValue] = useState(null)
|
||||
let [suffix, setSuffix] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Combobox value={value} onChange={setValue} nullable>
|
||||
{({ open }) => {
|
||||
if (!open) {
|
||||
return (
|
||||
<>
|
||||
<Combobox.Input
|
||||
onChange={NOOP}
|
||||
displayValue={(str?: string) => `${str?.toUpperCase() ?? ''} closed`}
|
||||
/>
|
||||
<Combobox.Button>Trigger</Combobox.Button>
|
||||
</>
|
||||
)
|
||||
<Combobox.Input
|
||||
onChange={NOOP}
|
||||
displayValue={(str?: string) =>
|
||||
`${str?.toUpperCase() ?? ''} ${suffix ? 'with suffix' : 'no suffix'}`
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Combobox.Input
|
||||
onChange={NOOP}
|
||||
displayValue={(str?: string) => `${str?.toUpperCase() ?? ''} open`}
|
||||
/>
|
||||
<Combobox.Button>Trigger</Combobox.Button>
|
||||
<Combobox.Options>
|
||||
<Combobox.Option value="a">Option A</Combobox.Option>
|
||||
<Combobox.Option value="b">Option B</Combobox.Option>
|
||||
<Combobox.Option value="c">Option C</Combobox.Option>
|
||||
</Combobox.Options>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Combobox.Button>Trigger</Combobox.Button>
|
||||
<Combobox.Options>
|
||||
<Combobox.Option value="a">Option A</Combobox.Option>
|
||||
<Combobox.Option value="b">Option B</Combobox.Option>
|
||||
<Combobox.Option value="c">Option C</Combobox.Option>
|
||||
</Combobox.Options>
|
||||
<button onClick={() => setSuffix((v) => !v)}>Toggle suffix</button>
|
||||
</Combobox>
|
||||
</>
|
||||
)
|
||||
@@ -466,23 +452,27 @@ describe('Rendering', () => {
|
||||
|
||||
render(<Example />)
|
||||
|
||||
expect(getComboboxInput()).toHaveValue(' closed')
|
||||
expect(getComboboxInput()).toHaveValue(' no suffix')
|
||||
|
||||
await click(getComboboxButton())
|
||||
|
||||
assertComboboxList({ state: ComboboxState.Visible })
|
||||
|
||||
expect(getComboboxInput()).toHaveValue(' open')
|
||||
expect(getComboboxInput()).toHaveValue(' no suffix')
|
||||
|
||||
await click(getComboboxOptions()[1])
|
||||
|
||||
expect(getComboboxInput()).toHaveValue('B closed')
|
||||
expect(getComboboxInput()).toHaveValue('B no suffix')
|
||||
|
||||
await click(getByText('Toggle suffix'))
|
||||
|
||||
expect(getComboboxInput()).toHaveValue('B no suffix') // No re-sync yet
|
||||
|
||||
await click(getComboboxButton())
|
||||
|
||||
assertComboboxList({ state: ComboboxState.Visible })
|
||||
expect(getComboboxInput()).toHaveValue('B no suffix') // No re-sync yet
|
||||
|
||||
expect(getComboboxInput()).toHaveValue('B open')
|
||||
await click(getComboboxOptions()[0])
|
||||
|
||||
expect(getComboboxInput()).toHaveValue('A with suffix')
|
||||
})
|
||||
)
|
||||
|
||||
@@ -510,6 +500,58 @@ describe('Rendering', () => {
|
||||
expect(getComboboxInput()).toHaveAttribute('type', 'search')
|
||||
})
|
||||
)
|
||||
|
||||
xit(
|
||||
'should reflect the value in the input when the value changes and when you are typing',
|
||||
suppressConsoleLogs(async () => {
|
||||
function Example() {
|
||||
let [value, setValue] = useState('bob')
|
||||
let [_query, setQuery] = useState('')
|
||||
|
||||
return (
|
||||
<Combobox value={value} onChange={setValue}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Combobox.Input
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
displayValue={(person) => `${person ?? ''} - ${open ? 'open' : 'closed'}`}
|
||||
/>
|
||||
|
||||
<Combobox.Button />
|
||||
|
||||
<Combobox.Options>
|
||||
<Combobox.Option value="alice">alice</Combobox.Option>
|
||||
<Combobox.Option value="bob">bob</Combobox.Option>
|
||||
<Combobox.Option value="charlie">charlie</Combobox.Option>
|
||||
</Combobox.Options>
|
||||
</>
|
||||
)}
|
||||
</Combobox>
|
||||
)
|
||||
}
|
||||
|
||||
render(<Example />)
|
||||
|
||||
// Check for proper state sync
|
||||
expect(getComboboxInput()).toHaveValue('bob - closed')
|
||||
await click(getComboboxButton())
|
||||
expect(getComboboxInput()).toHaveValue('bob - open')
|
||||
await click(getComboboxButton())
|
||||
expect(getComboboxInput()).toHaveValue('bob - closed')
|
||||
|
||||
// Check if we can still edit the input
|
||||
for (let _ of Array(' - closed'.length)) {
|
||||
await press(Keys.Backspace, getComboboxInput())
|
||||
}
|
||||
getComboboxInput()?.select()
|
||||
await type(word('alice'), getComboboxInput())
|
||||
expect(getComboboxInput()).toHaveValue('alice')
|
||||
|
||||
// Open the combobox and choose an option
|
||||
await click(getComboboxOptions()[2])
|
||||
expect(getComboboxInput()).toHaveValue('charlie - closed')
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe('Combobox.Label', () => {
|
||||
|
||||
@@ -41,6 +41,7 @@ import { useOpenClosed, State, OpenClosedProvider } from '../../internal/open-cl
|
||||
|
||||
import { Keys } from '../keyboard'
|
||||
import { useControllable } from '../../hooks/use-controllable'
|
||||
import { useWatch } from '../../hooks/use-watch'
|
||||
|
||||
enum ComboboxState {
|
||||
Open,
|
||||
@@ -262,9 +263,6 @@ let ComboboxDataContext = createContext<
|
||||
isSelected(value: unknown): boolean
|
||||
__demoMode: boolean
|
||||
|
||||
inputPropsRef: MutableRefObject<{
|
||||
displayValue?(item: unknown): string
|
||||
}>
|
||||
optionsPropsRef: MutableRefObject<{
|
||||
static: boolean
|
||||
hold: boolean
|
||||
@@ -352,7 +350,6 @@ let ComboboxRoot = forwardRefWithAs(function Combobox<
|
||||
let defaultToFirstOption = useRef(false)
|
||||
|
||||
let optionsPropsRef = useRef<_Data['optionsPropsRef']['current']>({ static: false, hold: false })
|
||||
let inputPropsRef = useRef<_Data['inputPropsRef']['current']>({ displayValue: undefined })
|
||||
|
||||
let labelRef = useRef<_Data['labelRef']['current']>(null)
|
||||
let inputRef = useRef<_Data['inputRef']['current']>(null)
|
||||
@@ -382,7 +379,6 @@ let ComboboxRoot = forwardRefWithAs(function Combobox<
|
||||
() => ({
|
||||
...state,
|
||||
optionsPropsRef,
|
||||
inputPropsRef,
|
||||
labelRef,
|
||||
inputRef,
|
||||
buttonRef,
|
||||
@@ -440,32 +436,17 @@ let ComboboxRoot = forwardRefWithAs(function Combobox<
|
||||
[data, disabled, value]
|
||||
)
|
||||
|
||||
let syncInputValue = useCallback(() => {
|
||||
if (!data.inputRef.current) return
|
||||
let displayValue = inputPropsRef.current.displayValue
|
||||
|
||||
if (typeof displayValue === 'function') {
|
||||
data.inputRef.current.value = displayValue(value) ?? ''
|
||||
} else if (typeof value === 'string') {
|
||||
data.inputRef.current.value = value
|
||||
} else {
|
||||
data.inputRef.current.value = ''
|
||||
}
|
||||
}, [value, data.inputRef, inputPropsRef.current?.displayValue])
|
||||
|
||||
let selectOption = useEvent((id: string) => {
|
||||
let option = data.options.find((item) => item.id === id)
|
||||
if (!option) return
|
||||
|
||||
onChange(option.dataRef.current.value)
|
||||
syncInputValue()
|
||||
})
|
||||
|
||||
let selectActiveOption = useEvent(() => {
|
||||
if (data.activeOptionIndex !== null) {
|
||||
let { dataRef, id } = data.options[data.activeOptionIndex]
|
||||
onChange(dataRef.current.value)
|
||||
syncInputValue()
|
||||
|
||||
// It could happen that the `activeOptionIndex` stored in state is actually null,
|
||||
// but we are getting the fallback active option back instead.
|
||||
@@ -531,13 +512,6 @@ let ComboboxRoot = forwardRefWithAs(function Combobox<
|
||||
[]
|
||||
)
|
||||
|
||||
useIsoMorphicEffect(() => {
|
||||
if (data.comboboxState !== ComboboxState.Closed) return
|
||||
syncInputValue()
|
||||
}, [syncInputValue, data.comboboxState])
|
||||
|
||||
// Ensure that we update the inputRef if the value changes
|
||||
useIsoMorphicEffect(syncInputValue, [syncInputValue])
|
||||
let ourProps = ref === null ? {} : { ref }
|
||||
|
||||
return (
|
||||
@@ -612,14 +586,33 @@ let Input = forwardRefWithAs(function Input<
|
||||
let actions = useActions('Combobox.Input')
|
||||
|
||||
let inputRef = useSyncRefs(data.inputRef, ref)
|
||||
let inputPropsRef = data.inputPropsRef
|
||||
|
||||
let id = `headlessui-combobox-input-${useId()}`
|
||||
let d = useDisposables()
|
||||
|
||||
useIsoMorphicEffect(() => {
|
||||
inputPropsRef.current.displayValue = displayValue
|
||||
}, [displayValue, inputPropsRef])
|
||||
let currentValue = useMemo(() => {
|
||||
if (typeof displayValue === 'function') {
|
||||
return displayValue(data.value as unknown as TType) ?? ''
|
||||
} else if (typeof data.value === 'string') {
|
||||
return data.value
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
|
||||
// displayValue is intentionally left out
|
||||
}, [data.value])
|
||||
|
||||
useWatch(
|
||||
([currentValue, state], [oldCurrentValue, oldState]) => {
|
||||
if (!data.inputRef.current) return
|
||||
if (oldState === ComboboxState.Open && state === ComboboxState.Closed) {
|
||||
data.inputRef.current.value = currentValue
|
||||
} else if (currentValue !== oldCurrentValue) {
|
||||
data.inputRef.current.value = currentValue
|
||||
}
|
||||
},
|
||||
[currentValue, data.comboboxState]
|
||||
)
|
||||
|
||||
let handleKeyDown = useEvent((event: ReactKeyboardEvent<HTMLInputElement>) => {
|
||||
switch (event.key) {
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEvent } from './use-event'
|
||||
|
||||
export function useWatch<T>(cb: (values: T[]) => void | (() => void), dependencies: T[]) {
|
||||
let track = useRef<typeof dependencies>([])
|
||||
export function useWatch<T extends any[]>(
|
||||
cb: (newValues: [...T], oldValues: [...T]) => void | (() => void),
|
||||
dependencies: [...T]
|
||||
) {
|
||||
let track = useRef<typeof dependencies>([] as unknown as [...T])
|
||||
let action = useEvent(cb)
|
||||
|
||||
useEffect(() => {
|
||||
let oldValues = [...track.current] as unknown as [...T]
|
||||
|
||||
for (let [idx, value] of dependencies.entries()) {
|
||||
if (track.current[idx] !== value) {
|
||||
// At least 1 item changed
|
||||
let returnValue = action(dependencies)
|
||||
let returnValue = action(dependencies, oldValues)
|
||||
track.current = dependencies
|
||||
return returnValue
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Don’t close dialog when drag ends outside dialog ([#1667](https://github.com/tailwindlabs/headlessui/pull/1667))
|
||||
- Fix outside clicks to close dialog when nested, unopened dialogs are present ([#1667](https://github.com/tailwindlabs/headlessui/pull/1667))
|
||||
- Close `Menu` component when using `tab` key ([#1673](https://github.com/tailwindlabs/headlessui/pull/1673))
|
||||
- Resync input when display value changes ([#1679](https://github.com/tailwindlabs/headlessui/pull/1679))
|
||||
- Resync input when display value changes ([#1679](https://github.com/tailwindlabs/headlessui/pull/1679), [#1755](https://github.com/tailwindlabs/headlessui/pull/1755))
|
||||
- Ensure controlled `Tabs` don't change automagically ([#1680](https://github.com/tailwindlabs/headlessui/pull/1680))
|
||||
- Improve outside click on Safari iOS ([#1712](https://github.com/tailwindlabs/headlessui/pull/1712))
|
||||
- Improve event handler merging ([#1715](https://github.com/tailwindlabs/headlessui/pull/1715))
|
||||
|
||||
@@ -389,7 +389,7 @@ describe('Rendering', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Combobox.Input', () => {
|
||||
describe('ComboboxInput', () => {
|
||||
it(
|
||||
'selecting an option puts the value into Combobox.Input when displayValue is not provided',
|
||||
suppressConsoleLogs(async () => {
|
||||
@@ -454,53 +454,51 @@ describe('Rendering', () => {
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'conditionally rendering the input should allow changing the display value',
|
||||
suppressConsoleLogs(async () => {
|
||||
let Example = defineComponent({
|
||||
template: html`
|
||||
<Combobox v-model="value" v-slot="{ open }" nullable>
|
||||
<template v-if="!open">
|
||||
<ComboboxInput :displayValue="(str) => (str?.toUpperCase() ?? '') + ' closed'" />
|
||||
<ComboboxButton>Trigger</ComboboxButton>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ComboboxInput :displayValue="(str) => (str?.toUpperCase() ?? '') + ' open'" />
|
||||
<ComboboxButton>Trigger</ComboboxButton>
|
||||
<ComboboxOptions>
|
||||
<ComboboxOption value="a">Option A</ComboboxOption>
|
||||
<ComboboxOption value="b">Option B</ComboboxOption>
|
||||
<ComboboxOption value="c">Option C</ComboboxOption>
|
||||
</ComboboxOptions>
|
||||
</template>
|
||||
</Combobox>
|
||||
`,
|
||||
setup: () => ({ value: ref(null) }),
|
||||
})
|
||||
|
||||
renderTemplate(Example)
|
||||
|
||||
await nextFrame()
|
||||
|
||||
expect(getComboboxInput()).toHaveValue(' closed')
|
||||
|
||||
await click(getComboboxButton())
|
||||
|
||||
assertComboboxList({ state: ComboboxState.Visible })
|
||||
|
||||
expect(getComboboxInput()).toHaveValue(' open')
|
||||
|
||||
await click(getComboboxOptions()[1])
|
||||
|
||||
expect(getComboboxInput()).toHaveValue('B closed')
|
||||
|
||||
await click(getComboboxButton())
|
||||
|
||||
assertComboboxList({ state: ComboboxState.Visible })
|
||||
|
||||
expect(getComboboxInput()).toHaveValue('B open')
|
||||
it('conditionally rendering the input should allow changing the display value', async () => {
|
||||
let Example = defineComponent({
|
||||
template: html`
|
||||
<Combobox v-model="value" v-slot="{ open }" nullable>
|
||||
<ComboboxInput
|
||||
:displayValue="(str) => (str?.toUpperCase() ?? '') + (suffix ? ' with suffix' : ' no suffix')"
|
||||
/>
|
||||
<ComboboxButton>Trigger</ComboboxButton>
|
||||
<ComboboxOptions>
|
||||
<ComboboxOption value="a">Option A</ComboboxOption>
|
||||
<ComboboxOption value="b">Option B</ComboboxOption>
|
||||
<ComboboxOption value="c">Option C</ComboboxOption>
|
||||
</ComboboxOptions>
|
||||
<button @click="suffix = !suffix">Toggle suffix</button>
|
||||
</Combobox>
|
||||
`,
|
||||
setup: () => ({ value: ref(null), suffix: ref(false) }),
|
||||
})
|
||||
)
|
||||
|
||||
renderTemplate(Example)
|
||||
|
||||
await nextFrame()
|
||||
|
||||
expect(getComboboxInput()).toHaveValue(' no suffix')
|
||||
|
||||
await click(getComboboxButton())
|
||||
|
||||
expect(getComboboxInput()).toHaveValue(' no suffix')
|
||||
|
||||
await click(getComboboxOptions()[1])
|
||||
|
||||
expect(getComboboxInput()).toHaveValue('B no suffix')
|
||||
|
||||
await click(getByText('Toggle suffix'))
|
||||
|
||||
expect(getComboboxInput()).toHaveValue('B no suffix') // No re-sync yet
|
||||
|
||||
await click(getComboboxButton())
|
||||
|
||||
expect(getComboboxInput()).toHaveValue('B no suffix') // No re-sync yet
|
||||
|
||||
await click(getComboboxOptions()[0])
|
||||
|
||||
expect(getComboboxInput()).toHaveValue('A with suffix')
|
||||
})
|
||||
|
||||
it(
|
||||
'should be possible to override the `type` on the input',
|
||||
@@ -525,6 +523,54 @@ describe('Rendering', () => {
|
||||
expect(getComboboxInput()).toHaveAttribute('type', 'search')
|
||||
})
|
||||
)
|
||||
|
||||
xit(
|
||||
'should reflect the value in the input when the value changes and when you are typing',
|
||||
suppressConsoleLogs(async () => {
|
||||
renderTemplate({
|
||||
template: html`
|
||||
<Combobox v-model="value" v-slot="{ open }">
|
||||
<ComboboxInput :displayValue="person => displayValue(person, open)" />
|
||||
|
||||
<ComboboxButton />
|
||||
|
||||
<ComboboxOptions>
|
||||
<ComboboxOption value="alice">alice</ComboboxOption>
|
||||
<ComboboxOption value="bob">bob</ComboboxOption>
|
||||
<ComboboxOption value="charlie">charlie</ComboboxOption>
|
||||
</ComboboxOptions>
|
||||
</Combobox>
|
||||
`,
|
||||
setup: () => ({
|
||||
value: ref('bob'),
|
||||
displayValue(person: string, open: boolean) {
|
||||
return `${person ?? ''} - ${open ? 'open' : 'closed'}`
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
await nextFrame()
|
||||
|
||||
// Check for proper state sync
|
||||
expect(getComboboxInput()).toHaveValue('bob - closed')
|
||||
await click(getComboboxButton())
|
||||
expect(getComboboxInput()).toHaveValue('bob - open')
|
||||
await click(getComboboxButton())
|
||||
expect(getComboboxInput()).toHaveValue('bob - closed')
|
||||
|
||||
// Check if we can still edit the input
|
||||
for (let _ of Array(' - closed'.length)) {
|
||||
await press(Keys.Backspace, getComboboxInput())
|
||||
}
|
||||
getComboboxInput()?.select()
|
||||
await type(word('alice'), getComboboxInput())
|
||||
expect(getComboboxInput()).toHaveValue('alice')
|
||||
|
||||
// Open the combobox and choose an option
|
||||
await click(getComboboxOptions()[2])
|
||||
expect(getComboboxInput()).toHaveValue('charlie - closed')
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe('ComboboxLabel', () => {
|
||||
|
||||
@@ -70,7 +70,6 @@ type StateDefinition = {
|
||||
|
||||
compare: (a: unknown, z: unknown) => boolean
|
||||
|
||||
inputPropsRef: Ref<{ displayValue?: (item: unknown) => string }>
|
||||
optionsPropsRef: Ref<{ static: boolean; hold: boolean }>
|
||||
|
||||
labelRef: Ref<HTMLLabelElement | null>
|
||||
@@ -217,7 +216,6 @@ export let Combobox = defineComponent({
|
||||
return activeOptionIndex.value
|
||||
}),
|
||||
activationTrigger,
|
||||
inputPropsRef: ref<StateDefinition['inputPropsRef']['value']>({ displayValue: undefined }),
|
||||
optionsPropsRef,
|
||||
closeCombobox() {
|
||||
defaultToFirstOption.value = false
|
||||
@@ -296,19 +294,6 @@ export let Combobox = defineComponent({
|
||||
activationTrigger.value = trigger ?? ActivationTrigger.Other
|
||||
options.value = adjustedState.options
|
||||
},
|
||||
syncInputValue() {
|
||||
let value = api.value.value
|
||||
if (!dom(api.inputRef)) return
|
||||
let displayValue = api.inputPropsRef.value.displayValue
|
||||
|
||||
if (typeof displayValue === 'function') {
|
||||
api.inputRef!.value!.value = displayValue(value) ?? ''
|
||||
} else if (typeof value === 'string') {
|
||||
api.inputRef!.value!.value = value
|
||||
} else {
|
||||
api.inputRef!.value!.value = ''
|
||||
}
|
||||
},
|
||||
selectOption(id: string) {
|
||||
let option = options.value.find((item) => item.id === id)
|
||||
if (!option) return
|
||||
@@ -332,7 +317,6 @@ export let Combobox = defineComponent({
|
||||
},
|
||||
})
|
||||
)
|
||||
api.syncInputValue()
|
||||
},
|
||||
selectActiveOption() {
|
||||
if (api.activeOptionIndex.value === null) return
|
||||
@@ -356,7 +340,6 @@ export let Combobox = defineComponent({
|
||||
},
|
||||
})
|
||||
)
|
||||
api.syncInputValue()
|
||||
|
||||
// It could happen that the `activeOptionIndex` stored in state is actually null,
|
||||
// but we are getting the fallback active option back instead.
|
||||
@@ -406,26 +389,6 @@ export let Combobox = defineComponent({
|
||||
computed(() => comboboxState.value === ComboboxStates.Open)
|
||||
)
|
||||
|
||||
watch([api.value, api.inputRef, api.inputPropsRef], () => api.syncInputValue(), {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
})
|
||||
|
||||
// Only sync the input value on close as typing into the input will trigger it to open
|
||||
// causing a resync of the input value with the currently stored, stale value that is
|
||||
// one character behind since the input's value has just been updated by the browser
|
||||
watch(
|
||||
api.comboboxState,
|
||||
(state) => {
|
||||
if (state === ComboboxStates.Closed) {
|
||||
api.syncInputValue()
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
)
|
||||
|
||||
// @ts-expect-error Types of property 'dataRef' are incompatible.
|
||||
provide(ComboboxContext, api)
|
||||
useOpenClosedProvider(
|
||||
@@ -647,12 +610,44 @@ export let ComboboxInput = defineComponent({
|
||||
let api = useComboboxContext('ComboboxInput')
|
||||
let id = `headlessui-combobox-input-${useId()}`
|
||||
|
||||
watchEffect(() => {
|
||||
api.inputPropsRef.value = props
|
||||
})
|
||||
|
||||
expose({ el: api.inputRef, $el: api.inputRef })
|
||||
|
||||
let currentValue = ref(api.value.value as unknown as string)
|
||||
|
||||
let getCurrentValue = () => {
|
||||
let value = api.value.value
|
||||
if (!dom(api.inputRef)) return ''
|
||||
|
||||
if (typeof props.displayValue !== 'undefined') {
|
||||
return props.displayValue(value as unknown) ?? ''
|
||||
} else if (typeof value === 'string') {
|
||||
return value
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
watch([api.value], () => (currentValue.value = getCurrentValue()), {
|
||||
flush: 'sync',
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
watch(
|
||||
[currentValue, api.comboboxState],
|
||||
([currentValue, state], [oldCurrentValue, oldState]) => {
|
||||
let input = dom(api.inputRef)
|
||||
if (!input) return
|
||||
if (oldState === ComboboxStates.Open && state === ComboboxStates.Closed) {
|
||||
input.value = currentValue
|
||||
} else if (currentValue !== oldCurrentValue) {
|
||||
input.value = currentValue
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
})
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
switch (event.key) {
|
||||
// Ref: https://www.w3.org/TR/wai-aria-practices-1.2/#keyboard-interaction-12
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { computed, ComputedRef, ref } from 'vue'
|
||||
import { computed, ComputedRef, UnwrapRef, ref } from 'vue'
|
||||
|
||||
export function useControllable<T>(
|
||||
controlledValue: ComputedRef<T | undefined>,
|
||||
@@ -14,7 +14,7 @@ export function useControllable<T>(
|
||||
if (isControlled.value) {
|
||||
return onChange?.(value as T)
|
||||
} else {
|
||||
internalValue.value = value as T
|
||||
internalValue.value = value as UnwrapRef<T>
|
||||
return onChange?.(value as T)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,8 +8,6 @@ import {
|
||||
InjectionKey,
|
||||
Ref,
|
||||
watch,
|
||||
ref,
|
||||
onBeforeUnmount,
|
||||
} from 'vue'
|
||||
|
||||
type OnUpdate = (message: StackMessage, type: string, element: Ref<HTMLElement | null>) => void
|
||||
|
||||
Reference in New Issue
Block a user