Skip to content

Commit 6d4a3f4

Browse files
authored
feat(SelectionChip): introduce 1.0 component (#2112)
- support event and state handling - support transition states for when selected - implement design API - add tests and snapshots
1 parent f205e07 commit 6d4a3f4

File tree

7 files changed

+376
-2
lines changed

7 files changed

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

src/components/SelectionChip/index.ts

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

0 commit comments

Comments
 (0)