7bb89871ba
* implement `objetToFormEntries` functionality
If we are working with more complex data structures then we have to
encode those data structures into a syntax that the HTML can understand.
This means that we have to use `<input type="hidden" name="..." value="...">` syntax.
To convert a simple array we can use the following syntax:
```js
// Assuming we have a `name` of `person`
let input = ['Alice', 'Bob', 'Charlie']
```
Results in:
```html
<input type="hidden" name="person[]" value="Alice" />
<input type="hidden" name="person[]" value="Bob" />
<input type="hidden" name="person[]" value="Charlie" />
```
Note: the additional `[]` in the name attribute.
---
A more complex object (even deeply nested) can be encoded like this:
```js
// Assuming we have a `name` of `person`
let input = {
id: 1,
name: {
first: 'Jane',
last: 'Doe'
}
}
```
Results in:
```html
<input type="hidden" name="person[id]" value="1" />
<input type="hidden" name="person[name][first]" value="Jane" />
<input type="hidden" name="person[name][last]" value="Doe" />
```
* implement VisuallyHidden component
* implement and export some extra helper utilities
* implement form element for Switch
* implement form element for Combobox
* implement form element for RadioGroup
* implement form element for Listbox
* add combined forms example to the playground
* update changelog
* enable support for iterators
* ensure to compile dom iterables
* remove unused imports
185 lines
4.9 KiB
TypeScript
185 lines
4.9 KiB
TypeScript
import React, {
|
|
Fragment,
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useMemo,
|
|
useState,
|
|
useRef,
|
|
|
|
// Types
|
|
ElementType,
|
|
KeyboardEvent as ReactKeyboardEvent,
|
|
MouseEvent as ReactMouseEvent,
|
|
Ref,
|
|
} from 'react'
|
|
|
|
import { Props } from '../../types'
|
|
import { forwardRefWithAs, render, compact } from '../../utils/render'
|
|
import { useId } from '../../hooks/use-id'
|
|
import { Keys } from '../keyboard'
|
|
import { isDisabledReactIssue7711 } from '../../utils/bugs'
|
|
import { Label, useLabels } from '../label/label'
|
|
import { Description, useDescriptions } from '../description/description'
|
|
import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
|
|
import { useSyncRefs } from '../../hooks/use-sync-refs'
|
|
import { VisuallyHidden } from '../../internal/visually-hidden'
|
|
|
|
interface StateDefinition {
|
|
switch: HTMLButtonElement | null
|
|
setSwitch(element: HTMLButtonElement): void
|
|
labelledby: string | undefined
|
|
describedby: string | undefined
|
|
}
|
|
|
|
let GroupContext = createContext<StateDefinition | null>(null)
|
|
GroupContext.displayName = 'GroupContext'
|
|
|
|
// ---
|
|
|
|
let DEFAULT_GROUP_TAG = Fragment
|
|
|
|
function Group<TTag extends ElementType = typeof DEFAULT_GROUP_TAG>(props: Props<TTag>) {
|
|
let [switchElement, setSwitchElement] = useState<HTMLButtonElement | null>(null)
|
|
let [labelledby, LabelProvider] = useLabels()
|
|
let [describedby, DescriptionProvider] = useDescriptions()
|
|
|
|
let context = useMemo<StateDefinition>(
|
|
() => ({ switch: switchElement, setSwitch: setSwitchElement, labelledby, describedby }),
|
|
[switchElement, setSwitchElement, labelledby, describedby]
|
|
)
|
|
|
|
return (
|
|
<DescriptionProvider name="Switch.Description">
|
|
<LabelProvider
|
|
name="Switch.Label"
|
|
props={{
|
|
onClick() {
|
|
if (!switchElement) return
|
|
switchElement.click()
|
|
switchElement.focus({ preventScroll: true })
|
|
},
|
|
}}
|
|
>
|
|
<GroupContext.Provider value={context}>
|
|
{render({ props, defaultTag: DEFAULT_GROUP_TAG, name: 'Switch.Group' })}
|
|
</GroupContext.Provider>
|
|
</LabelProvider>
|
|
</DescriptionProvider>
|
|
)
|
|
}
|
|
|
|
// ---
|
|
|
|
let DEFAULT_SWITCH_TAG = 'button' as const
|
|
interface SwitchRenderPropArg {
|
|
checked: boolean
|
|
}
|
|
type SwitchPropsWeControl =
|
|
| 'id'
|
|
| 'role'
|
|
| 'tabIndex'
|
|
| 'aria-checked'
|
|
| 'aria-labelledby'
|
|
| 'aria-describedby'
|
|
| 'onClick'
|
|
| 'onKeyUp'
|
|
| 'onKeyPress'
|
|
|
|
let SwitchRoot = forwardRefWithAs(function Switch<
|
|
TTag extends ElementType = typeof DEFAULT_SWITCH_TAG
|
|
>(
|
|
props: Props<
|
|
TTag,
|
|
SwitchRenderPropArg,
|
|
SwitchPropsWeControl | 'checked' | 'onChange' | 'name' | 'value'
|
|
> & {
|
|
checked: boolean
|
|
onChange(checked: boolean): void
|
|
name?: string
|
|
value?: string
|
|
},
|
|
ref: Ref<HTMLElement>
|
|
) {
|
|
let { checked, onChange, name, value, ...passThroughProps } = props
|
|
let id = `headlessui-switch-${useId()}`
|
|
let groupContext = useContext(GroupContext)
|
|
let internalSwitchRef = useRef<HTMLButtonElement | null>(null)
|
|
let switchRef = useSyncRefs(
|
|
internalSwitchRef,
|
|
ref,
|
|
// @ts-expect-error figure out the correct type here
|
|
groupContext === null ? null : groupContext.setSwitch
|
|
)
|
|
|
|
let toggle = useCallback(() => onChange(!checked), [onChange, checked])
|
|
let handleClick = useCallback(
|
|
(event: ReactMouseEvent) => {
|
|
if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
|
|
event.preventDefault()
|
|
toggle()
|
|
},
|
|
[toggle]
|
|
)
|
|
let handleKeyUp = useCallback(
|
|
(event: ReactKeyboardEvent<HTMLElement>) => {
|
|
if (event.key !== Keys.Tab) event.preventDefault()
|
|
if (event.key === Keys.Space) toggle()
|
|
},
|
|
[toggle]
|
|
)
|
|
|
|
// This is needed so that we can "cancel" the click event when we use the `Enter` key on a button.
|
|
let handleKeyPress = useCallback(
|
|
(event: ReactKeyboardEvent<HTMLElement>) => event.preventDefault(),
|
|
[]
|
|
)
|
|
|
|
let slot = useMemo<SwitchRenderPropArg>(() => ({ checked }), [checked])
|
|
let propsWeControl = {
|
|
id,
|
|
ref: switchRef,
|
|
role: 'switch',
|
|
type: useResolveButtonType(props, internalSwitchRef),
|
|
tabIndex: 0,
|
|
'aria-checked': checked,
|
|
'aria-labelledby': groupContext?.labelledby,
|
|
'aria-describedby': groupContext?.describedby,
|
|
onClick: handleClick,
|
|
onKeyUp: handleKeyUp,
|
|
onKeyPress: handleKeyPress,
|
|
}
|
|
|
|
let renderConfiguration = {
|
|
props: { ...passThroughProps, ...propsWeControl },
|
|
slot,
|
|
defaultTag: DEFAULT_SWITCH_TAG,
|
|
name: 'Switch',
|
|
}
|
|
|
|
if (name != null && checked) {
|
|
return (
|
|
<>
|
|
<VisuallyHidden
|
|
{...compact({
|
|
as: 'input',
|
|
type: 'checkbox',
|
|
hidden: true,
|
|
readOnly: true,
|
|
checked,
|
|
name,
|
|
value,
|
|
})}
|
|
/>
|
|
{render(renderConfiguration)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
return render(renderConfiguration)
|
|
})
|
|
|
|
// ---
|
|
|
|
export let Switch = Object.assign(SwitchRoot, { Group, Label, Description })
|