diff --git a/.vscode/settings.json b/.vscode/settings.json index cdf6f51f0..105a7ffee 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,5 +12,8 @@ "typescript.validate.enable": true, "javascript.validate.enable": true, "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[css]": { + "editor.defaultFormatter": "vscode.css-language-features" + } } diff --git a/src/components/InputChip/InputChip.module.css b/src/components/InputChip/InputChip.module.css new file mode 100644 index 000000000..9351882af --- /dev/null +++ b/src/components/InputChip/InputChip.module.css @@ -0,0 +1,72 @@ +/*------------------------------------*\ + # INPUT CHIP +\*------------------------------------*/ + +/** + * InputChip + */ + +.input-chip { + display: inline-flex; +} + +.input-chip__label { + display: inline-flex; + padding: calc(var(--eds-size-1) / 16 * 1rem); + padding-right: 0; + gap: calc(var(--eds-size-1) / 16 * 1rem); + align-items: center; + justify-content: center; + + border: 1px solid var(--eds-theme-color-border-utility-default-low-emphasis); + border-right: none; + + border-radius: calc(var(--eds-border-radius-full) * 1px); + border-top-right-radius: 0; + border-bottom-right-radius: 0; + + color: var(--eds-theme-color-text-utility-default-primary); +} + +.input-chip .input-chip__action-button { + display: flex; + align-items: center; + padding: calc(var(--eds-size-1) / 16 * 1rem); + + border: 1px solid var(--eds-theme-color-border-utility-default-low-emphasis); + border-left: none; + border-radius: calc(var(--eds-border-radius-full) * 1px); + border-top-left-radius: 0; + border-bottom-left-radius: 0; + outline: none; + + color: var(--eds-theme-color-text-utility-default-primary); +} + +.input-chip--disabled { + pointer-events: none; +} + +/** + * Theme tokens + */ + +.input-chip__action-button:hover { + background-color: var(--eds-theme-color-background-utility-default-no-emphasis-hover); +} + +.input-chip__action-button:active { + background-color: var(--eds-theme-color-background-utility-default-no-emphasis-active); +} + +.input-chip__action-button:focus-visible { + border: none; + box-shadow: inset 0 0 0 2px var(--eds-theme-color-border-utility-focus); +} + +@supports not selector(:focus-visible) { + .input-chip__action-button:focus { + border: none; + box-shadow: inset 0 0 0 2px var(--eds-theme-color-border-utility-focus); + } +} \ No newline at end of file diff --git a/src/components/InputChip/InputChip.stories.ts b/src/components/InputChip/InputChip.stories.ts new file mode 100644 index 000000000..e2b4b4793 --- /dev/null +++ b/src/components/InputChip/InputChip.stories.ts @@ -0,0 +1,43 @@ +import type { StoryObj, Meta } from '@storybook/react'; +import type React from 'react'; + +import { InputChip } from './InputChip'; + +export default { + title: 'Components/InputChip', + component: InputChip, + parameters: { + badges: ['intro-1.0', 'current-1.0'], + }, + argTypes: { + onClick: { + control: false, + }, + leadingComponent: { + control: false, + }, + }, +} as Meta; + +type Args = React.ComponentProps; + +export const Default: StoryObj = { + args: { + label: 'Chip Label', + onClick: () => {}, + }, +}; + +export const WithLeadingIcon: StoryObj = { + args: { + ...Default.args, + leadingComponent: 'person-encircled', + }, +}; + +export const Disabled: StoryObj = { + args: { + ...Default.args, + isDisabled: true, + }, +}; diff --git a/src/components/InputChip/InputChip.test.ts b/src/components/InputChip/InputChip.test.ts new file mode 100644 index 000000000..3643541d6 --- /dev/null +++ b/src/components/InputChip/InputChip.test.ts @@ -0,0 +1,7 @@ +import { generateSnapshots } from '@chanzuckerberg/story-utils'; +import * as stories from './InputChip.stories'; +import type { StoryFile } from '../../util/utility-types'; + +describe('', () => { + generateSnapshots(stories as StoryFile); +}); diff --git a/src/components/InputChip/InputChip.tsx b/src/components/InputChip/InputChip.tsx new file mode 100644 index 000000000..f35859f8c --- /dev/null +++ b/src/components/InputChip/InputChip.tsx @@ -0,0 +1,81 @@ +import clsx from 'clsx'; +import React, { type MouseEventHandler } from 'react'; +import type { Size } from '../../util/variant-types'; +import Icon, { type IconName } from '../Icon'; +import Text from '../Text'; + +import styles from './InputChip.module.css'; + +export interface Props { + // Component API + /** + * CSS class names that can be appended to the component. + */ + className?: string; + // Design API + /** + * Whether the chip is in a non-interactive, disabled state + */ + isDisabled?: boolean; + /** + * Text used in the chip to give it a description + */ + label: string; + /** + * Leading glyph (icon) for the chip + */ + leadingComponent: IconName | React.ReactNode; // TODO: check that it only allows Avatar + /** + * click handler for the action button on the chip (ex: to dismiss or remove the chip from the screen) + */ + onClick?: MouseEventHandler; + /** + * The display size of the chip + */ + size?: Extract; +} + +/** + * `import {InputChip} from "@chanzuckerberg/eds";` + * + * Compact, interactive elements used to display user-generated information. + */ +export const InputChip = ({ + className, + isDisabled, + label, + leadingComponent, + onClick, + size = 'md', + ...other +}: Props) => { + const componentClassName = clsx( + styles['input-chip'], + isDisabled && styles[`input-chip--disabled`], + className, + ); + + return ( +
+
+ {leadingComponent && typeof leadingComponent === 'string' && ( + + )} + + {label} + +
+ +
+ ); +}; diff --git a/src/components/InputChip/__snapshots__/InputChip.test.ts.snap b/src/components/InputChip/__snapshots__/InputChip.test.ts.snap new file mode 100644 index 000000000..b20239ce1 --- /dev/null +++ b/src/components/InputChip/__snapshots__/InputChip.test.ts.snap @@ -0,0 +1,115 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` Default story renders snapshot 1`] = ` +
+
+ + Chip Label + +
+ +
+`; + +exports[` Disabled story renders snapshot 1`] = ` +
+
+ + Chip Label + +
+ +
+`; + +exports[` WithLeadingIcon story renders snapshot 1`] = ` +
+
+ + + Chip Label + +
+ +
+`; diff --git a/src/components/InputChip/index.ts b/src/components/InputChip/index.ts new file mode 100644 index 000000000..6e3678d4a --- /dev/null +++ b/src/components/InputChip/index.ts @@ -0,0 +1 @@ +export { InputChip as default } from './InputChip'; diff --git a/src/index.ts b/src/index.ts index 9988d3c3e..d4f9d0391 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,6 +36,7 @@ export { default as FieldNote } from './components/FieldNote'; export { default as Fieldset } from './components/Fieldset'; export { default as Icon } from './components/Icon'; export { default as InlineNotification } from './components/InlineNotification'; +export { default as InputChip } from './components/InputChip'; export { default as InputField } from './components/InputField'; export { default as Link } from './components/Link'; export { default as LoadingIndicator } from './components/LoadingIndicator';