Skip to content

Commit

Permalink
fix(Checkbox,Toggle): forward refs (#889)
Browse files Browse the repository at this point in the history
## πŸ“ Changes

- Forward refs for checkbox and toggle so that tooltips can be attached
to them
- Add examples of tooltip attached in storybook

## βœ… Checklist

- [x] Visuals are complete and match Figma
- [x] Code is complete and in accordance with our style guide
- [x] Design and theme tokens are audited for any relevant changes
- [x] Unit tests are written and passing
- [x] TSDoc is written or updated for any component API surface area
- [x] Stories in Storybook accompany any relevant component changes
- [x] Ensure no accessibility violations are reported in Storybook
- [x] Specs and documentation are up-to-date
- [x] Cross-browser check is performed (Chrome, Safari, Firefox)
- [x] Changeset is added
  • Loading branch information
stephenjwatkins authored Jan 3, 2024
1 parent 355bef7 commit 8fbfe3a
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 115 deletions.
5 changes: 5 additions & 0 deletions .changeset/silly-otters-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@easypost/easy-ui": patch
---

fix(Checkbox,Toggle): forward refs
178 changes: 98 additions & 80 deletions easy-ui-react/src/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import CheckIcon from "@easypost/easy-ui-icons/Check600";
import RemoveIcon from "@easypost/easy-ui-icons/Remove600";
import React, { ReactNode } from "react";
import { mergeRefs } from "@react-aria/utils";
import React, { ReactNode, forwardRef } from "react";
import { mergeProps, useCheckbox, useFocusRing, useHover } from "react-aria";
import { ValidationState, useToggleState } from "react-stately";
import { Icon } from "../Icon";
Expand Down Expand Up @@ -111,85 +112,102 @@ export type CheckboxProps = {
* </Checkbox>
* ```
*/
export function Checkbox(props: CheckboxProps) {
const {
children,
errorText,
isDisabled,
isIndeterminate,
isReadOnly,
isNested,
size = DEFAULT_SIZE,
validationState,
} = props;

const ref = React.useRef(null);

const state = useToggleState(props);
const { inputProps: inputPropsFromAria } = useCheckbox(props, state, ref);
const { isFocusVisible, focusProps } = useFocusRing();
const { isHovered, hoverProps } = useHover(props);

const isSelected = state.isSelected && !isIndeterminate;

const className = classNames(
styles.Checkbox,
isIndeterminate && styles.indeterminate,
isSelected && styles.selected,
isDisabled && styles.disabled,
isReadOnly && styles.readOnly,
isNested && styles.nested,
isFocusVisible && styles.focusVisible,
isHovered && styles.hovered,
styles[variationName("size", size)],
validationState === "invalid" && styles.invalid,
!children && styles.standalone,
);

const textVariant =
size === "lg" ? "subtitle1" : isNested ? "body2" : "body1";
const textColor = isDisabled
? "disabled"
: validationState === "invalid"
? "danger"
: "primary";

if (size === "lg" && isNested) {
console.warn("isNested is incompatible with lg Checkbox");
}

const RootComponent = children ? "label" : "span";
const rootProps = children ? hoverProps : {};
const inputProps = children
? mergeProps(inputPropsFromAria, focusProps)
: mergeProps(inputPropsFromAria, focusProps, hoverProps);

return (
<span className={className} data-testid="root">
<RootComponent className={styles.label} {...rootProps}>
<input {...inputProps} className={styles.input} ref={ref} />
<span className={styles.box}>
{(isIndeterminate || isSelected) && (
<span className={styles.check}>
{isIndeterminate ? (
<Icon symbol={RemoveIcon} size={size === "lg" ? "md" : "xs"} />
) : (
<Icon symbol={CheckIcon} size={size === "lg" ? "md" : "xs"} />
)}
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
(props, outsideRef) => {
const {
children,
errorText,
isDisabled,
isIndeterminate,
isReadOnly,
isNested,
size = DEFAULT_SIZE,
validationState,
defaultSelected: _defaultSelected,
isSelected: _isSelected,
name: _name,
onChange: _onChange,
value: _value,
...restProps
} = props;

const ref = React.useRef(null);

const state = useToggleState(props);
const { inputProps: inputPropsFromAria } = useCheckbox(props, state, ref);
const { isFocusVisible, focusProps } = useFocusRing();
const { isHovered, hoverProps } = useHover(props);

const isSelected = state.isSelected && !isIndeterminate;

const className = classNames(
styles.Checkbox,
isIndeterminate && styles.indeterminate,
isSelected && styles.selected,
isDisabled && styles.disabled,
isReadOnly && styles.readOnly,
isNested && styles.nested,
isFocusVisible && styles.focusVisible,
isHovered && styles.hovered,
styles[variationName("size", size)],
validationState === "invalid" && styles.invalid,
!children && styles.standalone,
);

const textVariant =
size === "lg" ? "subtitle1" : isNested ? "body2" : "body1";
const textColor = isDisabled
? "disabled"
: validationState === "invalid"
? "danger"
: "primary";

if (size === "lg" && isNested) {
console.warn("isNested is incompatible with lg Checkbox");
}

const RootComponent = children ? "label" : "span";
const rootProps = children ? hoverProps : {};
const inputProps = children
? mergeProps(restProps, inputPropsFromAria, focusProps)
: mergeProps(restProps, inputPropsFromAria, focusProps, hoverProps);

return (
<span className={className} data-testid="root">
<RootComponent className={styles.label} {...rootProps}>
<input
{...inputProps}
className={styles.input}
ref={mergeRefs(ref, outsideRef)}
/>
<span className={styles.box}>
{(isIndeterminate || isSelected) && (
<span className={styles.check}>
{isIndeterminate ? (
<Icon
symbol={RemoveIcon}
size={size === "lg" ? "md" : "xs"}
/>
) : (
<Icon symbol={CheckIcon} size={size === "lg" ? "md" : "xs"} />
)}
</span>
)}
</span>
{children && (
<span className={styles.text}>
<Text variant={textVariant} color={textColor}>
{children}
</Text>
</span>
)}
</span>
{children && (
<span className={styles.text}>
<Text variant={textVariant} color={textColor}>
{children}
</Text>
</span>
</RootComponent>
{validationState === "invalid" && errorText && (
<SelectorErrorTooltip content={errorText} />
)}
</RootComponent>
{validationState === "invalid" && errorText && (
<SelectorErrorTooltip content={errorText} />
)}
</span>
);
}
</span>
);
},
);

Checkbox.displayName = "Checkbox";
89 changes: 54 additions & 35 deletions easy-ui-react/src/Toggle/Toggle.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { mergeRefs } from "@react-aria/utils";
import { AriaLabelingProps } from "@react-types/shared";
import React, { ReactNode } from "react";
import React, { ReactNode, forwardRef } from "react";
import { mergeProps, useFocusRing, useHover, useSwitch } from "react-aria";
import { useToggleState } from "react-stately";
import { Text } from "../Text";
Expand Down Expand Up @@ -84,41 +85,59 @@ export type ToggleProps = AriaLabelingProps & {
* <Toggle isDisabled>Toggle item</Toggle>
* ```
*/
export function Toggle(props: ToggleProps) {
const { children, isDisabled } = props;
export const Toggle = forwardRef<HTMLInputElement, ToggleProps>(
(props, externalRef) => {
const {
children,
isDisabled,
defaultSelected: _defaultSelected,
isReadOnly: _isReadOnly,
isSelected: _isSelected,
name: _name,
onChange: _onChange,
value: _value,
...restProps
} = props;

const ref = React.useRef(null);
const state = useToggleState(props);
const { inputProps: inputPropsFromSwitch } = useSwitch(props, state, ref);
const { isFocusVisible, focusProps } = useFocusRing();
const { isHovered, hoverProps } = useHover(props);
const isSelected = state.isSelected;
const ref = React.useRef(null);
const state = useToggleState(props);
const { inputProps: inputPropsFromSwitch } = useSwitch(props, state, ref);
const { isFocusVisible, focusProps } = useFocusRing();
const { isHovered, hoverProps } = useHover(props);
const isSelected = state.isSelected;

const className = classNames(styles.Toggle, !children && styles.standalone);
const textColor = isDisabled ? "disabled" : "primary";
const className = classNames(styles.Toggle, !children && styles.standalone);
const textColor = isDisabled ? "disabled" : "primary";

const RootComponent = children ? "label" : "span";
const rootProps = children ? hoverProps : {};
const inputProps = children
? mergeProps(inputPropsFromSwitch, focusProps)
: mergeProps(inputPropsFromSwitch, focusProps, hoverProps);
const RootComponent = children ? "label" : "span";
const rootProps = children ? hoverProps : {};
const inputProps = children
? mergeProps(restProps, inputPropsFromSwitch, focusProps)
: mergeProps(restProps, inputPropsFromSwitch, focusProps, hoverProps);

return (
<RootComponent {...rootProps} className={className}>
<input {...inputProps} className={styles.input} ref={ref} />
<Switch
isDisabled={isDisabled}
isFocusVisible={isFocusVisible}
isHovered={isHovered}
isSelected={isSelected}
/>
{children && (
<span className={styles.text}>
<Text variant="body1" color={textColor}>
{children}
</Text>
</span>
)}
</RootComponent>
);
}
return (
<RootComponent {...rootProps} className={className}>
<input
{...inputProps}
className={styles.input}
ref={mergeRefs(ref, externalRef)}
/>
<Switch
isDisabled={isDisabled}
isFocusVisible={isFocusVisible}
isHovered={isHovered}
isSelected={isSelected}
/>
{children && (
<span className={styles.text}>
<Text variant="body1" color={textColor}>
{children}
</Text>
</span>
)}
</RootComponent>
);
},
);

Toggle.displayName = "Toggle";
16 changes: 16 additions & 0 deletions easy-ui-react/src/Tooltip/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Meta, StoryObj } from "@storybook/react";
import { startCase } from "lodash";
import React from "react";
import { Button } from "../Button";
import { Checkbox } from "../Checkbox";
import { Toggle } from "../Toggle";
import {
OverlayLayoutDecorator,
overlayPlacements,
Expand Down Expand Up @@ -102,6 +104,20 @@ export const ButtonTrigger: Story = {
},
};

export const CheckboxTrigger: Story = {
render: Template.bind({}),
args: {
children: <Checkbox>Hover or focus me</Checkbox>,
},
};

export const ToggleTrigger: Story = {
render: Template.bind({}),
args: {
children: <Toggle>Hover or focus me</Toggle>,
},
};

export const Controls: Story = {
render: Template.bind({}),
};

0 comments on commit 8fbfe3a

Please sign in to comment.