Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

radio options #107

Merged
merged 24 commits into from
Dec 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
66b79bd
Remove unnecessary coercion
diogob Nov 14, 2022
d019a19
Move type Option into createField since this type is only used there …
diogob Nov 14, 2022
24f49f5
extract options mapping to select option
diogob Nov 14, 2022
5db9615
Keep selectCrhildren being used for backwards compatible API
diogob Nov 15, 2022
623073a
Extract commonprops and return in SmartInput
diogob Nov 15, 2022
6d678df
Checkbox is always checkbox
diogob Nov 15, 2022
8953ea0
Add radio properties so we can configure the form to render certain o…
diogob Nov 15, 2022
5726275
Add example for radio button
diogob Nov 15, 2022
26d69db
Pass radio option to smartinput when we have field children
diogob Nov 16, 2022
fc9f09f
Thighten the type for radio list so it only accepts fields that will …
diogob Nov 16, 2022
14e44a7
Simplify required assertion on example
diogob Nov 16, 2022
08a7386
Add test case for radio group
diogob Nov 16, 2022
fe2d533
Ensure our radio buttons have an unique id so we can properly label them
diogob Nov 16, 2022
43a07bb
Use radio parameter to correctly detect radio rendering and place rad…
diogob Nov 17, 2022
256b94f
Add some formatting so our radio button example looks a bit better
diogob Nov 17, 2022
a422b05
Rename property to role for clarity
diogob Nov 21, 2022
db51845
Add key props to input radio elements
diogob Nov 21, 2022
1e3287e
Ensure passing radio components works properly
diogob Nov 23, 2022
d1306a2
Tweak tests for radio buttons
diogob Nov 23, 2022
02d2882
s/radioWrapperComponent/radioGroupComponent/ since we are going to in…
diogob Nov 28, 2022
0440e8d
Use a radioWrapperComponent so we can wrap individual radios with the…
diogob Nov 29, 2022
3f0b271
Use fieldset and legend tags to build default radio group rendering s…
diogob Nov 29, 2022
fcb6bc8
a11y props go on the radio group instead of individual radio buttons
diogob Nov 30, 2022
b33b82b
Tidy up UI
diogob Dec 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/web/app/routes/examples.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ export default function Component() {
<SidebarLayout.NavLink to={'/examples/forms/labels-and-options'}>
Labels, options, etc
</SidebarLayout.NavLink>
<SidebarLayout.NavLink to={'/examples/forms/radio-buttons'}>
Radio buttons
</SidebarLayout.NavLink>
<SidebarLayout.NavLink to={'/examples/forms/hidden-field'}>
Hidden field
</SidebarLayout.NavLink>
Expand Down
2 changes: 2 additions & 0 deletions apps/web/app/routes/examples/forms/form-with-children.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export default () => (
{ name: 'Friend', value: 'fromAFriend' },
{ name: 'Search', value: 'google' },
]}
radio
/>
<Field name="message" multiline placeholder="Your message" />
<Errors />
Expand Down Expand Up @@ -81,6 +82,7 @@ export default function Component() {
{ name: 'Friend', value: 'fromAFriend' },
{ name: 'Search', value: 'google' },
]}
radio
/>
<Field name="message" multiline placeholder="Your message" />
<Errors />
Expand Down
49 changes: 49 additions & 0 deletions apps/web/app/routes/examples/forms/radio-buttons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import hljs from 'highlight.js/lib/common'
import type {
ActionFunction,
LoaderFunction,
MetaFunction,
} from '@remix-run/node'
import { formAction } from '~/formAction'
import { z } from 'zod'
import Form from '~/ui/form'
import { metaTags } from '~/helpers'
import { makeDomainFunction } from 'domain-functions'
import Example from '~/ui/example'

const title = 'Radio buttons'
const description =
'In this example, we render enum options in a radio button group.'

export const meta: MetaFunction = () => metaTags({ title, description })

const code = `const schema = z.object({
name: z.string().min(1),
role: z.enum(['Designer', 'Dev']),
})

export default () => (
<Form schema={schema} radio={['role']} />
)`

const schema = z.object({
name: z.string().min(1),
role: z.enum(['Designer', 'Dev']),
})

export const loader: LoaderFunction = () => ({
code: hljs.highlight(code, { language: 'ts' }).value,
})

const mutation = makeDomainFunction(schema)(async (values) => values)

export const action: ActionFunction = async ({ request }) =>
formAction({ request, schema, mutation })

export default function Component() {
return (
<Example title={title} description={description}>
<Form schema={schema} radio={['role']} />
</Example>
)
}
6 changes: 6 additions & 0 deletions apps/web/app/ui/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import Label from './label'
import Select from './select'
import SubmitButton from './submit-button'
import Checkbox from './checkbox'
import Radio from './radio'

import CheckboxWrapper from './checkbox-wrapper'
import RadioWrapper from './radio-wrapper'

