Skip to content

Commit

Permalink
refactor(Radio): address component QA feedback (#1986)
Browse files Browse the repository at this point in the history
- stop using radio icons which can now be removed
- replace with styling using in-DOM elements (future proof for transitions)
- better application of color tokens
- add tests
- update snapshots
  • Loading branch information
booc0mtaco authored Jun 11, 2024
1 parent f6e280f commit 03edc20
Show file tree
Hide file tree
Showing 7 changed files with 576 additions and 123 deletions.
1 change: 1 addition & 0 deletions src/components/Checkbox/Checkbox-v2.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { StoryObj, Meta } from '@storybook/react';
import React from 'react';

import { Checkbox } from './Checkbox-v2';

const meta: Meta<typeof Checkbox> = {
Expand Down
16 changes: 5 additions & 11 deletions src/components/Checkbox/Checkbox-v2.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import clsx from 'clsx';
import React, { forwardRef } from 'react';
import type { ReactNode } from 'react';

import { FieldLabelV2 as FieldLabel } from '../..';
import useForwardedRef from '../../util/useForwardedRef';
import { useId } from '../../util/useId';
import type { EitherInclusive } from '../../util/utility-types';

import { InputLabel, type InputLabelProps } from '../InputLabel/InputLabel';
import Text from '../Text';

import styles from './Checkbox-v2.module.css';

type CheckboxLabelProps = InputLabelProps;
type CheckboxHTMLElementProps = Omit<
React.InputHTMLAttributes<HTMLInputElement>,
'checked' | 'id' | 'size'
Expand Down Expand Up @@ -98,12 +98,6 @@ const CheckboxInput = React.forwardRef<HTMLInputElement, CheckboxInputProps>(
},
);

const CheckboxLabel = ({ className, ...other }: CheckboxLabelProps) => {
const componentClassName = clsx(className);

return <InputLabel className={componentClassName} {...other} />;
};

/**
* `import {Checkbox} from "@chanzuckerberg/eds";`
*
Expand Down Expand Up @@ -137,9 +131,9 @@ export const Checkbox = Object.assign(
/>
<div className={styles['checkbox__labels']}>
{label && (
<CheckboxLabel disabled={disabled} htmlFor={checkboxId}>
<FieldLabel disabled={disabled} htmlFor={checkboxId}>
{label}
</CheckboxLabel>
</FieldLabel>
)}
{subLabel && (
<Text
Expand All @@ -156,7 +150,7 @@ export const Checkbox = Object.assign(
}),
{
Input: CheckboxInput,
Label: CheckboxLabel,
Label: FieldLabel,
},
);

Expand Down
179 changes: 130 additions & 49 deletions src/components/Radio/Radio-v2.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,47 @@
}

/**
* Wraps the visually hidden radio input element and the visible sibling svg element.
* Wraps the visually hidden radio input element and the visible sibling container element.
*/
.input__wrapper {
position: relative;
/* Centers the radio icon in the wrapper. */
display: inline-flex;
align-items: center;
/* Aligns the radio with the first line of the label. */
align-self: flex-start;
flex-shrink: 0;

width: 1.5rem;
height: 1.5rem;
}

.radio__container,
.radio__selected {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
}

.radio__container {
border: 0.125rem solid currentColor;
border-radius: 50%;
content: '';

width: 1.25rem;
height: 1.25rem;
}

.radio__selected {
width: 0.75rem;
height: 0.75rem;
border-radius: 50%;
background-color: transparent;

content: '';
pointer-events: none;
}

/**
* The visually hidden input element for the radio. The visual radio is provided by an svg element.
*/
Expand All @@ -39,13 +70,14 @@
* especially on touch screen readers, still interact with the real radio element
* where it would naturally be present.
*/
display: block;
opacity: 0;
}

