Skip to content

Commit 2324c3e

Browse files
committed
feat(SelectionChip): introduce 1.0 component
- support event and state handling - support transition states for when selected - implement design API - add tests and snapshots
1 parent f205e07 commit 2324c3e

File tree

7 files changed

+334
-0
lines changed

7 files changed

+334
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*------------------------------------*\
2+
# SELECTION CHIP
3+
\*------------------------------------*/
4+
5+
/**
6+
* SelectionChip
7+
*/
8+
.selection-chip {
9+
display: inline-flex;
10+
align-items: center;
11+
gap: calc(var(--eds-size-1) / 16 * 1rem);
12+
13+
padding: calc(var(--eds-size-1) / 16 * 1rem) calc(var(--eds-size-2) / 16 * 1rem);
14+
border-radius: calc(var(--eds-border-radius-full) * 1px);
15+
16+
color: var(--eds-theme-color-text-utility-interactive-primary);
17+
/* TODO-AH: use token for 1px border */
18+
border: 1px solid var(--eds-theme-color-border-utility-default-low-emphasis);
19+
background-color: var(--eds-theme-color-background-utility-interactive-no-emphasis);
20+
}
21+
22+
.selection-chip__label {
23+
user-select: none;
24+
-webkit-user-select: none;
25+
-webkit-touch-callout: none;
26+
}
27+
28+
.selection-chip__input {
29+
/* TODO-AH: handle focus */
30+
display: none;
31+
}
32+
33+
/** TODO-AH: make sure spacing on right is 20px equivalent when icon present */
34+
35+
/**
36+
* Color theme tokens
37+
*/
38+
39+
.selection-chip:hover {
40+
background-color: var(--eds-theme-color-background-utility-default-no-emphasis-hover);
41+
}
42+
43+
.selection-chip:active {
44+
background-color: var(--eds-theme-color-background-utility-default-no-emphasis-active);
45+
}
46+
47+
.selection-chip:has(.selection-chip__input:checked) {
48+
/* TODO-AH: use token for 2px border */
49+
border: 1px solid var(--eds-theme-color-border-utility-interactive);
50+
box-shadow: inset 0 0 0 1px var(--eds-theme-color-border-utility-interactive);
51+
background-color: var(--eds-theme-color-background-utility-interactive-low-emphasis);
52+
}
53+
54+
.selection-chip:has(.selection-chip__input:checked):hover {
55+
border-color: var(--eds-theme-color-border-utility-interactive-hover);
56+
background-color: var(--eds-theme-color-background-utility-interactive-low-emphasis-hover);
57+
}
58+
59+
.selection-chip:has(.selection-chip__input:checked):active {
60+
border-color: var(--eds-theme-color-border-utility-interactive-active);
61+
background-color: var(--eds-theme-color-background-utility-interactive-low-emphasis-active);
62+
}
63+
64+
/** TODO-AH: add in selected border color. Also confirm how focus ring spacing should work */
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { BADGE } from '@geometricpanda/storybook-addon-badges';
2+
import type { StoryObj, Meta } from '@storybook/react';
3+
import type React from 'react';
4+
5+
import { SelectionChip } from './SelectionChip';
6+
7+
export default {
8+
title: 'Components/SelectionChip',
9+
component: SelectionChip,
10+
parameters: {
11+
// TODO-AH: add in the version details
12+
badges: [BADGE.BETA],
13+
},
14+
} as Meta<Args>;
15+
16+
type Args = React.ComponentProps<typeof SelectionChip>;
17+
18+
export const Default: StoryObj<Args> = {
19+
args: {
20+
label: 'Label',
21+
},
22+
};
23+
24+
export const Disabled: StoryObj<Args> = {
25+
args: {
26+
...Default.args,
27+
isDisabled: true,
28+
},
29+
};
30+
31+
export const WithIcon: StoryObj<Args> = {
32+
args: {
33+
...Default.args,
34+
leadingIcon: 'alarm-add',
35+
},
36+
};
37+
38+
export const ControlledChecked: StoryObj<Args> = {
39+
args: {
40+
...WithIcon.args,
41+
checked: true,
42+
onChange: () => {},
43+
},
44+
};
45+
46+
export const UncontrolledChecked: StoryObj<Args> = {
47+
args: {
48+
...WithIcon.args,
49+
defaultChecked: true,
50+
},
51+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { generateSnapshots } from '@chanzuckerberg/story-utils';
2+
import * as stories from './SelectionChip.stories';
3+
import type { StoryFile } from '../../util/utility-types';
4+
5+
describe('<SelectionChip />', () => {
6+
generateSnapshots(stories as StoryFile);
7+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import clsx from 'clsx';
2+
import React from 'react';
3+
4+
import { useId } from '../../util/useId';
5+
import Icon, { type IconName } from '../Icon';
6+
import Text from '../Text';
7+
8+
import styles from './SelectionChip.module.css';
9+
10+
export type SelectionChipProps = {
11+
// Component API
12+
// Design API
13+
/**
14+
* Whether the chip is disabled or not
15+
*/
16+
isDisabled?: boolean;
17+
/**
18+
* Text used in the chip to give it a description
19+
*/
20+
label: string;
21+
/**
22+
* Leading icon for the chip
23+
*/
24+
leadingIcon: IconName;
25+
/**
26+
* Chip types (correspond to the equivalent input types)
27+
*/
28+
type?: 'checkbox' | 'radio';
29+
} & Pick<
30+
React.InputHTMLAttributes<HTMLInputElement>,
31+
'id' | 'name' | 'className' | 'checked' | 'defaultChecked' | 'onChange'
32+
>;
33+
34+
/**
35+
* `import {SelectionChip} from "@chanzuckerberg/eds";`
36+
*
37+
* Compact, interactive UI elements used to make selections.
38+
*
39+
*/
40+
// TODO-AH: hook up using forwardRef
41+
export const SelectionChip = ({
42+
checked,
43+
className,
44+
defaultChecked,
45+
id,
46+
isDisabled,
47+
label,
48+
leadingIcon,
49+
name,
50+
onChange,
51+
type = 'checkbox',
52+
// Add other deferenced props to use
53+
...other
54+
}: SelectionChipProps) => {
55+
const componentClassName = clsx(styles['selection-chip'], className);
56+
57+
const generatedIdVar = useId();
58+
const idVar = id || generatedIdVar;
59+
60+
// TODO-AH: add in a context aware control based on the selection type: radio or checkbox
61+
// TODO-AH: use focus ring class
62+
63+
return (
64+
<label className={componentClassName} htmlFor={idVar} {...other}>
65+
{leadingIcon && <Icon name={leadingIcon} purpose="decorative" />}
66+
<Text
67+
as="span"
68+
className={styles['selection-chip__label']}
69+
preset="button-md"
70+
>
71+
{label}
72+
</Text>
73+
<input
74+
checked={checked}
75+
className={styles['selection-chip__input']}
76+
defaultChecked={defaultChecked}
77+
id={idVar}
78+
name={name}
79+
onChange={onChange}
80+
type={type}
81+
/>
82+
</label>
83+
);
84+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`<SelectionChip /> ControlledChecked story renders snapshot 1`] = `
4+
<label
5+
class="selection-chip"
6+
for=":r3:"
7+
>
8+
<svg
9+
aria-hidden="true"
10+
class="icon"
11+
fill="currentColor"
12+
viewBox="0 0 24 24"
13+
xmlns="http://www.w3.org/2000/svg"
14+
>
15+
<path
16+
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"
17+
/>
18+
</svg>
19+
<span
20+
class="text text--button-md selection-chip__label"
21+
>
22+
Label
23+
</span>
24+
<input
25+
checked=""
26+
class="selection-chip__input"
27+
id=":r3:"
28+
type="checkbox"
29+
/>
30+
</label>
31+
`;
32+
33+
exports[`<SelectionChip /> Default story renders snapshot 1`] = `
34+
<label
35+
class="selection-chip"
36+
for=":r0:"
37+
>
38+
<span
39+
class="text text--button-md selection-chip__label"
40+
>
41+
Label
42+
</span>
43+
<input
44+
class="selection-chip__input"
45+
id=":r0:"
46+
type="checkbox"
47+
/>
48+
</label>
49+
`;
50+
51+
exports[`<SelectionChip /> Disabled story renders snapshot 1`] = `
52+
<label
53+
class="selection-chip"
54+
for=":r1:"
55+
>
56+
<span
57+
class="text text--button-md selection-chip__label"
58+
>
59+
Label
60+
</span>
61+
<input
62+
class="selection-chip__input"
63+
id=":r1:"
64+
type="checkbox"
65+
/>
66+
</label>
67+
`;
68+
69+
exports[`<SelectionChip /> UncontrolledChecked story renders snapshot 1`] = `
70+
<label
71+
class="selection-chip"
72+
for=":r4:"
73+
>
74+
<svg
75+
aria-hidden="true"
76+
class="icon"
77+
fill="currentColor"
78+
viewBox="0 0 24 24"
79+
xmlns="http://www.w3.org/2000/svg"
80+
>
81+
<path
82+
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"
83+
/>
84+
</svg>
85+
<span
86+
class="text text--button-md selection-chip__label"
87+
>
88+
Label
89+
</span>
90+
<input
91+
checked=""
92+
class="selection-chip__input"
93+
id=":r4:"
94+
type="checkbox"
95+
/>
96+
</label>
97+
`;
98+
99+
exports[`<SelectionChip /> WithIcon story renders snapshot 1`] = `
100+
<label
101+
class="selection-chip"
102+
for=":r2:"
103+
>
104+
<svg
105+
aria-hidden="true"
106+
class="icon"
107+
fill="currentColor"
108+
viewBox="0 0 24 24"
109+
xmlns="http://www.w3.org/2000/svg"
110+
>
111+
<path
112+
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"
113+
/>
114+
</svg>
115+
<span
116+
class="text text--button-md selection-chip__label"
117+
>
118+
Label
119+
</span>
120+
<input
121+
class="selection-chip__input"
122+
id=":r2:"
123+
type="checkbox"
124+
/>
125+
</label>
126+
`;

src/components/SelectionChip/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { SelectionChip as default } from './SelectionChip';

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,4 @@ export type { AppNotificationProps as AppNotificationV2Props } from './component
7777
*/
7878
// https://headlessui.com/v1/react/transition
7979
export { Transition } from '@headlessui/react';
80+
export { default as SelectionChip } from './components/SelectionChip';

0 commit comments

Comments
 (0)