Skip to content

Commit

Permalink
feat(SelectionChip): introduce 1.0 component
Browse files Browse the repository at this point in the history
- support event and state handling
- support transition states for when selected
- implement design API
- add tests and snapshots
  • Loading branch information
booc0mtaco committed Dec 3, 2024
1 parent f205e07 commit 2df8ff9
Show file tree
Hide file tree
Showing 7 changed files with 372 additions and 0 deletions.
92 changes: 92 additions & 0 deletions src/components/SelectionChip/SelectionChip.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*------------------------------------*\
# SELECTION CHIP
\*------------------------------------*/

/**
* SelectionChip
*/
.selection-chip {
position: relative;
display: inline-flex;
align-items: center;
gap: calc(var(--eds-size-1) / 16 * 1rem);
overflow: hidden;

padding: calc(var(--eds-size-1) / 16 * 1rem) calc(var(--eds-size-2) / 16 * 1rem);
border-radius: calc(var(--eds-border-radius-full) * 1px);

color: var(--eds-theme-color-text-utility-interactive-primary);
border: calc(var(--eds-border-width-sm) * 1px) solid var(--eds-theme-color-border-utility-default-low-emphasis);
background-color: var(--eds-theme-color-background-utility-interactive-no-emphasis);
}

.selection-chip__label {
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
}

.selection-chip__input {
position: absolute;
top: 0;
left: 0;
width: 1px;
height: 1px;
outline: none;
}

.selection-chip--has-icon {
padding-right: calc(var(--eds-size-2-and-half) / 16 * 1rem);
}

.selection-chip:has(.selection-chip__input:focus-visible) {
outline: none;
box-shadow: 0 0 0 calc(var(--eds-border-width-md) * 1px) white, 0 0 0 calc(var(--eds-border-width-lg) * 1px) var(--eds-theme-color-border-utility-focus);
}

@supports not selector(:focus-visible) {
.selection-chip:has(.selection-chip__input:focus) {
outline: none;
box-shadow: 0 0 0 calc(var(--eds-border-width-md) * 1px) white, 0 0 0 calc(var(--eds-border-width-lg) * 1px) var(--eds-theme-color-border-utility-focus);
}
}

/**
* Color theme tokens
*/

.selection-chip:hover {
background-color: var(--eds-theme-color-background-utility-default-no-emphasis-hover);
}

.selection-chip:active {
background-color: var(--eds-theme-color-background-utility-default-no-emphasis-active);
}

.selection-chip:has(.selection-chip__input:checked) {
border: calc(var(--eds-border-width-sm) * 1px) solid var(--eds-theme-color-border-utility-interactive);
box-shadow: inset 0 0 0 calc(var(--eds-border-width-sm) * 1px) var(--eds-theme-color-border-utility-interactive);
background-color: var(--eds-theme-color-background-utility-interactive-low-emphasis);
}

.selection-chip:has(.selection-chip__input:checked):hover {
border-color: var(--eds-theme-color-border-utility-interactive-hover);
background-color: var(--eds-theme-color-background-utility-interactive-low-emphasis-hover);
}

.selection-chip:has(.selection-chip__input:checked):active {
border-color: var(--eds-theme-color-border-utility-interactive-active);
background-color: var(--eds-theme-color-background-utility-interactive-low-emphasis-active);
}

.selection-chip:has(.selection-chip__input:focus-visible:checked) {
outline: none;
box-shadow: inset 0 0 0 calc(var(--eds-border-width-sm) * 1px) var(--eds-theme-color-border-utility-interactive), 0 0 0 calc(var(--eds-border-width-md) * 1px) white, 0 0 0 calc(var(--eds-border-width-lg) * 1px) var(--eds-theme-color-border-utility-focus);
}

@supports not selector(:focus-visible) {
.selection-chip:has(.selection-chip__input:focus:checked) {
outline: none;
box-shadow: inset 0 0 0 calc(var(--eds-border-width-sm) * 1px) var(--eds-theme-color-border-utility-interactive), 0 0 0 calc(var(--eds-border-width-md) * 1px) white, 0 0 0 calc(var(--eds-border-width-lg) * 1px) var(--eds-theme-color-border-utility-focus);
}
}
49 changes: 49 additions & 0 deletions src/components/SelectionChip/SelectionChip.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { StoryObj, Meta } from '@storybook/react';
import type React from 'react';

