-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
55c687f
commit d45813e
Showing
14 changed files
with
614 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# Radio | ||
|
||
`Radio` component follows the | ||
[WAI-ARIA Radio Pattern](https://w3c.github.io/aria-practices/#radiobutton) for | ||
it's | ||
[accessibility properties](https://w3c.github.io/aria-practices/#wai-aria-roles-states-and-properties-16). | ||
By default, it renders the native `<input type="radio">`. | ||
|
||
<!-- INJECT_TOC --> | ||
|
||
## Usage | ||
|
||
<!-- IMPORT_EXAMPLE src/radio/stories/templates/RadioBasicJsx.ts --> | ||
|
||
<!-- CODESANDBOX | ||
link_title: Radio Basic | ||
js: src/radio/stories/templates/RadioBasicJsx.ts | ||
--> | ||
|
||
## Accessibility Requirement | ||
|
||
- Radio has role `radio`. | ||
- Radio has aria-checked set to true when it's checked. Otherwise, aria-checked | ||
is set to false. | ||
- Radio extends the accessibility features of CompositeItem, which means it uses | ||
the roving tabindex method to manage focus. | ||
- When Radio is not rendered as a native input checkbox, Radio will add | ||
`role="radio"` | ||
- RadioGroup has role `radiogroup`. | ||
- RadioGroup must has `aria-label` or `aria-labelledby` to describe the group. | ||
|
||
<!-- INJECT_COMPOSITION src/radio --> | ||
|
||
<!-- INJECT_PROPS src/radio --> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
import { | ||
CompositeItemOptions, | ||
CompositeItemHTMLProps, | ||
useCompositeItem, | ||
} from "reakit"; | ||
import * as React from "react"; | ||
import { warning } from "reakit-warning/warning"; | ||
import { useLiveRef, useForkRef } from "reakit-utils"; | ||
import { createComponent, createHook } from "reakit-system"; | ||
|
||
import { RADIO_KEYS } from "./__keys"; | ||
import { RadioStateReturn } from "./RadioState"; | ||
import { useInitialChecked, getChecked, fireChange } from "./helpers"; | ||
|
||
export type RadioOptions = CompositeItemOptions & | ||
Pick<Partial<RadioStateReturn>, "state" | "setState"> & { | ||
/** | ||
* Same as the `value` attribute. | ||
*/ | ||
value: string | number; | ||
/** | ||
* Same as the `checked` attribute. | ||
*/ | ||
checked?: boolean; | ||
/** | ||
* @private | ||
*/ | ||
unstable_checkOnFocus?: boolean; | ||
}; | ||
|
||
export type RadioHTMLProps = CompositeItemHTMLProps & | ||
React.InputHTMLAttributes<any>; | ||
|
||
export type RadioProps = RadioOptions & RadioHTMLProps; | ||
|
||
export const useRadio = createHook<RadioOptions, RadioHTMLProps>({ | ||
name: "Radio", | ||
compose: useCompositeItem, | ||
keys: RADIO_KEYS, | ||
|
||
useOptions( | ||
{ unstable_clickOnEnter = false, unstable_checkOnFocus = true, ...options }, | ||
{ value, checked }, | ||
) { | ||
return { | ||
checked, | ||
unstable_clickOnEnter, | ||
unstable_checkOnFocus, | ||
...options, | ||
value: options.value ?? value, | ||
}; | ||
}, | ||
|
||
useProps( | ||
options, | ||
{ | ||
ref: htmlRef, | ||
onChange: htmlOnChange, | ||
onClick: htmlOnClick, | ||
...htmlProps | ||
}, | ||
) { | ||
const { | ||
currentId, | ||
id, | ||
disabled, | ||
setState, | ||
value, | ||
unstable_moves, | ||
unstable_checkOnFocus, | ||
baseId, | ||
} = options; | ||
const ref = React.useRef<HTMLInputElement>(null); | ||
const [isNativeRadio, setIsNativeRadio] = React.useState(true); | ||
const checked = getChecked(options); | ||
const isCurrentItemRef = useLiveRef(currentId === id); | ||
const onChangeRef = useLiveRef(htmlOnChange); | ||
const onClickRef = useLiveRef(htmlOnClick); | ||
|
||
useInitialChecked(options); | ||
|
||
React.useEffect(() => { | ||
const element = ref.current; | ||
if (!element) { | ||
warning( | ||
true, | ||
"Can't determine whether the element is a native radio because `ref` wasn't passed to the component", | ||
"See https://reakit.io/docs/radio", | ||
); | ||
return; | ||
} | ||
if (element.tagName !== "INPUT" || element.type !== "radio") { | ||
setIsNativeRadio(false); | ||
} | ||
}, []); | ||
|
||
const onChange = React.useCallback( | ||
(event: React.ChangeEvent) => { | ||
onChangeRef.current?.(event); | ||
|
||
if (event.defaultPrevented) return; | ||
if (disabled) return; | ||
|
||
setState?.(value); | ||
}, | ||
[disabled, onChangeRef, setState, value], | ||
); | ||
|
||
const onClick = React.useCallback( | ||
(event: React.MouseEvent<HTMLInputElement, MouseEvent>) => { | ||
onClickRef.current?.(event); | ||
|
||
if (event.defaultPrevented) return; | ||
if (isNativeRadio) return; | ||
|
||
fireChange(event.currentTarget, onChange); | ||
}, | ||
[onClickRef, isNativeRadio, onChange], | ||
); | ||
|
||
React.useEffect(() => { | ||
const element = ref.current; | ||
if (!element) return; | ||
|
||
if (unstable_moves && isCurrentItemRef.current && unstable_checkOnFocus) { | ||
fireChange(element, onChange); | ||
} | ||
}, [unstable_moves, unstable_checkOnFocus, onChange, isCurrentItemRef]); | ||
|
||
return { | ||
ref: useForkRef(ref, htmlRef), | ||
role: !isNativeRadio ? "radio" : undefined, | ||
type: isNativeRadio ? "radio" : undefined, | ||
value: isNativeRadio ? value : undefined, | ||
name: isNativeRadio ? baseId : undefined, | ||
"aria-checked": checked, | ||
checked, | ||
onChange, | ||
onClick, | ||
...htmlProps, | ||
}; | ||
}, | ||
}); | ||
|
||
export const Radio = createComponent({ | ||
as: "input", | ||
memo: true, | ||
useHook: useRadio, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import * as React from "react"; | ||
import { useWarning } from "reakit-warning"; | ||
import { CompositeOptions, CompositeHTMLProps, useComposite } from "reakit"; | ||
import { createComponent, createHook, useCreateElement } from "reakit-system"; | ||
|
||
import { RADIO_GROUP_KEYS } from "./__keys"; | ||
|
||
export type RadioGroupOptions = CompositeOptions; | ||
|
||
export type RadioGroupHTMLProps = CompositeHTMLProps & | ||
React.FieldsetHTMLAttributes<any>; | ||
|
||
export type RadioGroupProps = RadioGroupOptions & RadioGroupHTMLProps; | ||
|
||
const useRadioGroup = createHook<RadioGroupOptions, RadioGroupHTMLProps>({ | ||
name: "RadioGroup", | ||
compose: useComposite, | ||
keys: RADIO_GROUP_KEYS, | ||
|
||
useProps(options, htmlProps) { | ||
return { role: "radiogroup", ...htmlProps }; | ||
}, | ||
}); | ||
|
||
export const RadioGroup = createComponent({ | ||
as: "div", | ||
useHook: useRadioGroup, | ||
useCreateElement: (type, props, children) => { | ||
useWarning( | ||
!props["aria-label"] && !props["aria-labelledby"], | ||
"You should provide either `aria-label` or `aria-labelledby` props.", | ||
"See https://reakit.io/docs/radio", | ||
); | ||
return useCreateElement(type, props, children); | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import * as React from "react"; | ||
import { useControllableState } from "../utils"; | ||
import { | ||
CompositeState, | ||
CompositeActions, | ||
CompositeInitialState, | ||
useCompositeState, | ||
} from "reakit"; | ||
|
||
export type RadioState = CompositeState & { | ||
/** | ||
* The `value` attribute of the current checked radio. | ||
*/ | ||
state: string | number | null; | ||
}; | ||
|
||
export type RadioActions = CompositeActions & { | ||
/** | ||
* Sets `state`. | ||
*/ | ||
setState: React.Dispatch<React.SetStateAction<string | number | null>>; | ||
}; | ||
|
||
export type RadioInitialState = CompositeInitialState & { | ||
/** | ||
* Default State of the Checkbox for uncontrolled Checkbox. | ||
* | ||
* @default false | ||
*/ | ||
defaultState?: RadioState["state"]; | ||
|
||
/** | ||
* State of the Checkbox for controlled Checkbox.. | ||
*/ | ||
state?: RadioState["state"]; | ||
|
||
/** | ||
* OnChange callback for controlled Checkbox. | ||
*/ | ||
onStateChange?: RadioActions["setState"]; | ||
}; | ||
|
||
export type RadioStateReturn = RadioState & RadioActions; | ||
|
||
export function useRadioState(props: RadioInitialState): RadioStateReturn { | ||
const { | ||
defaultState, | ||
state: stateProps, | ||
onStateChange, | ||
loop = true, | ||
...sealed | ||
} = props; | ||
const [state, setState] = useControllableState({ | ||
defaultValue: defaultState, | ||
value: stateProps, | ||
onChange: onStateChange, | ||
}); | ||
const composite = useCompositeState({ ...sealed, loop }); | ||
|
||
return { | ||
...composite, | ||
state, | ||
setState, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
// Automatically generated | ||
export const USE_RADIO_STATE_KEYS = [ | ||
"baseId", | ||
"unstable_virtual", | ||
"rtl", | ||
"orientation", | ||
"currentId", | ||
"loop", | ||
"wrap", | ||
"shift", | ||
"unstable_includesBaseElement", | ||
"defaultState", | ||
"state", | ||
"onStateChange", | ||
] as const; | ||
export const RADIO_STATE_KEYS = [ | ||
"baseId", | ||
"unstable_idCountRef", | ||
"unstable_virtual", | ||
"rtl", | ||
"orientation", | ||
"items", | ||
"groups", | ||
"currentId", | ||
"loop", | ||
"wrap", | ||
"shift", | ||
"unstable_moves", | ||
"unstable_hasActiveWidget", | ||
"unstable_includesBaseElement", | ||
"state", | ||
"setBaseId", | ||
"registerItem", | ||
"unregisterItem", | ||
"registerGroup", | ||
"unregisterGroup", | ||
"move", | ||
"next", | ||
"previous", | ||
"up", | ||
"down", | ||
"first", | ||
"last", | ||
"sort", | ||
"unstable_setVirtual", | ||
"setRTL", | ||
"setOrientation", | ||
"setCurrentId", | ||
"setLoop", | ||
"setWrap", | ||
"setShift", | ||
"reset", | ||
"unstable_setIncludesBaseElement", | ||
"unstable_setHasActiveWidget", | ||
"setState", | ||
] as const; | ||
export const RADIO_KEYS = [ | ||
...RADIO_STATE_KEYS, | ||
"value", | ||
"checked", | ||
"unstable_checkOnFocus", | ||
] as const; | ||
export const RADIO_GROUP_KEYS = RADIO_STATE_KEYS; |
Oops, something went wrong.