Skip to content

Commit

Permalink
Merge pull request #2658 from chakra-ui/feat/radio-form-group
Browse files Browse the repository at this point in the history
  • Loading branch information
with-heart committed Nov 29, 2020
2 parents d6b7aac + 58e2665 commit 44ee1f9
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 45 deletions.
6 changes: 6 additions & 0 deletions .changeset/soft-grapes-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@chakra-ui/radio": patch
---

This change enables `Radio` to automatically derive various values from a
surrounding `FormControl` if found, similar to `Input` and `Select`.
1 change: 1 addition & 0 deletions packages/radio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"lint:types": "tsc --noEmit"
},
"dependencies": {
"@chakra-ui/form-control": "1.0.1",
"@chakra-ui/hooks": "1.0.1",
"@chakra-ui/utils": "1.0.1",
"@chakra-ui/visually-hidden": "1.0.1"
Expand Down
133 changes: 90 additions & 43 deletions packages/radio/src/use-radio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
callAllHandlers,
dataAttr,
mergeRefs,
pick,
PropGetter,
} from "@chakra-ui/utils"
import { visuallyHiddenStyle } from "@chakra-ui/visually-hidden"
Expand All @@ -14,6 +15,7 @@ import {
useRef,
useState,
} from "react"
import { useFormControl } from "@chakra-ui/form-control"