import TextArea from './text-area'
import {
Form as RemixForm,
Expand Down Expand Up @@ -36,6 +40,8 @@ export default function Form<Schema extends FormSchema>(
multilineComponent={TextArea}
selectComponent={Select}
checkboxComponent={Checkbox}
radioComponent={Radio}
radioGroupComponent={RadioWrapper}
checkboxWrapperComponent={CheckboxWrapper}
buttonComponent={SubmitButton}
globalErrorsComponent={Errors}
Expand Down
3 changes: 3 additions & 0 deletions apps/web/app/ui/radio-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function RadioWrapper(props: JSX.IntrinsicElements['fieldset']) {
return <fieldset className="flex items-center space-x-2" {...props} />
}
20 changes: 20 additions & 0 deletions apps/web/app/ui/radio.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as React from 'react'
import { cx } from '~/helpers'

const Radio = React.forwardRef<
HTMLInputElement,
JSX.IntrinsicElements['input']
>(({ type = 'radio', className, ...props }, ref) => (
<input
ref={ref}
type={type}
className={cx(
'h-4 w-4 rounded',
className,
!className && 'border-gray-300 text-pink-600 focus:ring-pink-500',
diogob marked this conversation as resolved.
Show resolved Hide resolved
)}
{...props}
/>
))

export default Radio
16 changes: 7 additions & 9 deletions apps/web/tests/examples/forms/form-with-children.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,12 @@ test('With JS enabled', async ({ example }) => {
await expect(page.locator('form em:visible')).toHaveText(
"You'll hear from us at this address 👆🏽",
)
await example.expectRadioToHaveOptions('howYouFoundOutAboutUs', [
{ name: 'Friend', value: 'fromAFriend' },
{ name: 'Search', value: 'google' },
])

await example.expectSelect(howYouFoundOutAboutUs, { value: '' })
await howYouFoundOutAboutUs.input.selectOption({ value: 'fromAFriend' })

const options = howYouFoundOutAboutUs.input.locator('option')
await expect(options.first()).toHaveText('Friend')
await expect(options.last()).toHaveText('Search')
await howYouFoundOutAboutUs.input.first().click()

await example.expectField(message, {
multiline: true,
Expand Down Expand Up @@ -65,10 +64,9 @@ test('With JS enabled', async ({ example }) => {

// Make form be valid
await email.input.fill('john@doe.com')
await howYouFoundOutAboutUs.input.selectOption('google')
await howYouFoundOutAboutUs.input.last().click()
await message.input.fill('My message')
await example.expectValid(email)
await example.expectValid(howYouFoundOutAboutUs)

// Submit form
button.click()
Expand Down Expand Up @@ -126,7 +124,7 @@ testWithoutJS('With JS disabled', async ({ example }) => {

// Make form be valid and test selecting an option
await email.input.fill('john@doe.com')
await howYouFoundOutAboutUs.input.selectOption('google')
await howYouFoundOutAboutUs.input.last().click()

// Submit form
await button.click()
Expand Down
83 changes: 83 additions & 0 deletions apps/web/tests/examples/forms/radio-buttons.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { test, testWithoutJS, expect } from 'tests/setup/tests'

const route = '/examples/forms/radio-buttons'

test('With JS enabled', async ({ example }) => {
const { button, page } = example
const name = example.field('name')
const radio = example.field('role')

await page.goto(route)

// Render
await example.expectField(name)
await example.expectRadioToHaveOptions('role', [
{ name: 'Dev', value: 'Dev' },
{ name: 'Designer', value: 'Designer' },
])
await expect(button).toBeEnabled()

// Client-side validation
await button.click()

// Show field errors and focus on the first field

await example.expectError(name, 'String must contain at least 1 character(s)')
await example.expectErrorMessage(
'role',
"Invalid enum value. Expected 'Designer' | 'Dev', received ''",
)

await expect(name.input).toBeFocused()

await name.input.type('John Corn')
await radio.input.first().click()

// Submit form
await button.click()

await example.expectData({
name: 'John Corn',
role: 'Designer',
})
})

testWithoutJS('With JS disabled', async ({ example }) => {
const { button, page } = example
const name = example.field('name')
const radio = example.field('role')

await page.goto(route)

// Render
await example.expectField(name)
await example.expectRadioToHaveOptions('role', [
{ name: 'Dev', value: 'Dev' },
{ name: 'Designer', value: 'Designer' },
])
await expect(button).toBeEnabled()

// Client-side validation
await button.click()

// Show field errors and focus on the first field

await example.expectError(name, 'String must contain at least 1 character(s)')
await example.expectErrorMessage(
'role',
"Invalid enum value. Expected 'Designer' | 'Dev', received ''",
)

await expect(name.input).toBeFocused()

await name.input.type('John Corn')
await radio.input.first().click()

// Submit form
await button.click()

await example.expectData({
name: 'John Corn',
role: 'Designer',
})
})
23 changes: 19 additions & 4 deletions apps/web/tests/setup/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type FieldOptions = {
required?: boolean
invalid?: boolean
multiline?: boolean
radio?: boolean
options?: { name: string; value: string }[]
}

Expand Down Expand Up @@ -79,9 +80,20 @@ class Example {
`label-for-${field.name}`,
)

required
? await expect(field.input).toHaveAttribute('aria-required', 'true')
: await expect(field.input).not.toHaveAttribute('aria-required', 'true')
await expect(field.input).toHaveAttribute('aria-required', String(required))
}

async expectRadioToHaveOptions(
radioName: string,
options: { name: string; value: string }[],
) {
Promise.all(
options.map(({ name, value }) => {
expect(
this.page.locator(`[name="${radioName}"][value="${value}"]:visible`),
).toBeVisible()
}),
)
}

async expectSelect(field: Field, options: FieldOptions = {}) {
Expand All @@ -108,8 +120,11 @@ class Example {
async expectError(field: Field, message: string) {
await this.expectInvalid(field)

await this.expectErrorMessage(field.name, message)
}
async expectErrorMessage(fieldName: string, message: string) {
await expect(
this.page.locator(`#errors-for-${field.name}:visible`),
this.page.locator(`#errors-for-${fieldName}:visible`),
).toHaveText(message)
}

Expand Down
Loading