/**
* The disabled status of the visually hidden input element.
*/
.radio__input:disabled {
.radio__input:disabled {
/* Needed since the input element overlays the visible svg icon for user input and cursor. */
cursor: not-allowed;
pointer-events: none;
Expand All @@ -55,85 +87,134 @@
position: relative;
}

/**
* Text that labels a radio input.
*/
.radio__label {
position: relative;
}

.radio__sub-label {
display: block;

color: var(--eds-theme-color-text-utility-default-secondary);
}

/**
* The visible radio svg icon element
*/
.radio__icon {
/* Creates space for the border so that there's no jitter when the focus border is visible. */
border: 0.125rem solid transparent;
.radio__input:not(:checked) {
& ~ .radio__container {
border-color: var(--eds-theme-color-border-utility-default-medium-emphasis);
}
}

.radio__input:not(:checked):hover {
& ~ .radio__container {
border-color: var(
--eds-theme-color-border-utility-default-medium-emphasis-hover
);
}
}

/* Theming when unchecked */
.radio__input:not(:checked) + & {
color: var(--eds-theme-color-border-utility-default-medium-emphasis);
.radio__input:not(:checked):active {
& ~ .radio__container {
border-color: var(
--eds-theme-color-border-utility-default-medium-emphasis-active
);
}
}

.radio__input:not(:checked):hover + & {
color: var(--eds-theme-color-border-utility-default-medium-emphasis-hover);
.radio__input:not(:checked):disabled {
pointer-events: none;
& ~ .radio__container {
border-color: var(--eds-theme-color-border-utility-disabled);
background-color: var(--eds-theme-color-background-utility-disabled-low-emphasis);
}
}

.radio__input:not(:checked):active + & {
color: var(--eds-theme-color-border-utility-default-medium-emphasis-active);
.radio__input:checked {
& ~ .radio__container {
border-color: var(
--eds-theme-color-background-utility-interactive-high-emphasis
);
}

.radio__input:not(:checked):disabled + & {
color: var(--eds-theme-color-border-utility-disabled);
pointer-events: none;
& ~ .radio__selected {
background-color: var(
--eds-theme-color-background-utility-interactive-high-emphasis
);
}
}

/* Theming when checked */
.radio__input:checked + & {
color: var(--eds-theme-color-background-utility-interactive-high-emphasis);
.radio__input:checked:hover {
& ~ .radio__container {
border-color: var(
--eds-theme-color-background-utility-interactive-high-emphasis-hover
);
}

.radio__input:checked:hover ~ & {
color: var(--eds-theme-color-background-utility-interactive-high-emphasis-hover);
& ~ .radio__selected {
background-color: var(
--eds-theme-color-background-utility-interactive-high-emphasis-hover
);
}
}

.radio__input:checked:active ~ & {
color: var(--eds-theme-color-background-utility-interactive-high-emphasis-active);
.radio__input:checked:active {
& ~ .radio__container {
border-color: var(
--eds-theme-color-background-utility-interactive-high-emphasis-active
);
}

.radio__input:checked:disabled ~ & {
color: var(--eds-theme-color-border-utility-disabled);
& ~ .radio__selected {
background-color: var(
--eds-theme-color-background-utility-interactive-high-emphasis-active
);
}
}

/**
* Error Theming
*/
.radio__input.radio--error ~ & {
color: var(--eds-theme-color-border-utility-critical);
.radio__input:checked:disabled {
& ~ .radio__container {
border-color: var(--eds-theme-color-border-utility-disabled);
}

& ~ .radio__selected {
background-color: var(--eds-theme-color-border-utility-disabled);
}
}

.radio__input.radio--error {
& ~ .radio__container {
border-color: var(--eds-theme-color-border-utility-critical);
}

& ~ .radio__selected {
background-color: var(--eds-theme-color-border-utility-critical);
}
}

.radio__input.radio--error:hover {
& ~ .radio__container {
border-color: var(--eds-theme-color-border-utility-critical-hover);
}

& ~ .radio__selected {
background-color: var(--eds-theme-color-border-utility-critical-hover);
}
}

.radio__input.radio--error:hover ~ & {
color: var(--eds-theme-color-border-utility-critical-hover);
.radio__input.radio--error:active {
& ~ .radio__container {
border-color: var(--eds-theme-color-border-utility-critical-active);
}

.radio__input.radio--error:active ~ & {
color: var(--eds-theme-color-border-utility-critical-active);
& ~ .radio__selected {
background-color: var(--eds-theme-color-border-utility-critical-active);
}
}

.radio__input:focus-visible + .radio__icon {
border: 0.125rem solid var(--eds-theme-color-border-utility-focus);
.radio__input:focus-visible + .radio__container {
outline: 0.125rem solid var(--eds-theme-color-border-utility-focus);
outline-offset: 0.125rem;
border-radius: calc(var(--eds-border-radius-full) * 1px);
}

@supports not selector(:focus-visible) {
.radio__input:focus + .radio__icon {
border: 0.125rem solid var(--eds-theme-color-border-utility-focus);
.radio__input:focus + .radio__container {
outline: 0.125rem solid var(--eds-theme-color-border-utility-focus);
outline-offset: 0.125rem;
border-radius: calc(var(--eds-border-radius-full) * 1px);
}
}
6 changes: 2 additions & 4 deletions src/components/Radio/Radio-v2.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@ import React from 'react';

import { Radio } from './Radio-v2';

const meta: Meta<typeof Radio> = {
export default {
title: 'Components/V2/Radio',
component: Radio,
parameters: {
layout: 'centered',
badges: ['intro-1.0', 'current-2.0'],
},
decorators: [(Story) => <div className="p-8">{Story()}</div>],
};

export default meta;
} as Meta<Args>;

type Args = React.ComponentProps<typeof Radio>;
type Story = StoryObj<Args>;
Expand Down
41 changes: 41 additions & 0 deletions src/components/Radio/Radio-v2.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { generateSnapshots } from '@chanzuckerberg/story-utils';
import type { StoryFile } from '@storybook/testing-react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { Radio } from './Radio-v2';
import * as stories from './Radio-v2.stories';

describe('<Radio /> (v2)', () => {
generateSnapshots(stories as StoryFile);

test('should toggle the radio with space', async () => {
const user = userEvent.setup();
const onChange = jest.fn();

function ControlledRadio() {
const [checked, setChecked] = React.useState(false);
const handleChange = () => {
setChecked(!checked);
onChange();
};

return (
<Radio
aria-label="test-radio"
checked={checked}
onChange={handleChange}
/>
);
}

render(<ControlledRadio />);
const radio = screen.getByRole('radio');
radio.focus();

await user.keyboard(' ');

expect(radio).toBeChecked();
expect(onChange).toHaveBeenCalledTimes(1);
});
});
Loading

0 comments on commit 03edc20

Please sign in to comment.