Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
154 changes: 101 additions & 53 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,20 +44,50 @@ 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({
display: 'grid',
gridTemplateColumns: ['max-content', '1fr'],
columnGap: 'text-to-control',
alignContent: 'start',
width: {
default: 'fit',
isInCheckboxGroup: 'auto'
},
font: controlFont(),
'--field-height': {
type: 'height',
value: controlSize()
},
rowGap: {
default: 'calc(var(--field-height) - 1lh)',
isInCheckboxGroup: {
size: {
S: space(1),
M: space(1),
L: 2,
XL: 2
}
}
},
gridColumnStart: {
isInForm: 'field'
}
}, getAllowedOverrides());

const wrapper = style({
display: 'flex',
display: 'grid',
gridTemplateColumns: 'subgrid',
gridColumnStart: 1,
gridColumnEnd: -1,
position: 'relative',
columnGap: 'text-to-control',
alignItems: 'baseline',
width: 'fit',
font: controlFont(),
transition: 'colors',
color: {
default: baseColor('neutral'),
Expand All @@ -64,11 +96,8 @@ const wrapper = style({
forcedColors: 'GrayText'
}
},
gridColumnStart: {
isInForm: 'field'
},
disableTapHighlight: true
}, getAllowedOverrides());
});

export const box = style<RenderProps>({
...focusRing(),
Expand Down Expand Up @@ -126,7 +155,7 @@ export const iconStyles = style({
}
});

const iconSize = {
const smallerSize = {
S: 'XS',
M: 'S',
L: 'M',
Expand All @@ -137,7 +166,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 +177,66 @@ 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', isInCheckboxGroup}, 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={smallerSize[props.size || 'M']} className={iconStyles} />
}
{renderProps.isSelected && !renderProps.isIndeterminate &&
<CheckmarkIcon size={smallerSize[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>
<span className={style({gridColumnStart: 2})}>{children}</span>
</>
);
}}
</CheckboxButton>
<HelpText
size={isInCheckboxGroup ? smallerSize[props.size || 'M'] : props.size || 'M'}
styles={style({
gridColumnStart: {
default: 1,
isInCheckboxGroup: 2
},
paddingTop: 0
})({isInCheckboxGroup})}
isDisabled={isDisabled}
isInvalid={isInCheckboxGroup ? false : isInvalid}
description={props.description}
showErrorIcon>
{props.errorMessage}
</HelpText>
</>)}
</CheckboxField>
);
});
7 changes: 6 additions & 1 deletion packages/@react-spectrum/s2/src/CheckboxGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export const CheckboxGroup = forwardRef(function CheckboxGroup(props: CheckboxGr
size={size}
labelPosition={labelPosition}
labelAlign={labelAlign}
isQuiet // Make the label affect the width of the group
necessityIndicator={necessityIndicator}
contextualHelp={props.contextualHelp}>
{label}
Expand All @@ -119,7 +120,11 @@ export const CheckboxGroup = forwardRef(function CheckboxGroup(props: CheckboxGr
// Spectrum uses a fixed spacing value for horizontal,
// but the gap changes depending on t-shirt size in vertical.
columnGap: 16,
flexWrap: 'wrap'
flexWrap: {
orientation: {
horizontal: 'wrap'
}
}
})({orientation})}>
<FormContext.Provider value={{...formContext, size, isRequired: undefined}}>
<CheckboxContext.Provider value={{isEmphasized}}>
Expand Down
27 changes: 22 additions & 5 deletions packages/@react-spectrum/s2/src/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,11 +273,12 @@ export const Input = forwardRef(function Input(props: InputProps, ref: Forwarded
});

interface HelpTextProps extends FieldErrorProps {
size?: 'S' | 'M' | 'L' | 'XL',
size?: 'XS' | 'S' | 'M' | 'L' | 'XL',
isDisabled?: boolean,
isInvalid?: boolean, // TODO: export FieldErrorContext from RAC to get this.
description?: ReactNode,
showErrorIcon?: boolean
showErrorIcon?: boolean,
styles?: StyleString
}

export const helpTextStyles = style({
Expand Down Expand Up @@ -318,21 +319,37 @@ export function HelpText(props: HelpTextProps & {descriptionRef?: DOMRef<HTMLDiv
<Text
slot="description"
ref={domDescriptionRef}
className={helpTextStyles({size: props.size || 'M', isDisabled: props.isDisabled})}>
className={mergeStyles(helpTextStyles({size: props.size || 'M', isDisabled: props.isDisabled}), props.styles)}>
{props.description}
</Text>
);
}

if (!props.isInvalid) {
return null;
}

return (
<FieldError
{...props}
ref={domErrorRef}
className={renderProps => helpTextStyles({...renderProps, size: props.size || 'M', isDisabled: props.isDisabled})}>
className={renderProps => mergeStyles(helpTextStyles({...renderProps, size: props.size || 'M', isDisabled: props.isDisabled}), props.styles)}>
{composeRenderProps(props.children, (children, {validationErrors}) => (<>
{props.showErrorIcon &&
<CenterBaseline>
<AlertIcon />
<AlertIcon
// @ts-ignore - ts doesn't know that AlertIcon is an icon and not a raw SVG
styles={style({
size: {
size: {
Comment on lines +343 to +344
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol this made me do a double take

XS: 14,
S: 16,
M: 20,
L: 22,
XL: 26
}
}
})({size: props.size || 'M'})} />
</CenterBaseline>
}
<span>{children || validationErrors.join(' ')}</span>
Expand Down
Loading
Loading