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
This commit is contained in:
Robin Malfait
2022-03-09 11:24:45 +01:00
committed by GitHub
parent 2414bbd127
commit 7bb89871ba
30 changed files with 1953 additions and 66 deletions
@@ -8,6 +8,7 @@ import {
getSwitch,
assertActiveElement,
getSwitchLabel,
getByText,
} from '../../test-utils/accessibility-assertions'
import { press, click, Keys } from '../../test-utils/interactions'
@@ -395,3 +396,83 @@ describe('Mouse interactions', () => {
assertSwitch({ state: SwitchState.Off })
})
})
describe('Form compatibility', () => {
it('should be possible to submit a form with an boolean value', async () => {
let submits = jest.fn()
function Example() {
let [state, setState] = useState(false)
return (
<form
onSubmit={(event) => {
event.preventDefault()
submits([...new FormData(event.currentTarget).entries()])
}}
>
<Switch.Group>
<Switch checked={state} onChange={setState} name="notifications" />
<Switch.Label>Enable notifications</Switch.Label>
</Switch.Group>
<button>Submit</button>
</form>
)
}
render(<Example />)
// Submit the form
await click(getByText('Submit'))
// Verify that the form has been submitted
expect(submits).lastCalledWith([]) // no data
// Toggle
await click(getSwitchLabel())
// Submit the form again
await click(getByText('Submit'))
// Verify that the form has been submitted
expect(submits).lastCalledWith([['notifications', 'on']])
})
it('should be possible to submit a form with a provided string value', async () => {
let submits = jest.fn()
function Example() {
let [state, setState] = useState(false)
return (
<form
onSubmit={(event) => {
event.preventDefault()
submits([...new FormData(event.currentTarget).entries()])
}}
>
<Switch.Group>
<Switch checked={state} onChange={setState} name="fruit" value="apple" />
<Switch.Label>Apple</Switch.Label>
</Switch.Group>
<button>Submit</button>
</form>
)
}
render(<Example />)
// Submit the form
await click(getByText('Submit'))
// Verify that the form has been submitted
expect(submits).lastCalledWith([]) // no data
// Toggle
await click(getSwitchLabel())
// Submit the form again
await click(getByText('Submit'))
// Verify that the form has been submitted
expect(submits).lastCalledWith([['fruit', 'apple']])
})
})
@@ -5,17 +5,17 @@ import React, {
useContext,
useMemo,
useState,
useRef,
// Types
ElementType,
KeyboardEvent as ReactKeyboardEvent,
MouseEvent as ReactMouseEvent,
useRef,
Ref,
} from 'react'
import { Props } from '../../types'
import { forwardRefWithAs, render } from '../../utils/render'
import { forwardRefWithAs, render, compact } from '../../utils/render'
import { useId } from '../../hooks/use-id'
import { Keys } from '../keyboard'
import { isDisabledReactIssue7711 } from '../../utils/bugs'
@@ -23,6 +23,7 @@ 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
@@ -88,13 +89,19 @@ type SwitchPropsWeControl =
let SwitchRoot = forwardRefWithAs(function Switch<
TTag extends ElementType = typeof DEFAULT_SWITCH_TAG
>(
props: Props<TTag, SwitchRenderPropArg, SwitchPropsWeControl | 'checked' | 'onChange'> & {
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, ...passThroughProps } = props
let { checked, onChange, name, value, ...passThroughProps } = props
let id = `headlessui-switch-${useId()}`
let groupContext = useContext(GroupContext)
let internalSwitchRef = useRef<HTMLButtonElement | null>(null)
@@ -143,12 +150,33 @@ let SwitchRoot = forwardRefWithAs(function Switch<
onKeyPress: handleKeyPress,
}
return render({
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)
})
// ---