Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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: 2 additions & 1 deletion packages/@adobe/react-spectrum/src/checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const Checkbox = forwardRef(function Checkbox(props: SpectrumCheckboxProp
// This is a bit unorthodox. Typically, hooks cannot be called in a conditional,
// but since the checkbox won't move in and out of a group, it should be safe.
let groupState = useContext(CheckboxGroupContext);
let {inputProps, isInvalid, isDisabled} = groupState
let {labelProps, inputProps, isInvalid, isDisabled} = groupState
// eslint-disable-next-line react-hooks/rules-of-hooks
? useCheckboxGroupItem({
...props,
Expand Down Expand Up @@ -104,6 +104,7 @@ export const Checkbox = forwardRef(function Checkbox(props: SpectrumCheckboxProp

return (
<label
{...labelProps}
{...styleProps}
{...hoverProps}
ref={domRef}
Expand Down
3 changes: 2 additions & 1 deletion packages/@adobe/react-spectrum/src/radio/Radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,15 @@ export const Radio = forwardRef(function Radio(props: SpectrumRadioProps, ref: F
state
} = radioGroupProps;

let {inputProps} = useRadio({
let {labelProps, inputProps} = useRadio({
...props,
...radioGroupProps,
isDisabled
}, state, inputRef);

return (
<label
{...labelProps}
{...styleProps}
{...hoverProps}
ref={domRef}
Expand Down
3 changes: 2 additions & 1 deletion packages/@adobe/react-spectrum/src/switch/Switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,12 @@ export const Switch = forwardRef(function Switch(props: SpectrumSwitchProps, ref
let inputRef = useRef<HTMLInputElement>(null);
let domRef = useFocusableRef(ref, inputRef);
let state = useToggleState(props);
let {inputProps} = useSwitch(props, state, inputRef);
let {labelProps, inputProps} = useSwitch(props, state, inputRef);


return (
<label
{...labelProps}
{...styleProps}
{...hoverProps}
ref={domRef}
Expand Down
118 changes: 72 additions & 46 deletions packages/@react-spectrum/s2/src/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,23 @@
* governing permissions and limitations under the License.
*/

import {baseColor, focusRing, space, style} from '../style' with {type: 'macro'};
import {CenterBaseline} from './CenterBaseline';
import {
Checkbox as AriaCheckbox,
CheckboxProps as AriaCheckboxProps,
CheckboxButton,
CheckboxField,
CheckboxFieldProps,
CheckboxRenderProps
} from 'react-aria-components/Checkbox';
import {baseColor, focusRing, space, style} from '../style' with {type: 'macro'};
import {CenterBaseline} from './CenterBaseline';
import {CheckboxGroupStateContext} from 'react-aria-components/CheckboxGroup';
import CheckmarkIcon from '../ui-icons/Checkmark';
import {ContextValue, useSlottedContext} from 'react-aria-components/slots';
import {controlBorderRadius, controlFont, controlSize, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'};
import {createContext, forwardRef, ReactNode, useContext, useRef} from 'react';
import DashIcon from '../ui-icons/Dash';
import {FocusableRef, FocusableRefValue, GlobalDOMAttributes} from '@react-types/shared';
import {FocusableRef, FocusableRefValue, GlobalDOMAttributes, HelpTextProps} from '@react-types/shared';
import {FormContext, useFormProps} from './Form';
import {HelpText} from './Field';
import {pressScale} from './pressScale';
import {useFocusableRef} from './useDOMRef';
import {useSpectrumContextProps} from './useSpectrumContextProps';
Expand All @@ -42,19 +44,31 @@ interface CheckboxStyleProps {

interface RenderProps extends CheckboxRenderProps, CheckboxStyleProps {}

export interface CheckboxProps extends Omit<AriaCheckboxProps, 'className' | 'style' | 'render' | 'children' | 'onHover' | 'onHoverStart' | 'onHoverEnd' | 'onHoverChange' | 'onClick' | keyof GlobalDOMAttributes>, StyleProps, CheckboxStyleProps {
export interface CheckboxProps extends Omit<CheckboxFieldProps, 'className' | 'style' | 'render' | 'children' | 'onHover' | 'onHoverStart' | 'onHoverEnd' | 'onHoverChange' | 'onClick' | keyof GlobalDOMAttributes>, HelpTextProps, StyleProps, CheckboxStyleProps {
/** The label for the element. */
children?: ReactNode
}

export const CheckboxContext = createContext<ContextValue<Partial<CheckboxProps>, FocusableRefValue<HTMLLabelElement>>>(null);
export const CheckboxContext = createContext<ContextValue<Partial<CheckboxProps>, FocusableRefValue<HTMLInputElement, HTMLDivElement>>>(null);

const field = style({
width: 'fit',
'--field-height': {
type: 'height',
value: controlSize()
},
'--field-gap': {
type: 'rowGap',
value: 'calc(var(--field-height) - 1lh)'
}
}, getAllowedOverrides());

const wrapper = style({
display: 'flex',
position: 'relative',
columnGap: 'text-to-control',
alignItems: 'baseline',
width: 'fit',
width: 'full',
font: controlFont(),
transition: 'colors',
color: {
Expand All @@ -68,7 +82,7 @@ const wrapper = style({
isInForm: 'field'
},
disableTapHighlight: true
}, getAllowedOverrides());
});

export const box = style<RenderProps>({
...focusRing(),
Expand Down Expand Up @@ -137,7 +151,7 @@ const iconSize = {
* Checkboxes allow users to select multiple items from a list of individual items,
* or to mark one individual item as selected.
*/
export const Checkbox = forwardRef(function Checkbox({children, ...props}: CheckboxProps, ref: FocusableRef<HTMLLabelElement>) {
export const Checkbox = forwardRef(function Checkbox({children, ...props}: CheckboxProps, ref: FocusableRef<HTMLInputElement, HTMLDivElement>) {
[props, ref] = useSpectrumContextProps(props, ref, CheckboxContext);
let boxRef = useRef(null);
let inputRef = useRef<HTMLInputElement | null>(null);
Expand All @@ -148,47 +162,59 @@ export const Checkbox = forwardRef(function Checkbox({children, ...props}: Check
let ctx = useSlottedContext(CheckboxContext, props.slot);

return (
<AriaCheckbox
<CheckboxField
{...props}
ref={domRef}
inputRef={inputRef}
style={props.UNSAFE_style}
className={renderProps => (props.UNSAFE_className || '') + wrapper({...renderProps, isInForm, size: props.size || 'M'}, props.styles)}>
{renderProps => {
let checkbox = (
<div
ref={boxRef}
style={pressScale(boxRef)(renderProps)}
className={box({
...renderProps,
isSelected: renderProps.isSelected || renderProps.isIndeterminate,
size: props.size || 'M',
isEmphasized: isInCheckboxGroup ? ctx?.isEmphasized : props.isEmphasized
})}>
{renderProps.isIndeterminate &&
<DashIcon size={iconSize[props.size || 'M']} className={iconStyles} />
}
{renderProps.isSelected && !renderProps.isIndeterminate &&
<CheckmarkIcon size={iconSize[props.size || 'M']} className={iconStyles} />
}
</div>
);
className={(props.UNSAFE_className || '') + field({size: props.size || 'M'}, props.styles)}>
{({isDisabled, isInvalid}) => (<>
<CheckboxButton className={renderProps => wrapper({...renderProps, isInForm, size: props.size || 'M'})}>
{renderProps => {
let checkbox = (
<div
ref={boxRef}
style={pressScale(boxRef)(renderProps)}
className={box({
...renderProps,
isSelected: renderProps.isSelected || renderProps.isIndeterminate,
size: props.size || 'M',
isEmphasized: isInCheckboxGroup ? ctx?.isEmphasized : props.isEmphasized
})}>
{renderProps.isIndeterminate &&
<DashIcon size={iconSize[props.size || 'M']} className={iconStyles} />
}
{renderProps.isSelected && !renderProps.isIndeterminate &&
<CheckmarkIcon size={iconSize[props.size || 'M']} className={iconStyles} />
}
</div>
);

// Only render checkbox without center baseline if no label.
// This avoids expanding the checkbox height to the font's line height.
if (!children) {
return checkbox;
}
// Only render checkbox without center baseline if no label.
// This avoids expanding the checkbox height to the font's line height.
if (!children) {
return checkbox;
}

return (
<>
<CenterBaseline>
{checkbox}
</CenterBaseline>
{children}
</>
);
}}
</AriaCheckbox>
return (
<>
<CenterBaseline>
{checkbox}
</CenterBaseline>
{children}
</>
);
}}
</CheckboxButton>
<HelpText
size={props.size || 'M'}
isDisabled={isDisabled}
isInvalid={isInvalid}
description={props.description}
showErrorIcon>
{props.errorMessage}
</HelpText>
</>)}
</CheckboxField>
);
});
4 changes: 2 additions & 2 deletions packages/@react-spectrum/s2/src/RadioGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export const RadioGroup = /*#__PURE__*/ forwardRef(function RadioGroup(props: Ra
);
});

export interface RadioProps extends Omit<AriaRadioProps, 'className' | 'style' | 'render' | 'children' | 'onHover' | 'onHoverStart' | 'onHoverEnd' | 'onHoverChange' | 'onClick' | keyof GlobalDOMAttributes>, StyleProps {
export interface RadioProps extends Omit<AriaRadioProps, 'description' | 'className' | 'style' | 'render' | 'children' | 'onHover' | 'onHoverStart' | 'onHoverEnd' | 'onHoverChange' | 'onClick' | keyof GlobalDOMAttributes>, StyleProps {
/**
* The label for the element.
*/
Expand Down Expand Up @@ -227,7 +227,7 @@ const circle = style<RenderProps>({
* Radio buttons allow users to select a single option from a list of mutually exclusive options.
* All possible options are exposed up front for users to compare.
*/
export const Radio = /*#__PURE__*/ forwardRef(function Radio(props: RadioProps, ref: FocusableRef<HTMLLabelElement>) {
export const Radio = /*#__PURE__*/ forwardRef(function Radio(props: RadioProps, ref: FocusableRef<HTMLInputElement, HTMLLabelElement>) {
let {children, UNSAFE_className = '', UNSAFE_style} = props;
let circleRef = useRef(null);
let inputRef = useRef<HTMLInputElement | null>(null);
Expand Down
5 changes: 2 additions & 3 deletions packages/@react-spectrum/s2/src/Switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
*/

import {Switch as AriaSwitch, SwitchProps as AriaSwitchProps, SwitchRenderProps} from 'react-aria-components/Switch';

import {baseColor, focusRing, fontRelative, style} from '../style' with {type: 'macro'};
import {CenterBaseline} from './CenterBaseline';
import {ContextValue} from 'react-aria-components/slots';
Expand Down Expand Up @@ -43,7 +42,7 @@ export interface SwitchProps extends Omit<AriaSwitchProps, 'className' | 'style'
children?: ReactNode
}

export const SwitchContext = createContext<ContextValue<Partial<SwitchProps>, FocusableRefValue<HTMLLabelElement>>>(null);
export const SwitchContext = createContext<ContextValue<Partial<SwitchProps>, FocusableRefValue<HTMLInputElement, HTMLLabelElement>>>(null);

const wrapper = style({
display: 'flex',
Expand Down Expand Up @@ -156,7 +155,7 @@ const transformStyle = ({isSelected, direction}: SwitchRenderProps & {direction:
* Switches allow users to turn an individual option on or off.
* They are usually used to activate or deactivate a specific setting.
*/
export const Switch = /*#__PURE__*/ forwardRef(function Switch(props: SwitchProps, ref: FocusableRef<HTMLLabelElement>) {
export const Switch = /*#__PURE__*/ forwardRef(function Switch(props: SwitchProps, ref: FocusableRef<HTMLInputElement, HTMLLabelElement>) {
[props, ref] = useSpectrumContextProps(props, ref, SwitchContext);
let {children, UNSAFE_className = '', UNSAFE_style} = props;
let inputRef = useRef<HTMLInputElement | null>(null);
Expand Down
15 changes: 8 additions & 7 deletions packages/@react-spectrum/s2/src/useDOMRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* governing permissions and limitations under the License.
*/

import {DOMRef, DOMRefValue, FocusableElement, FocusableRef, FocusableRefValue, RefObject} from '@react-types/shared';
import {DOMRef, DOMRefValue, FocusableRef, FocusableRefValue, RefObject} from '@react-types/shared';
import {useImperativeHandle, useMemo, useRef} from 'react';

export function createDOMRef<T extends HTMLElement = HTMLElement>(ref: RefObject<T | null>): DOMRefValue<T> {
Expand All @@ -21,12 +21,13 @@ export function createDOMRef<T extends HTMLElement = HTMLElement>(ref: RefObject
};
}

export function createFocusableRef<T extends HTMLElement = HTMLElement>(domRef: RefObject<T | null>, focusableRef: RefObject<FocusableElement | null> = domRef): FocusableRefValue<T> {
export function createFocusableRef<T extends HTMLElement = HTMLElement, D extends HTMLElement = T>(domRef: RefObject<D | null>, focusableRef?: RefObject<T | null>): FocusableRefValue<T, D> {
let resolvedFocusableRef = focusableRef || domRef;
return {
...createDOMRef(domRef),
focus() {
if (focusableRef.current) {
focusableRef.current.focus();
if (resolvedFocusableRef.current) {
resolvedFocusableRef.current.focus();
}
}
};
Expand All @@ -38,9 +39,9 @@ export function useDOMRef<T extends HTMLElement = HTMLElement>(ref: DOMRef<T>):
return domRef;
}

export function useFocusableRef<T extends HTMLElement = HTMLElement>(ref: FocusableRef<T>, focusableRef?: RefObject<FocusableElement | null>): RefObject<T | null> {
let domRef = useRef<T>(null);
useImperativeHandle(ref, () => createFocusableRef(domRef, focusableRef));
export function useFocusableRef<T extends HTMLElement = HTMLElement, D extends HTMLElement = T>(ref: FocusableRef<T, D>, focusableRef?: RefObject<T | null>): RefObject<D | null> {
let domRef = useRef<D>(null);
useImperativeHandle(ref, () => createFocusableRef<T, D>(domRef, focusableRef));
return domRef;
}

Expand Down
3 changes: 3 additions & 0 deletions packages/@react-types/shared/src/dom.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
PointerEventHandler,
DOMAttributes as ReactDOMAttributes,
ReactEventHandler,
RefAttributes,
TouchEventHandler,
TransitionEventHandler,
UIEventHandler,
Expand Down Expand Up @@ -237,6 +238,8 @@ export interface DOMAttributes<T = FocusableElement> extends AriaAttributes, Rea
className?: string | undefined
}

export interface DOMAttributesWithRef<T = Element> extends DOMAttributes<T>, RefAttributes<T> {}

export interface GroupDOMAttributes extends Omit<DOMAttributes<HTMLElement>, 'role'> {
role?: 'group' | 'region' | 'presentation'
}
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-types/shared/src/refs.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface FocusableRefValue<T extends HTMLElement = HTMLElement, D extend
}

export type DOMRef<T extends HTMLElement = HTMLElement> = Ref<DOMRefValue<T>>;
export type FocusableRef<T extends HTMLElement = HTMLElement> = Ref<FocusableRefValue<T>>;
export type FocusableRef<T extends HTMLElement = HTMLElement, D extends HTMLElement = T> = Ref<FocusableRefValue<T, D>>;

export interface RefObject<T> {
current: T
Expand Down
Loading