Skip to content

Commit

Permalink
feat(Radio)!: introduce 2.0 component
Browse files Browse the repository at this point in the history
  • Loading branch information
booc0mtaco committed Mar 19, 2024
1 parent e6fffa2 commit 0455d0f
Show file tree
Hide file tree
Showing 6 changed files with 569 additions and 0 deletions.
29 changes: 29 additions & 0 deletions src/components/InputLabel/InputLabel-v2.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@import '../../design-tokens/mixins.css';

/*------------------------------------*\
# INPUT LABEL
\*------------------------------------*/

/**
* Text labeling the input component.
*/
.label {
color: var(--eds-theme-color-text-utility-neutral-primary);
}

/**
* Disabled variant of the input label.
*/
.label--disabled {
color: var(--eds-theme-color-text-utility-disabled-primary);
}

/**
* Input label size variants.
*/
.label--md {
font: var(--eds-theme-typography-body-sm);
}
.label--lg {
font: var(--eds-theme-typography-body-md);
}
55 changes: 55 additions & 0 deletions src/components/InputLabel/InputLabel-v2.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { StoryObj, Meta } from '@storybook/react';
import type { ComponentProps } from 'react';

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

export default {
title: 'Components/V2/InputLabel',
component: InputLabel,
args: {
children: 'Label',
},
parameters: {
badges: ['intro-1.0'],
},
} as Meta<Args>;

type Args = ComponentProps<typeof InputLabel>;

export const Default: StoryObj<Args> = {};

export const Medium: StoryObj<Args> = {
args: {
size: 'md',
},
};

export const LargeDisabled: StoryObj<Args> = {
args: {
disabled: true,
},
parameters: {
axe: {
disabledRules: ['color-contrast'],
},
},
};

export const MediumDisabled: StoryObj<Args> = {
args: {
disabled: true,
size: 'md',
},
parameters: {
axe: {
disabledRules: ['color-contrast'],
},
},
};

export const LongCopy: StoryObj<Args> = {
args: {
children:
'Long label lorem ipsum dolor sit amet, consectetur adipiscing elit. Ac id velit ut egestas arcu. Atmaecenas urna, risus donec praesent eu consectetur.',
},
};
58 changes: 58 additions & 0 deletions src/components/InputLabel/InputLabel-v2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import clsx from 'clsx';
import React from 'react';
import type { ReactNode } from 'react';
import type { Size } from '../../util/variant-types';
import styles from './InputLabel-v2.module.css';

export type InputLabelProps = {
/**
* Text to render in label.
*/
children: ReactNode;
/**
* Additional classnames passed in for styling.
*/
className?: string;
/**
* ID of input that label is associated with.
*/
htmlFor: string;
/**
* Size of the label.
*
* **Default is `"lg"`**.
*/
size?: Extract<Size, 'md' | 'lg'>;
/**
* Indicates disabled state of the input.
*/
disabled?: boolean;
};

/**
* `import {InputLabel} from "@chanzuckerberg/eds";`
*
* Label associated with an input element such as a radio or checkbox.
*/
export const InputLabel = ({
children,
className,
htmlFor,
size = 'lg',
disabled,
}: InputLabelProps) => {
const componentClassName = clsx(
styles['label'],
size === 'md' && styles['label--md'],
size === 'lg' && styles['label--lg'],
disabled && styles['label--disabled'],
className,
);
return (
<label className={componentClassName} htmlFor={htmlFor}>
{children}
</label>
);
};

InputLabel.displayName = 'InputLabel';
136 changes: 136 additions & 0 deletions src/components/Radio/Radio-v2.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*------------------------------------*\
# RADIO BUTTON
\*------------------------------------*/

/**
* A custom individual radio control
*/
.radio {
display: flex;
gap: 0.5rem;
}

/**
* Wraps the visually hidden radio input element and the visible sibling svg 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;
}
/**
* The visually hidden input element for the radio. The visual radio is provided by an svg element.
*/
.radio__input {
/* Removes default margins placed by browser for radioes. */
margin: 0;
/* Remove the radio from the page flow, positioning it on top of the SVG. */
position: absolute;
/* Set same dimensions as the RadioSvg element. */
height: 1.5rem;
width: 1.5rem;
/**
* Hide the input element visually.
* This ensures the radio is still "physically" present so that all users,
* especially on touch screen readers, still interact with the real radio element
* where it would naturally be present.
*/
opacity: 0;
}

/**
* The disabled status of the visually hidden input element.
*/
.radio__input:disabled {
/* Needed since the input element overlays the visible svg icon for user input and cursor. */
cursor: not-allowed;
pointer-events: none;
}

.radio__labels {
position: relative;
}

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

.radio__sub-label {
display: block;

color: var(--eds-theme-color-text-utility-neutral-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;

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

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

.radio__input:not(:checked):active + & {
color: var(--eds-theme-color-border-utility-neutral-medium-emphasis-active);
}

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

.radio__input:checked:hover ~ & {
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:disabled ~ & {
color: var(--eds-theme-color-border-utility-disabled);
}

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

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

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

/**
* Handling focus ring
*/
.radio__input:focus-visible + .radio__icon {
border: 0.125rem solid var(--eds-theme-color-border-utility-focus);
border-radius: var(--eds-border-radius-full);
}

@supports not selector(:focus-visible) {
.radio__input:focus + .radio__icon {
border: 0.125rem solid var(--eds-theme-color-border-utility-focus);
border-radius: var(--eds-border-radius-full);
}
}
111 changes: 111 additions & 0 deletions src/components/Radio/Radio-v2.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type { StoryObj, Meta } from '@storybook/react';
import React from 'react';

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

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

export default meta;

type Args = React.ComponentProps<typeof Radio>;
type Story = StoryObj<Args>;

export const Default: Story = {
args: {
name: 'option-1',
label: 'Option 1',
disabled: false,
checked: false,
readOnly: true,
},
};

export const Checked: Story = {
args: {
...Default.args,
name: 'option-checked',
checked: true,
readOnly: true,
},
};

export const Disabled: Story = {
args: {
...Default.args,
name: 'option-disabled',
disabled: true,
},
parameters: {
axe: {
disabledRules: ['color-contrast'],
},
},
};

export const Error: Story = {
args: {
...Default.args,
name: 'option-error',
isError: true,
},
};

export const ErrorAndChecked: Story = {
args: {
...Error.args,
name: 'option-error',
checked: true,
readOnly: true,
},
};

export const WithSublabel: Story = {
args: {
...Default.args,
subLabel: 'Some additional label text',
},
};

export const WithoutVisibleLabel: Story = {
args: {
...Default.args,
label: undefined,
'aria-label': 'unchecked radio button',
},
parameters: {
axe: {
disabledRules: ['color-contrast'],
},
},
};

export const LongLabels = {
render: () => {
const label = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit';

return (
<div
style={{
display: 'grid',
width: '20rem',
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
gap: '1rem',
}}
>
<Radio checked label={label} name="option-long-label" readOnly />
</div>
);
},
parameters: {
axe: {
disabledRules: ['color-contrast'],
},
},
};
Loading

0 comments on commit 0455d0f

Please sign in to comment.