import { SelectionChip } from './SelectionChip';

export default {
title: 'Components/SelectionChip',
component: SelectionChip,
parameters: {
badges: ['intro-1.0', 'current-1.0'],
},
} as Meta<Args>;

type Args = React.ComponentProps<typeof SelectionChip>;

export const Default: StoryObj<Args> = {
args: {
label: 'Label',
},
};

export const Disabled: StoryObj<Args> = {
args: {
...Default.args,
isDisabled: true,
},
};

export const WithIcon: StoryObj<Args> = {
args: {
...Default.args,
leadingIcon: 'alarm-add',
},
};

export const ControlledChecked: StoryObj<Args> = {
args: {
...WithIcon.args,
checked: true,
onChange: () => {},
},
};

export const UncontrolledChecked: StoryObj<Args> = {
args: {
...WithIcon.args,
defaultChecked: true,
},
};
7 changes: 7 additions & 0 deletions src/components/SelectionChip/SelectionChip.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { generateSnapshots } from '@chanzuckerberg/story-utils';
import * as stories from './SelectionChip.stories';
import type { StoryFile } from '../../util/utility-types';

describe('<SelectionChip />', () => {
generateSnapshots(stories as StoryFile);
});
96 changes: 96 additions & 0 deletions src/components/SelectionChip/SelectionChip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import clsx from 'clsx';
import React, { forwardRef } from 'react';

import { useId } from '../../util/useId';
import type { ForwardedRefComponent } from '../../util/utility-types';

import Icon, { type IconName } from '../Icon';
import Text from '../Text';

import styles from './SelectionChip.module.css';

export type SelectionChipProps = {
// Component API
// Design API
/**
* Whether the chip is disabled or not
*/
isDisabled?: boolean;
/**
* Text used in the chip to give it a description
*/
label: string;
/**
* Leading icon for the chip
*/
leadingIcon: IconName;
/**
* Chip types (correspond to the equivalent input types)
*/
type?: 'checkbox' | 'radio';
} & Pick<
React.InputHTMLAttributes<HTMLInputElement>,
'id' | 'name' | 'className' | 'checked' | 'defaultChecked' | 'onChange'
>;

type SelectionChipRefProps = ForwardedRefComponent<
HTMLInputElement,
SelectionChipProps
>;