/**
* @todo use the `useClickable` hook here
Expand Down Expand Up @@ -61,7 +63,7 @@ export interface UseRadioProps {
*/
isInvalid?: boolean
/**
* If `true`, the radio button will be invalid. This sets `aria-invalid` to `true`.
* If `true`, the radio button will be required. This sets `aria-invalid` to `true`.
*/
isRequired?: boolean
/**
Expand Down Expand Up @@ -112,8 +114,6 @@ export function useRadio(props: UseRadioProps = {}) {
onChange?.(event)
}

const trulyDisabled = isDisabled && !isFocusable

const onKeyDown = useCallback(
(event: React.KeyboardEvent) => {
if (event.key === " ") {
Expand All @@ -132,46 +132,93 @@ export function useRadio(props: UseRadioProps = {}) {
[setActive],
)

const getCheckboxProps: PropGetter = (props = {}, ref = null) => ({
...props,
ref,
"data-active": dataAttr(isActive),
"data-hover": dataAttr(isHovered),
"data-disabled": dataAttr(isDisabled),
"data-invalid": dataAttr(isInvalid),
"data-checked": dataAttr(isChecked),
"data-focus": dataAttr(isFocused),
"data-readonly": dataAttr(isReadOnly),
"aria-hidden": true,
onMouseDown: callAllHandlers(props.onMouseDown, setActive.on),
onMouseUp: callAllHandlers(props.onMouseUp, setActive.off),
onMouseEnter: callAllHandlers(props.onMouseEnter, setHovering.on),
onMouseLeave: callAllHandlers(props.onMouseLeave, setHovering.off),
})

const getInputProps: PropGetter<HTMLInputElement> = (
props = {},
forwardedRef = null,
) => ({
...props,
ref: mergeRefs(forwardedRef, ref),
type: "radio",
name,
value,
id,
onChange: callAllHandlers(props.onChange, handleChange),
onBlur: callAllHandlers(props.onBlur, setFocused.off),
onFocus: callAllHandlers(props.onFocus, setFocused.on),
onKeyDown: callAllHandlers(props.onKeyDown, onKeyDown),
onKeyUp: callAllHandlers(props.onKeyUp, onKeyUp),
"aria-required": ariaAttr(isRequired),
checked: isChecked,
disabled: trulyDisabled,
readOnly: isReadOnly,
"aria-invalid": ariaAttr(isInvalid),
"aria-disabled": ariaAttr(isDisabled),
style: visuallyHiddenStyle,
})
const getCheckboxProps: PropGetter = useCallback(
(props = {}, ref = null) => ({
...props,
ref,
"data-active": dataAttr(isActive),
"data-hover": dataAttr(isHovered),
"data-disabled": dataAttr(isDisabled),
"data-invalid": dataAttr(isInvalid),
"data-checked": dataAttr(isChecked),
"data-focus": dataAttr(isFocused),
"data-readonly": dataAttr(isReadOnly),
"aria-hidden": true,
onMouseDown: callAllHandlers(props.onMouseDown, setActive.on),
onMouseUp: callAllHandlers(props.onMouseUp, setActive.off),
onMouseEnter: callAllHandlers(props.onMouseEnter, setHovering.on),
onMouseLeave: callAllHandlers(props.onMouseLeave, setHovering.off),
}),
[
isActive,
isHovered,
isDisabled,
isInvalid,
isChecked,
isFocused,
isReadOnly,
setActive.on,
setActive.off,
setHovering.on,
setHovering.off,
],
)

const inputProps = useFormControl<HTMLInputElement>(props)

const getInputProps: PropGetter<HTMLInputElement> = useCallback(
(props = {}, forwardedRef = null) => {
const ownProps = pick(inputProps, [
"id",
"disabled",
"readOnly",
"required",
"aria-invalid",
"aria-required",
"aria-readonly",
"aria-describedby",
"onFocus",
"onBlur",
])

const trulyDisabled = ownProps.disabled && !isFocusable

return {
...props,
...ownProps,
ref: mergeRefs(forwardedRef, ref),
type: "radio",
name,
value,
onChange: callAllHandlers(props.onChange, handleChange),
onBlur: callAllHandlers(ownProps.onBlur, props.onBlur, setFocused.off),
onFocus: callAllHandlers(
ownProps.onFocus,
props.onFocus,
setFocused.on,
),
onKeyDown: callAllHandlers(props.onKeyDown, onKeyDown),
onKeyUp: callAllHandlers(props.onKeyUp, onKeyUp),
checked: isChecked,
disabled: trulyDisabled,
"aria-disabled": ariaAttr(trulyDisabled),
style: visuallyHiddenStyle,
}
},
[
inputProps,
isFocusable,
name,
value,
handleChange,
setFocused.off,
setFocused.on,
onKeyDown,
onKeyUp,
isChecked,
visuallyHiddenStyle,
],
)

const getLabelProps: PropGetter = (props = {}, ref = null) => {
return {
Expand Down
43 changes: 41 additions & 2 deletions packages/radio/tests/radio.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from "react"
import { render, fireEvent } from "@chakra-ui/test-utils"
import { useRadio, UseRadioProps } from "../src"
import { render, fireEvent, screen } from "@chakra-ui/test-utils"
import { Radio, useRadio, UseRadioProps } from "../src"
import { FormControl, FormHelperText, FormLabel } from "@chakra-ui/form-control"

test("has proper aria and data attributes", async () => {
const Component = (props: UseRadioProps = {}) => {
Expand All @@ -23,6 +24,7 @@ test("has proper aria and data attributes", async () => {
expect(input).toHaveAttribute("value", "")
expect(input).not.toBeDisabled()
expect(input).not.toHaveAttribute("aria-required")
expect(input).not.toHaveAttribute("required")
expect(input).not.toHaveAttribute("aria-invalid")
expect(input).not.toHaveAttribute("aria-disabled")
expect(checkbox).toHaveAttribute("aria-hidden", "true")
Expand All @@ -39,6 +41,7 @@ test("has proper aria and data attributes", async () => {
checkbox = utils.getByTestId("checkbox")

expect(input).toHaveAttribute("aria-required")
expect(input).toHaveAttribute("required")
expect(input).toHaveAttribute("aria-invalid")
expect(input).toHaveAttribute("aria-disabled")
expect(input).toBeDisabled()
Expand Down Expand Up @@ -113,3 +116,39 @@ test("handles events and callbacks correctly", () => {
expect(checkbox).not.toHaveAttribute("data-active")
expect(inputProps.onKeyUp).toHaveBeenCalled()
})

test("should derive values from surrounding FormControl", () => {
const onFocus = jest.fn()
const onBlur = jest.fn()

render(
<FormControl
id="radio"
isRequired
isInvalid
isDisabled
isReadOnly
onFocus={onFocus}
onBlur={onBlur}
>
<FormLabel>Radio</FormLabel>
<Radio value="Chakra UI">Chakra UI</Radio>
<FormHelperText>Select a value</FormHelperText>
</FormControl>,
)

const radio = screen.getByRole("radio")

expect(radio).toHaveAttribute("id", "radio")
expect(radio).toHaveAttribute("aria-invalid", "true")
expect(radio).toHaveAttribute("aria-required", "true")
expect(radio).toHaveAttribute("aria-readonly", "true")
expect(radio).toHaveAttribute("aria-invalid", "true")
expect(radio).toHaveAttribute("aria-describedby")

fireEvent.focus(radio)
expect(onFocus).toHaveBeenCalled()

fireEvent.blur(radio)
expect(onBlur).toHaveBeenCalled()
})

0 comments on commit 44ee1f9

Please sign in to comment.