Files
headlessui/packages/@headlessui-react/src/components/switch/switch.tsx
T
Robin Malfait 7bb89871ba Add <form> compatibility (#1214)
* 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
2022-03-09 11:24:45 +01:00

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 })