/**
* `import {SelectionChip} from "@chanzuckerberg/eds";`
*
* Compact, interactive UI elements used to make selections.
*/
export const SelectionChip: SelectionChipRefProps = forwardRef(
(
{
checked,
className,
defaultChecked,
id,
isDisabled,
label,
leadingIcon,
name,
onChange,
type = 'checkbox',
// Add other deferenced props to use
...other
},
ref,
) => {
const componentClassName = clsx(
styles['selection-chip'],
leadingIcon && styles['selection-chip--has-icon'],
className,
);

const generatedIdVar = useId();
const idVar = id || generatedIdVar;

return (
<label className={componentClassName} htmlFor={idVar} {...other}>
{leadingIcon && <Icon name={leadingIcon} purpose="decorative" />}
<Text
as="span"
className={styles['selection-chip__label']}
preset="button-md"
>
{label}
</Text>
<input
checked={checked}
className={styles['selection-chip__input']}
defaultChecked={defaultChecked}
id={idVar}
name={name}
onChange={onChange}
ref={ref}
type={type}
/>
</label>
);
},
);
126 changes: 126 additions & 0 deletions src/components/SelectionChip/__snapshots__/SelectionChip.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<SelectionChip /> ControlledChecked story renders snapshot 1`] = `
<label
class="selection-chip selection-chip--has-icon"
for=":r3:"
>
<svg
aria-hidden="true"
class="icon"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15 12h-2v-2c0-.55-.45-1-1-1s-1 .45-1 1v2H9c-.55 0-1 .45-1 1s.45 1 1 1h2v2c0 .55.45 1 1 1s1-.45 1-1v-2h2c.55 0 1-.45 1-1s-.45-1-1-1zm6.18-6.99L18.1 2.45c-.42-.35-1.05-.3-1.41.13-.35.42-.29 1.05.13 1.41l3.07 2.56c.42.35 1.05.3 1.41-.13.36-.42.3-1.05-.12-1.41zM4.1 6.55l3.07-2.56c.43-.36.49-.99.13-1.41-.35-.43-.98-.48-1.4-.13L2.82 5.01c-.42.36-.48.99-.12 1.41.35.43.98.48 1.4.13zM12 4c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9-4.03-9-9-9zm0 16c-3.86 0-7-3.14-7-7s3.14-7 7-7 7 3.14 7 7-3.14 7-7 7z"
/>
</svg>
<span
class="text text--button-md selection-chip__label"
>
Label
</span>
<input
checked=""
class="selection-chip__input"
id=":r3:"
type="checkbox"
/>
</label>
`;

exports[`<SelectionChip /> Default story renders snapshot 1`] = `
<label
class="selection-chip"
for=":r0:"
>
<span
class="text text--button-md selection-chip__label"
>
Label
</span>
<input
class="selection-chip__input"
id=":r0:"
type="checkbox"
/>
</label>
`;

exports[`<SelectionChip /> Disabled story renders snapshot 1`] = `
<label
class="selection-chip"
for=":r1:"
>
<span
class="text text--button-md selection-chip__label"
>
Label
</span>
<input
class="selection-chip__input"
id=":r1:"
type="checkbox"
/>
</label>
`;

exports[`<SelectionChip /> UncontrolledChecked story renders snapshot 1`] = `
<label
class="selection-chip selection-chip--has-icon"
for=":r4:"
>
<svg
aria-hidden="true"
class="icon"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15 12h-2v-2c0-.55-.45-1-1-1s-1 .45-1 1v2H9c-.55 0-1 .45-1 1s.45 1 1 1h2v2c0 .55.45 1 1 1s1-.45 1-1v-2h2c.55 0 1-.45 1-1s-.45-1-1-1zm6.18-6.99L18.1 2.45c-.42-.35-1.05-.3-1.41.13-.35.42-.29 1.05.13 1.41l3.07 2.56c.42.35 1.05.3 1.41-.13.36-.42.3-1.05-.12-1.41zM4.1 6.55l3.07-2.56c.43-.36.49-.99.13-1.41-.35-.43-.98-.48-1.4-.13L2.82 5.01c-.42.36-.48.99-.12 1.41.35.43.98.48 1.4.13zM12 4c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9-4.03-9-9-9zm0 16c-3.86 0-7-3.14-7-7s3.14-7 7-7 7 3.14 7 7-3.14 7-7 7z"
/>
</svg>
<span
class="text text--button-md selection-chip__label"
>
Label
</span>
<input
checked=""
class="selection-chip__input"
id=":r4:"
type="checkbox"
/>
</label>
`;

exports[`<SelectionChip /> WithIcon story renders snapshot 1`] = `
<label
class="selection-chip selection-chip--has-icon"
for=":r2:"
>
<svg
aria-hidden="true"
class="icon"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15 12h-2v-2c0-.55-.45-1-1-1s-1 .45-1 1v2H9c-.55 0-1 .45-1 1s.45 1 1 1h2v2c0 .55.45 1 1 1s1-.45 1-1v-2h2c.55 0 1-.45 1-1s-.45-1-1-1zm6.18-6.99L18.1 2.45c-.42-.35-1.05-.3-1.41.13-.35.42-.29 1.05.13 1.41l3.07 2.56c.42.35 1.05.3 1.41-.13.36-.42.3-1.05-.12-1.41zM4.1 6.55l3.07-2.56c.43-.36.49-.99.13-1.41-.35-.43-.98-.48-1.4-.13L2.82 5.01c-.42.36-.48.99-.12 1.41.35.43.98.48 1.4.13zM12 4c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9-4.03-9-9-9zm0 16c-3.86 0-7-3.14-7-7s3.14-7 7-7 7 3.14 7 7-3.14 7-7 7z"
/>
</svg>
<span
class="text text--button-md selection-chip__label"
>
Label
</span>
<input
class="selection-chip__input"
id=":r2:"
type="checkbox"
/>
</label>
`;
1 change: 1 addition & 0 deletions src/components/SelectionChip/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SelectionChip as default } from './SelectionChip';
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,4 @@ export type { AppNotificationProps as AppNotificationV2Props } from './component
*/
// https://headlessui.com/v1/react/transition
export { Transition } from '@headlessui/react';
export { default as SelectionChip } from './components/SelectionChip';

0 comments on commit 2df8ff9

Please sign in to comment.