From 95b65bdfd87ba4c6a1e9fd20a6cdcf0d5d7f95bf Mon Sep 17 00:00:00 2001 From: Shadi Date: Thu, 21 Jul 2022 18:23:07 -0500 Subject: [PATCH] feat(form-pill-group): add error variant, disabled pills, and update styles (#2526) Co-authored-by: shleewhite --- .changeset/hungry-plants-repair.md | 6 + .changeset/real-moles-exercise.md | 6 + .changeset/sour-buckets-care.md | 6 + .../form-pill-group/__tests__/index.spec.tsx | 78 ++- .../components/form-pill-group/package.json | 2 +- .../form-pill-group/src/FormPill.styles.ts | 160 +++++ .../form-pill-group/src/FormPill.tsx | 139 ++--- .../form-pill-group/src/FormPillButton.tsx | 81 +++ .../form-pill-group/src/FormPillGroup.tsx | 7 +- .../form-pill-group/src/PillCloseIcon.tsx | 37 +- .../components/form-pill-group/src/types.ts | 4 + .../stories/customization.stories.tsx | 71 +++ .../form-pill-group/stories/index.stories.tsx | 207 ++----- .../paste-core/primitives/box/package.json | 3 +- .../primitives/box/src/PseudoPropStyles.ts | 1 + .../paste-core/primitives/box/src/types.ts | 1 + .../__snapshots__/index.test.tsx.snap | 6 + .../tokens/global/background-color.yml | 3 + .../tokens/global/text-color.yml | 15 + .../themes/dark/global/background-color.yml | 3 + .../tokens/themes/dark/global/text-color.yml | 3 + .../component-examples/DisplayPillGroup.ts | 197 ++++++ .../src/component-examples/FormPillGroup.tsx | 296 ++++++++- .../components/FormPillVsDisplayPillTable.tsx | 118 +++- .../components/display-pill-group/index.mdx | 287 ++++++--- .../components/form-pill-group/index.mdx | 579 ++++++++---------- yarn.lock | 1 + 27 files changed, 1592 insertions(+), 725 deletions(-) create mode 100644 .changeset/hungry-plants-repair.md create mode 100644 .changeset/real-moles-exercise.md create mode 100644 .changeset/sour-buckets-care.md create mode 100644 packages/paste-core/components/form-pill-group/src/FormPill.styles.ts create mode 100644 packages/paste-core/components/form-pill-group/src/FormPillButton.tsx create mode 100644 packages/paste-core/components/form-pill-group/src/types.ts create mode 100644 packages/paste-core/components/form-pill-group/stories/customization.stories.tsx create mode 100644 packages/paste-website/src/component-examples/DisplayPillGroup.ts diff --git a/.changeset/hungry-plants-repair.md b/.changeset/hungry-plants-repair.md new file mode 100644 index 0000000000..29c8ebf948 --- /dev/null +++ b/.changeset/hungry-plants-repair.md @@ -0,0 +1,6 @@ +--- +'@twilio-paste/design-tokens': minor +'@twilio-paste/core': minor +--- + +[Design Tokens] add color-background-error-strongest and color-text-error-stronger tokens diff --git a/.changeset/real-moles-exercise.md b/.changeset/real-moles-exercise.md new file mode 100644 index 0000000000..0e0dde535e --- /dev/null +++ b/.changeset/real-moles-exercise.md @@ -0,0 +1,6 @@ +--- +'@twilio-paste/box': patch +'@twilio-paste/core': patch +--- + +[Box] add missing disabled prop and \_focus_hover pseudoSelector prop diff --git a/.changeset/sour-buckets-care.md b/.changeset/sour-buckets-care.md new file mode 100644 index 0000000000..21cdb59911 --- /dev/null +++ b/.changeset/sour-buckets-care.md @@ -0,0 +1,6 @@ +--- +'@twilio-paste/form-pill-group': minor +'@twilio-paste/core': minor +--- + +[Form Pill Group] add error variant, disabled pills, and update styles diff --git a/packages/paste-core/components/form-pill-group/__tests__/index.spec.tsx b/packages/paste-core/components/form-pill-group/__tests__/index.spec.tsx index 1d29e1436c..ad57f7d167 100644 --- a/packages/paste-core/components/form-pill-group/__tests__/index.spec.tsx +++ b/packages/paste-core/components/form-pill-group/__tests__/index.spec.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; import {render, fireEvent, screen} from '@testing-library/react'; - import {CustomizationProvider} from '@twilio-paste/customization'; import {useFormPillState, FormPillGroup, FormPill} from '../src'; -import {Basic, SelectableAndRemovable, CustomFormPillGroup} from '../stories/index.stories'; +import {Basic, SelectableAndDismissable} from '../stories/index.stories'; +import {CustomFormPillGroup} from '../stories/customization.stories'; const CustomElementFormPillGroup: React.FC = () => { const pillState = useFormPillState(); @@ -35,7 +35,7 @@ const I18nProp: React.FC = () => { aria-label="Votre sports favoris:" i18nKeyboardControls="Appuyez sur Supprimer ou Retour arrière pour supprimer. Appuyez sur Entrée pour basculer la sélection." > - + Le tennis @@ -46,30 +46,27 @@ const I18nProp: React.FC = () => { describe('FormPillGroup', () => { describe('Rendered shape', () => { it('has the correct aria attributes and semantic HTML', () => { - const {getByTestId, getByText} = render(); - expect(getByText('Tennis')).toBeDefined(); + render(); + expect(screen.getByText('Default pill')).toBeDefined(); - const group = getByTestId('form-pill-group'); - expect(group.getAttribute('aria-label')).toBe('Your favorite sports:'); + const group = screen.getByTestId('form-pill-group'); + expect(group.getAttribute('aria-label')).toBe('Basic pills:'); expect(group.tagName).toBe('DIV'); expect(group.getAttribute('role')).toBe('listbox'); - const pill = getByTestId('form-pill'); + const pill = screen.getByTestId('form-pill-0'); expect(pill.tagName).toBe('BUTTON'); expect(pill.getAttribute('role')).toBe('option'); expect(pill.getAttribute('aria-selected')).toBe('false'); - - const pillSelected = getByTestId('form-pill-selected'); - expect(pillSelected.getAttribute('aria-selected')).toBe('true'); }); }); describe('Selecting and Removing', () => { it('can select and navigate pills', () => { - const {getByTestId} = render(); + render(); // Get the first pill - const firstPill = getByTestId('form-pill-0'); + const firstPill = screen.getByTestId('form-pill-0'); // Click it and make sure it selected fireEvent.click(firstPill); expect(firstPill.getAttribute('aria-selected')).toBe('true'); @@ -82,19 +79,22 @@ describe('FormPillGroup', () => { fireEvent.keyDown(firstPill, {key: 'Enter', code: 'Enter'}); expect(firstPill.getAttribute('aria-selected')).toBe('true'); + // Make sure it deselects on Enter key + fireEvent.keyDown(firstPill, {key: 'Enter', code: 'Enter'}); + expect(firstPill.getAttribute('aria-selected')).toBe('false'); + // Make sure we can navigate with right arrow fireEvent.keyDown(firstPill, {key: 'ArrowRight', code: 'ArrowRight'}); if (document.activeElement == null) { throw new Error('document.activeElement is null'); } expect(document.activeElement.getAttribute('data-testid')).toBe('form-pill-1'); - expect(document.activeElement.getAttribute('aria-selected')).toBe('false'); // Move right again and check for selection fireEvent.keyDown(document.activeElement, {key: 'ArrowRight', code: 'ArrowRight'}); expect(document.activeElement.getAttribute('data-testid')).toBe('form-pill-2'); fireEvent.keyDown(document.activeElement, {key: 'Enter', code: 'Enter'}); - expect(document.activeElement.getAttribute('aria-selected')).toBe('false'); + expect(document.activeElement.getAttribute('aria-selected')).toBe('true'); // Try moving left this time fireEvent.keyDown(document.activeElement, {key: 'ArrowLeft', code: 'ArrowLeft'}); @@ -103,24 +103,24 @@ describe('FormPillGroup', () => { // Loop movement fireEvent.keyDown(document.activeElement, {key: 'ArrowLeft', code: 'ArrowLeft'}); fireEvent.keyDown(document.activeElement, {key: 'ArrowLeft', code: 'ArrowLeft'}); - expect(document.activeElement.getAttribute('data-testid')).toBe('form-pill-3'); + expect(document.activeElement.getAttribute('data-testid')).toBe('form-pill-5'); }); it('can remove pills', () => { - const {getByRole} = render(); + render(); /* Test click to remove */ - const firstPill = getByRole('option', {name: 'Tennis'}); - const firstPillX = firstPill.querySelector('[data-paste-element="BOX"]'); + const firstPill = screen.getByRole('option', {name: 'Default pill'}).parentElement; + const firstPillX = firstPill?.querySelector('[data-paste-element="FORM_PILL_CLOSE"]'); fireEvent.click(firstPillX as Element); expect(firstPill).not.toBeInTheDocument(); /* Test keyboard to remove */ - const secondPill = getByRole('option', {name: 'Baseball'}); + const secondPill = screen.getByRole('option', {name: 'Pill with icon'}); fireEvent.keyDown(secondPill, {key: 'Delete', code: 'Delete'}); expect(secondPill).not.toBeInTheDocument(); - const thirdPill = getByRole('option', {name: 'Football'}); + const thirdPill = screen.getByRole('option', {name: 'Pill with avatar'}); fireEvent.keyDown(thirdPill, {key: 'Backspace', code: 'Backspace'}); expect(thirdPill).not.toBeInTheDocument(); }); @@ -128,33 +128,33 @@ describe('FormPillGroup', () => { describe('Customization', () => { it('should set an element data attribute for FormPillGroup & FormPill', () => { - const {getByTestId} = render(); - const group = getByTestId('form-pill-group'); + render(); + const group = screen.getByTestId('form-pill-group'); expect(group.getAttribute('data-paste-element')).toEqual('FORM_PILL_GROUP'); - const pill = getByTestId('form-pill'); + const pill = screen.getByTestId('form-pill-0'); expect(pill.getAttribute('data-paste-element')).toEqual('FORM_PILL'); }); it('should set a custom element data attribute for FormPillGroup & FormPill', () => { - const {getByTestId} = render(); - const group = getByTestId('form-pill-group'); + render(); + const group = screen.getByTestId('form-pill-group'); expect(group.getAttribute('data-paste-element')).toEqual('CUSTOM_PILL_GROUP'); - const pill = getByTestId('form-pill'); + const pill = screen.getByTestId('form-pill'); expect(pill.getAttribute('data-paste-element')).toEqual('CUSTOM_PILL'); }); it('should add custom styles to FormPillGroup & FormPill', () => { - const {getByTestId} = render(); + render(); - const group = getByTestId('form-pill-group'); + const group = screen.getByTestId('form-pill-group'); expect(group).toHaveStyleRule('margin', '0.75rem'); - const pill = getByTestId('form-pill'); + const pill = screen.getByTestId('form-pill'); expect(pill).toHaveStyleRule('background-color', 'rgb(245, 240, 252)'); }); it('should add custom styles to custom element FormPillGroup & FormPill', () => { - const {getByTestId} = render( + render( { ); - const group = getByTestId('form-pill-group'); + const group = screen.getByTestId('form-pill-group'); expect(group).toHaveStyleRule('margin', '0.75rem'); - const pill = getByTestId('form-pill'); + const pill = screen.getByTestId('form-pill'); expect(pill).toHaveStyleRule('background-color', 'rgb(231, 220, 250)'); }); }); @@ -196,5 +196,17 @@ describe('FormPillGroup', () => { ); expect(keyboardControlsText).toBeDefined(); }); + + it('should have default error text', () => { + render(); + const errorLabel = screen.getAllByText('(error)'); + expect(errorLabel).toBeDefined(); + }); + + it('should use i18nErrorLabel for error text', () => { + render(); + const errorLabel = screen.getByText('(erreur)'); + expect(errorLabel).toBeDefined(); + }); }); }); diff --git a/packages/paste-core/components/form-pill-group/package.json b/packages/paste-core/components/form-pill-group/package.json index 7d4d67abce..93f61d5488 100644 --- a/packages/paste-core/components/form-pill-group/package.json +++ b/packages/paste-core/components/form-pill-group/package.json @@ -3,7 +3,7 @@ "version": "4.0.2", "category": "interaction", "status": "production", - "description": "A Form Pill Group is an editable set of Pills used to visually represent a collection of entities inside a form field.", + "description": "A Form Pill Group is an editable set of Pills that represent a collection of selectable or removable objects.", "author": "Twilio Inc.", "license": "MIT", "main:dev": "src/index.tsx", diff --git a/packages/paste-core/components/form-pill-group/src/FormPill.styles.ts b/packages/paste-core/components/form-pill-group/src/FormPill.styles.ts new file mode 100644 index 0000000000..2605b1cae1 --- /dev/null +++ b/packages/paste-core/components/form-pill-group/src/FormPill.styles.ts @@ -0,0 +1,160 @@ +import type {VariantStyles} from './types'; + +/** + * Wrapper styles + */ + +export const wrapperStyles: VariantStyles = { + default: { + color: 'colorTextIcon', + _hover: { + color: 'colorTextLinkStronger', + }, + }, + error: { + color: 'colorTextIcon', + _hover: { + color: 'colorTextErrorStronger', + }, + }, +}; + +export const selectedWrapperStyles: VariantStyles = { + default: { + color: 'colorTextWeakest', + _hover: { + color: 'colorTextInverse', + }, + }, + error: { + color: 'colorTextInverse', + _hover: { + color: 'colorTextWeakest', + }, + }, +}; + +/* + * Pill styles + */ + +export const pillStyles: VariantStyles = { + default: { + color: 'colorText', + backgroundColor: 'colorBackgroundPrimaryWeakest', + + _focus: { + boxShadow: 'shadowFocus', + color: 'colorText', + }, + _selected: { + backgroundColor: 'colorBackgroundPrimaryStronger', + color: 'colorTextWeakest', + }, + _selected_focus: { + boxShadow: 'shadowFocus', + color: 'colorTextWeakest', + }, + _disabled: { + backgroundColor: 'colorBackgroundStrong', + cursor: 'not-allowed', + color: 'colorText', + }, + }, + error: { + backgroundColor: 'colorBackgroundErrorWeakest', + color: 'colorTextErrorStrong', + + _focus: { + boxShadow: 'shadowFocus', + color: 'colorTextErrorStrong', + }, + _selected: { + backgroundColor: 'colorBackgroundError', + color: 'colorTextInverse', + }, + _selected_focus: { + boxShadow: 'shadowFocus', + color: 'colorTextInverse', + }, + _disabled: { + backgroundColor: 'colorBackgroundStrong', + cursor: 'not-allowed', + color: 'colorText', + }, + }, +}; + +export const hoverPillStyles: VariantStyles = { + default: { + cursor: 'pointer', + color: 'colorText', + + _hover: { + borderColor: 'colorBorderPrimaryStronger', + color: 'colorTextLinkStronger', + }, + _selected_hover: { + backgroundColor: 'colorBackgroundPrimary', + borderColor: 'transparent', + color: 'inherit', + }, + _focus_hover: { + borderColor: 'transparent', + }, + }, + error: { + cursor: 'pointer', + color: 'colorTextErrorStrong', + + _hover: { + borderColor: 'colorBorderErrorStronger', + color: 'inherit', + }, + _selected_hover: { + backgroundColor: 'colorBackgroundErrorStrongest', + borderColor: 'transparent', + }, + _focus_hover: { + borderColor: 'transparent', + }, + }, +}; + +/** + * Close icon styles + */ + +export const closeStyles: VariantStyles = { + default: { + color: 'inherit', + _hover: { + cursor: 'pointer', + borderColor: 'colorBorderPrimaryStronger', + }, + }, + error: { + color: 'inherit', + _hover: { + cursor: 'pointer', + borderColor: 'colorBorderErrorStronger', + }, + }, +}; + +export const selectedCloseStyles: VariantStyles = { + default: { + _hover: { + cursor: 'pointer', + borderColor: 'transparent', + backgroundColor: 'colorBackgroundPrimary', + }, + }, + error: { + _hover: { + cursor: 'pointer', + borderColor: 'transparent', + backgroundColor: 'colorBackgroundErrorStrongest', + }, + }, +}; diff --git a/packages/paste-core/components/form-pill-group/src/FormPill.tsx b/packages/paste-core/components/form-pill-group/src/FormPill.tsx index c7005b2f06..1ad2370175 100644 --- a/packages/paste-core/components/form-pill-group/src/FormPill.tsx +++ b/packages/paste-core/components/form-pill-group/src/FormPill.tsx @@ -1,79 +1,18 @@ import * as React from 'react'; import {Box, safelySpreadBoxProps} from '@twilio-paste/box'; -import type {BoxStyleProps, BoxProps} from '@twilio-paste/box'; +import type {BoxProps} from '@twilio-paste/box'; import {CompositeItem} from '@twilio-paste/reakit-library'; import type {CompositeStateReturn} from '@twilio-paste/reakit-library'; import {PillCloseIcon} from './PillCloseIcon'; - -interface FormPillStylesProps { - selected?: boolean; - element?: string; - children: React.ReactNode; - onKeyDown?: (event: React.KeyboardEvent) => void; - onClick?: () => void; - isHoverable?: boolean; -} - -const hoverStyles: BoxStyleProps = { - cursor: 'pointer', - _hover: { - backgroundColor: 'colorBackgroundPrimaryWeakest', - boxShadow: 'shadowBorderPrimaryStrong', - color: 'colorTextLinkStronger', - }, - _selected_hover: { - backgroundColor: 'colorBackgroundPrimaryStronger', - color: 'colorTextWeakest', - }, -}; - -const FormPillStyles = React.forwardRef( - ({element = 'FORM_PILL', selected = false, isHoverable, ...props}, ref) => ( - - {props.children} - - ) -); - -FormPillStyles.displayName = 'StyledFormPill'; +import {FormPillButton} from './FormPillButton'; +import {wrapperStyles, selectedWrapperStyles} from './FormPill.styles'; +import type {PillVariant} from './types'; interface FormPillProps extends CompositeStateReturn, Pick { selected?: boolean; + disabled?: boolean; children: React.ReactNode; + variant?: PillVariant; /** Event handler to respond to selection events */ onSelect?: () => void; /** Event handler to respond to dismiss events */ @@ -82,6 +21,7 @@ interface FormPillProps extends CompositeStateReturn, Pick onFocus?: () => void; /** Event handler to respond to blur events */ onBlur?: () => void; + i18nErrorLabel?: string; } /** @@ -102,10 +42,32 @@ interface FormPillProps extends CompositeStateReturn, Pick * @see https://paste.twilio.design/components/form-pill-group */ export const FormPill = React.forwardRef( - ({element = 'FORM_PILL', onDismiss, onSelect, next, ...props}, ref) => { + ( + { + element = 'FORM_PILL', + onDismiss, + onSelect, + next, + selected, + variant = 'default', + disabled = false, + i18nErrorLabel, + ...props + }, + ref + ) => { + if (selected && disabled) { + throw new Error('[Paste FormPill] FormPills cannot be selected and disabled at the same time.'); + } + + const isHoverable = onSelect != null; + const isDismissable = onDismiss != null; + + const computedStyles = selected ? selectedWrapperStyles[variant] : wrapperStyles[variant]; + // Handles delete and backspace keypresses const handleKeydown = React.useCallback( - (event) => { + (event: React.KeyboardEvent) => { if ((event.key === 'Backspace' || event.key === 'Delete') && typeof onDismiss === 'function') { onDismiss(); @@ -117,23 +79,30 @@ export const FormPill = React.forwardRef( }, [onDismiss, next] ); - - const isHoverable = onSelect != null; - return ( - - {props.children} - {onDismiss != null ? : null} - + + {} : handleKeydown} + onClick={disabled ? () => {} : onSelect} + next={next} + isDisabled={disabled} + isDismissable={isDismissable} + isHoverable={isHoverable} + selected={selected} + variant={variant} + i18nErrorLabel={i18nErrorLabel} + > + {props.children} + + {isDismissable && !disabled ? ( + + ) : null} + ); } ); diff --git a/packages/paste-core/components/form-pill-group/src/FormPillButton.tsx b/packages/paste-core/components/form-pill-group/src/FormPillButton.tsx new file mode 100644 index 0000000000..6a698712e6 --- /dev/null +++ b/packages/paste-core/components/form-pill-group/src/FormPillButton.tsx @@ -0,0 +1,81 @@ +import * as React from 'react'; +import {Box, safelySpreadBoxProps} from '@twilio-paste/box'; +import type {BoxElementProps} from '@twilio-paste/box'; +import {ScreenReaderOnly} from '@twilio-paste/screen-reader-only'; +import {ErrorIcon} from '@twilio-paste/icons/esm/ErrorIcon'; +import {pillStyles, hoverPillStyles} from './FormPill.styles'; +import type {PillVariant} from './types'; + +interface FormPillStylesProps { + variant?: PillVariant; + selected?: boolean; + element?: BoxElementProps['element']; + children: React.ReactNode; + onKeyDown?: (event: React.KeyboardEvent) => void; + onClick?: () => void; + isHoverable?: boolean; + isDismissable?: boolean; + /* We can't call this `disabled` because it conflicts with the internal `CompositeItem disabled prop */ + isDisabled?: boolean; + i18nErrorLabel?: string; +} + +export const FormPillButton = React.forwardRef( + ( + { + element = 'FORM_PILL', + selected = false, + variant = 'default', + isHoverable = false, + isDisabled = false, + isDismissable = false, + i18nErrorLabel = '(error)', + ...props + }, + ref + ) => { + const computedStyles = React.useMemo(() => { + const hasHoverStyles = isHoverable && !isDisabled; + return hasHoverStyles ? {...pillStyles[variant], ...hoverPillStyles[variant]} : pillStyles[variant]; + }, [isHoverable, isDisabled, variant]); + + return ( + + + {variant === 'error' ? ( + <> + + {i18nErrorLabel} + + ) : null} + {props.children} + + + ); + } +); + +FormPillButton.displayName = 'FormPillButton'; diff --git a/packages/paste-core/components/form-pill-group/src/FormPillGroup.tsx b/packages/paste-core/components/form-pill-group/src/FormPillGroup.tsx index 67a8f86e64..0522cfa416 100644 --- a/packages/paste-core/components/form-pill-group/src/FormPillGroup.tsx +++ b/packages/paste-core/components/form-pill-group/src/FormPillGroup.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import {Box, safelySpreadBoxProps} from '@twilio-paste/box'; +import type {BoxElementProps} from '@twilio-paste/box'; import {ScreenReaderOnly} from '@twilio-paste/screen-reader-only'; import {useUID} from '@twilio-paste/uid-library'; import {Composite} from '@twilio-paste/reakit-library'; @@ -8,7 +9,7 @@ import type {CompositeProps} from '@twilio-paste/reakit-library'; export interface FormPillGroupProps extends Omit { 'aria-label': string; - element?: string; + element?: BoxElementProps['element']; children: React.ReactNode; i18nKeyboardControls?: string; } @@ -24,8 +25,8 @@ const FormPillGroupStyles = React.forwardRef {props.children} diff --git a/packages/paste-core/components/form-pill-group/src/PillCloseIcon.tsx b/packages/paste-core/components/form-pill-group/src/PillCloseIcon.tsx index 7c900f0c2f..c4e50138c1 100644 --- a/packages/paste-core/components/form-pill-group/src/PillCloseIcon.tsx +++ b/packages/paste-core/components/form-pill-group/src/PillCloseIcon.tsx @@ -1,20 +1,45 @@ import * as React from 'react'; import {Box} from '@twilio-paste/box'; +import type {BoxProps} from '@twilio-paste/box'; import {CloseIcon} from '@twilio-paste/icons/esm/CloseIcon'; +import {selectedCloseStyles, closeStyles} from './FormPill.styles'; +import type {PillVariant} from './types'; interface PillCloseIconProps { - onClick: () => void; + onClick?: () => void; + selected?: boolean; + variant?: PillVariant; + element?: BoxProps['element']; } -export const PillCloseIcon: React.FC = ({onClick}) => { +export const PillCloseIcon: React.FC = ({ + element = 'FORM_PILL_CLOSE', + onClick = () => {}, + selected = false, + variant = 'default', +}) => { + const computedStyles = selected ? selectedCloseStyles[variant] : closeStyles[variant]; + return ( - + ); }; diff --git a/packages/paste-core/components/form-pill-group/src/types.ts b/packages/paste-core/components/form-pill-group/src/types.ts new file mode 100644 index 0000000000..80fa6704e5 --- /dev/null +++ b/packages/paste-core/components/form-pill-group/src/types.ts @@ -0,0 +1,4 @@ +import type {BoxStyleProps} from '@twilio-paste/box'; + +export type PillVariant = 'error' | 'default'; +export type VariantStyles = Record; diff --git a/packages/paste-core/components/form-pill-group/stories/customization.stories.tsx b/packages/paste-core/components/form-pill-group/stories/customization.stories.tsx new file mode 100644 index 0000000000..9f43b95791 --- /dev/null +++ b/packages/paste-core/components/form-pill-group/stories/customization.stories.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; +import {useTheme} from '@twilio-paste/theme'; +import {CustomizationProvider} from '@twilio-paste/customization'; +import {CalendarIcon} from '@twilio-paste/icons/esm/CalendarIcon'; +import {useFormPillState, FormPillGroup, FormPill} from '../src'; + +export const CustomFormPillGroup: React.FC = () => { + const currentTheme = useTheme(); + const [showTennis, setShowTennis] = React.useState(true); + const pillState = useFormPillState(); + + return ( + +
+ + {showTennis && ( + { + setShowTennis(false); + }} + onSelect={() => {}} + onFocus={() => {}} + onBlur={() => {}} + > + + Tennis + + )} + {}}> + Baseball + + {}}> + Football + + {}}> + Soccer + + +
+
+ ); +}; + +// eslint-disable-next-line import/no-default-export +export default { + title: 'Components/Form Pill Group/Customization', + component: FormPillGroup, +}; diff --git a/packages/paste-core/components/form-pill-group/stories/index.stories.tsx b/packages/paste-core/components/form-pill-group/stories/index.stories.tsx index 17a1673f2c..c01ca5d336 100644 --- a/packages/paste-core/components/form-pill-group/stories/index.stories.tsx +++ b/packages/paste-core/components/form-pill-group/stories/index.stories.tsx @@ -1,84 +1,40 @@ import * as React from 'react'; -import {useTheme} from '@twilio-paste/theme'; -import {Text} from '@twilio-paste/text'; import {Box} from '@twilio-paste/box'; -import {CustomizationProvider} from '@twilio-paste/customization'; +import {Avatar} from '@twilio-paste/avatar'; import {CalendarIcon} from '@twilio-paste/icons/esm/CalendarIcon'; import {useFormPillState, FormPillGroup, FormPill} from '../src'; -export const Basic: React.FC = () => { +const PILL_NAMES = [ + 'Default pill', + 'Pill with icon', + 'Pill with avatar', + 'Error pill', + 'Error pill with icon', + 'Error pill with avatar', +]; + +export const Basic: React.FC<{selected?: boolean; dismissable?: boolean; disabled?: boolean; ariaLabel?: string}> = ({ + selected = false, + dismissable = false, + disabled = false, + ariaLabel = 'Basic pills:', +}) => { const pillState = useFormPillState(); - return (
- - - - Tennis - - - Baseball - - Football - - Soccer - - -
- ); -}; - -export const OverflowWrapping: React.FC = () => { - const pillState = useFormPillState(); - - return ( - -
- - - - Tennis - - - Baseball - - Football - Basketball and Volleyball and Swimming and Jumping - - Soccer - - -
-
- ); -}; - -type Pills = string[]; - -export const Selectable: React.FC = () => { - const [pills] = React.useState(['Tennis', 'Baseball', 'Football', 'Soccer']); - const [selectedSet, updateSelectedSet] = React.useState>(new Set(['Football'])); - const pillState = useFormPillState(); - - return ( -
- - {pills.map((pill, index) => ( + + {PILL_NAMES.map((pill, index) => ( { - const newSelectedSet = new Set(selectedSet); - if (newSelectedSet.has(pill)) { - newSelectedSet.delete(pill); - } else { - newSelectedSet.add(pill); - } - updateSelectedSet(newSelectedSet); - }} + selected={selected} + variant={index > 2 ? 'error' : 'default'} + onDismiss={dismissable ? () => {} : undefined} + disabled={disabled} > + {index % 3 === 2 ? : null} + {index % 3 === 1 ? : null} {pill} ))} @@ -87,45 +43,31 @@ export const Selectable: React.FC = () => { ); }; -export const Removable: React.FC = () => { - const [pills, setPills] = React.useState(['Tennis', 'Baseball', 'Football', 'Soccer']); - const pillState = useFormPillState(); +export const Disabled: React.FC = () => ; +export const Selected: React.FC = () => ; +export const Dismissable: React.FC = () => ; - return ( - - - {pills.map((pill, index) => ( - { - setPills(pills.filter((_, i) => i !== index)); - }} - > - {pill} - - ))} - {pills.length === 0 ? No sports remaining : null} - - - ); -}; +export const OverflowWrapping: React.FC = () => ( + + + +); -export const SelectableAndRemovable: React.FC = () => { - const [pills, setPills] = React.useState(['Tennis', 'Baseball', 'Football', 'Soccer']); - const [selectedSet, updateSelectedSet] = React.useState>(new Set(['Football'])); +export const SelectableAndDismissable: React.FC = () => { + const [pills, setPills] = React.useState([...PILL_NAMES]); + const [selectedSet, updateSelectedSet] = React.useState>(new Set([PILL_NAMES[1], PILL_NAMES[4]])); const pillState = useFormPillState(); return (
- + {pills.map((pill, index) => ( 2 ? 'error' : 'default'} onDismiss={() => { setPills(pills.filter((_, i) => i !== index)); }} @@ -139,70 +81,17 @@ export const SelectableAndRemovable: React.FC = () => { updateSelectedSet(newSelectedSet); }} > + {index % 3 === 2 ? : null} + {index % 3 === 1 ? : null} {pill} ))} - {pills.length === 0 ? No sports remaining : null} ); }; -export const CustomFormPillGroup: React.FC = () => { - const currentTheme = useTheme(); - const [showTennis, setShowTennis] = React.useState(true); - const pillState = useFormPillState(); - - return ( - -
- - {showTennis && ( - { - setShowTennis(false); - }} - onSelect={() => {}} - onFocus={() => {}} - onBlur={() => {}} - > - - Tennis - - )} - {}}> - Baseball - - {}}> - Football - - {}}> - Soccer - - -
-
- ); -}; +const I18N_PILL_NAMES = ['Le tennis', 'Le football américain', 'Le ski', 'Le football']; export const I18nProp = (): React.ReactNode => { const pillState = useFormPillState(); @@ -215,17 +104,11 @@ export const I18nProp = (): React.ReactNode => { aria-label="Votre sports favoris:" i18nKeyboardControls="Appuyez sur Supprimer ou Retour arrière pour supprimer. Appuyez sur Entrée pour basculer la sélection." > - - - Le tennis - - - Le football américain - - - Le ski - - Le football + {I18N_PILL_NAMES.map((pill) => ( + + {pill} + + ))}
); diff --git a/packages/paste-core/primitives/box/package.json b/packages/paste-core/primitives/box/package.json index e72d8e7725..c3e5c2a088 100644 --- a/packages/paste-core/primitives/box/package.json +++ b/packages/paste-core/primitives/box/package.json @@ -49,6 +49,7 @@ "@twilio-paste/types": "^3.1.8", "prop-types": "^15.7.2", "react": "^17.0.2", - "react-dom": "^17.0.2" + "react-dom": "^17.0.2", + "typescript": "^4.6.4" } } diff --git a/packages/paste-core/primitives/box/src/PseudoPropStyles.ts b/packages/paste-core/primitives/box/src/PseudoPropStyles.ts index 0950cbf14c..1c0f2f5091 100644 --- a/packages/paste-core/primitives/box/src/PseudoPropStyles.ts +++ b/packages/paste-core/primitives/box/src/PseudoPropStyles.ts @@ -6,6 +6,7 @@ export const PseudoPropStyles = { _hover: '&:hover', _active: '&:active, &[data-active=true]', _focus: '&:focus', + _focus_hover: '&:focus:hover', _focus_placeholder: '&:focus::placeholder', _visited: '&:visited', _even: '&:nth-of-type(even)', diff --git a/packages/paste-core/primitives/box/src/types.ts b/packages/paste-core/primitives/box/src/types.ts index ea981f3492..922b0a041c 100644 --- a/packages/paste-core/primitives/box/src/types.ts +++ b/packages/paste-core/primitives/box/src/types.ts @@ -90,6 +90,7 @@ export interface BoxElementProps extends Omit, element?: string; /** variant for variant styling */ variant?: string; + disabled?: boolean; } export interface BoxProps extends BoxElementProps, BoxStyleProps {} diff --git a/packages/paste-design-tokens/__tests__/__snapshots__/index.test.tsx.snap b/packages/paste-design-tokens/__tests__/__snapshots__/index.test.tsx.snap index 2979b92d07..74cec78191 100644 --- a/packages/paste-design-tokens/__tests__/__snapshots__/index.test.tsx.snap +++ b/packages/paste-design-tokens/__tests__/__snapshots__/index.test.tsx.snap @@ -33,6 +33,7 @@ exports[`Design Tokens matches the Dark theme 1`] = ` \\"color-background-available\\": \\"rgb(20, 176, 83)\\", \\"color-background-error-weakest\\": \\"rgb(49, 12, 12)\\", \\"color-background-required\\": \\"rgb(235, 86, 86)\\", + \\"color-background-error-strongest\\": \\"rgb(254, 236, 236)\\", \\"color-background-decorative-40-weakest\\": \\"rgb(56, 14, 120)\\", \\"color-background-decorative-30-weakest\\": \\"rgb(5, 41, 18)\\", \\"color-background-body\\": \\"rgb(13, 19, 28)\\", @@ -339,6 +340,7 @@ exports[`Design Tokens matches the Dark theme 1`] = ` \\"color-text-weaker\\": \\"rgb(57, 71, 98)\\", \\"color-text-warning-strong\\": \\"rgb(250, 194, 160)\\", \\"color-text-icon-warning\\": \\"rgb(255, 179, 122)\\", + \\"color-text-error-stronger\\": \\"rgb(254, 236, 236)\\", \\"color-text-link-destructive-strong\\": \\"rgb(246, 177, 177)\\", \\"z-index-0\\": \\"0\\", \\"z-index-90\\": \\"90\\", @@ -407,6 +409,7 @@ exports[`Design Tokens matches the Global theme 1`] = ` \\"color-background-available\\": \\"rgb(20, 176, 83)\\", \\"color-background-error-weakest\\": \\"rgb(254, 236, 236)\\", \\"color-background-required\\": \\"rgb(235, 86, 86)\\", + \\"color-background-error-strongest\\": \\"rgb(49, 12, 12)\\", \\"color-background-decorative-40-weakest\\": \\"rgb(245, 240, 252)\\", \\"color-background-decorative-30-weakest\\": \\"rgb(237, 253, 243)\\", \\"color-background-body\\": \\"rgb(255, 255, 255)\\", @@ -760,6 +763,7 @@ exports[`Design Tokens matches the Global theme 1`] = ` \\"color-text-weaker\\": \\"rgb(174, 178, 193)\\", \\"color-text-warning-strong\\": \\"rgb(141, 49, 24)\\", \\"color-text-icon-warning\\": \\"rgb(227, 106, 25)\\", + \\"color-text-error-stronger\\": \\"rgb(74, 11, 11)\\", \\"color-text-link-destructive-strong\\": \\"rgb(173, 17, 17)\\", \\"z-index-0\\": \\"0\\", \\"z-index-90\\": \\"90\\", @@ -807,6 +811,7 @@ exports[`Design Tokens matches the Sendgrid theme 1`] = ` \\"color-background-available\\": \\"rgb(20, 176, 83)\\", \\"color-background-error-weakest\\": \\"rgb(254, 236, 236)\\", \\"color-background-required\\": \\"rgb(235, 86, 86)\\", + \\"color-background-error-strongest\\": \\"rgb(49, 12, 12)\\", \\"color-background-decorative-40-weakest\\": \\"rgb(245, 240, 252)\\", \\"color-background-decorative-30-weakest\\": \\"rgb(237, 253, 243)\\", \\"color-background-body\\": \\"rgb(255, 255, 255)\\", @@ -1113,6 +1118,7 @@ exports[`Design Tokens matches the Sendgrid theme 1`] = ` \\"color-text-weaker\\": \\"rgb(174, 178, 193)\\", \\"color-text-warning-strong\\": \\"rgb(141, 49, 24)\\", \\"color-text-icon-warning\\": \\"rgb(227, 106, 25)\\", + \\"color-text-error-stronger\\": \\"rgb(74, 11, 11)\\", \\"color-text-link-destructive-strong\\": \\"rgb(173, 17, 17)\\", \\"z-index-0\\": \\"0\\", \\"z-index-90\\": \\"90\\", diff --git a/packages/paste-design-tokens/tokens/global/background-color.yml b/packages/paste-design-tokens/tokens/global/background-color.yml index fcbeadb778..d8c327e2ff 100644 --- a/packages/paste-design-tokens/tokens/global/background-color.yml +++ b/packages/paste-design-tokens/tokens/global/background-color.yml @@ -79,6 +79,9 @@ props: color-background-warning-weakest: value: "{!palette-orange-10}" comment: Weakest background color used for warning alerts and toasts. + color-background-error-strongest: + value: "{!palette-red-100}" + comment: Strongest error background color color-background-error-stronger: value: "{!palette-red-90}" comment: Stronger error background color diff --git a/packages/paste-design-tokens/tokens/global/text-color.yml b/packages/paste-design-tokens/tokens/global/text-color.yml index 6226d5098d..f5883b68cd 100644 --- a/packages/paste-design-tokens/tokens/global/text-color.yml +++ b/packages/paste-design-tokens/tokens/global/text-color.yml @@ -254,6 +254,21 @@ props: - color-background-row-striped - color-background-primary-weakest - color-background-destructive-weakest + color-text-error-stronger: + value: "{!palette-red-90}" + comment: Stronger error text for inputs and error misc + text_contrast_pairing: + - color-background + - color-background-body + - color-background-subaccount + - color-background-trial + - color-background-neutral-weakest + - color-background-success-weakest + - color-background-warning-weakest + - color-background-error-weakest + - color-background-row-striped + - color-background-primary-weakest + - color-background-destructive-weakest color-text-success: value: "{!palette-green-70}" comment: Text color for success text. diff --git a/packages/paste-design-tokens/tokens/themes/dark/global/background-color.yml b/packages/paste-design-tokens/tokens/themes/dark/global/background-color.yml index 1cec607dd4..6e597a450a 100644 --- a/packages/paste-design-tokens/tokens/themes/dark/global/background-color.yml +++ b/packages/paste-design-tokens/tokens/themes/dark/global/background-color.yml @@ -80,6 +80,9 @@ props: color-background-warning-weakest: value: "{!palette-orange-100}" comment: Weakest warning background color + color-background-error-strongest: + value: "{!palette-red-10}" + comment: Strongest error background color color-background-error-stronger: value: "{!palette-red-20}" comment: Stronger error background color diff --git a/packages/paste-design-tokens/tokens/themes/dark/global/text-color.yml b/packages/paste-design-tokens/tokens/themes/dark/global/text-color.yml index 5095dca618..d6a7e2008c 100644 --- a/packages/paste-design-tokens/tokens/themes/dark/global/text-color.yml +++ b/packages/paste-design-tokens/tokens/themes/dark/global/text-color.yml @@ -77,6 +77,9 @@ props: color-text-error-strong: value: "{!palette-red-30}" comment: Strong error text for inputs and error misc + color-text-error-stronger: + value: "{!palette-red-10}" + comment: Stronger error text for inputs and error misc color-text-success: value: "{!palette-green-30}" comment: Text color for success text. diff --git a/packages/paste-website/src/component-examples/DisplayPillGroup.ts b/packages/paste-website/src/component-examples/DisplayPillGroup.ts new file mode 100644 index 0000000000..3ebb547fb6 --- /dev/null +++ b/packages/paste-website/src/component-examples/DisplayPillGroup.ts @@ -0,0 +1,197 @@ +export const mainExample = ` +const DisplayPillGroupExample = () => { + return ( + + Voice + + Studio + + + + SMS + + + + MMS + + + + Customer + + + + Agent + + + ); +} + +render( + +) +`.trim(); + +export const basicExample = ` +const BasicDisplayPillGroup = () => { + return ( + + Notify + Proxy + + + Verify + + + + Interconnect + + Transcriptions + Chat + + ); +} + +render( + +) +`.trim(); + +export const linkedExample = ` +const LinkedDisplayPillGroup = () => { + return ( + + Authy + + + Phone Numbers + + + + Frontline + + + + Customer + + + + Agent + + + ); +} + +render( + +) +`.trim(); + +export const groupExample = ` +const DisplayPillGroupExample = () => { + return ( + + + + Segment + + Flex + SendGrid + + + Twilio + + + ); +} + +render( + +) +`.trim(); + +export const truncateExample = ` +const TruncateDisplayPillGroup = () => { + return ( + + + + + Internet of Things + + + + + + Marketing Campaigns + + + + + + CodeExchange Partner + + + + + + Engagement Intelligence Platform + + + + ); +}; + +render( + +) +`.trim(); + +export const avatarExample = ` +const AvatarDisplayPillGroupExample = () => { + return ( + + + + Customer + + + + Agent + + + ); +} + +render( + +) +`.trim(); + +export const iconExample = ` +const IconDisplayPillGroup = () => { + return ( + + + + Messaging + + + + Billing + + + + Lookup + + + + Conversations + + + ); +}; + +render( + +) +`.trim(); diff --git a/packages/paste-website/src/component-examples/FormPillGroup.tsx b/packages/paste-website/src/component-examples/FormPillGroup.tsx index 7443259c10..14e0043766 100644 --- a/packages/paste-website/src/component-examples/FormPillGroup.tsx +++ b/packages/paste-website/src/component-examples/FormPillGroup.tsx @@ -1,20 +1,20 @@ -export const defaultExample = ` +export const basicExample = ` const BasicFormPillGroup = () => { const pillState = useFormPillState(); return (
- + - - Tennis + Voice - Baseball + + Video - Football - Soccer + + Verify @@ -28,13 +28,20 @@ render( export const selectableExample = ` const SelectableFormPillGroup = () => { - const [pills] = React.useState(['Tennis', 'Baseball', 'Football', 'Soccer']); - const [selectedSet, updateSelectedSet] = React.useState(new Set(['Football'])); + const [pills] = React.useState(['SMS', 'MMS', 'Fax', 'Voice', 'Messaging', 'Chat']); + const [selectedSet, updateSelectedSet] = React.useState(new Set(['MMS', 'Voice', 'Chat'])); const pillState = useFormPillState(); + const iconMap = { + ['Fax']: , + ['Voice']: , + ['Messaging']: , + ['Chat']: , + } + return (
- + {pills.map((pill) => ( { updateSelectedSet(newSelectedSet); }} > + {iconMap[pill]} {pill} ))} @@ -63,14 +71,18 @@ render( ) `.trim(); -export const removableExample = ` -const RemovableFormPillGroup = () => { - const [pills, setPills] = React.useState(['Tennis', 'Baseball', 'Football', 'Soccer']); +export const dismissableExample = ` +const DismissableFormPillGroup = () => { + const [pills, setPills] = React.useState(['Frontline', 'Phone Numbers', 'Authy']); const pillState = useFormPillState(); + const iconMap = { + ['Phone Numbers']: , + } + return ( - + {pills.map((pill, index) => ( { setPills(pills.filter((_, i) => i !== index)); }} > + {iconMap[pill]} {pill} ))} - {pills.length === 0 ? No sports remaining : null} ); }; render( - + +) +`.trim(); + +export const selectableAndDismissableExample = ` +const SelectableAndDismissableFormPillGroup = () => { + const [pills, setPills] = React.useState(['Proxy', 'Interconnect', 'Trust Hub']); + const [selectedSet, updateSelectedSet] = React.useState(new Set(['Interconnect'])); + const pillState = useFormPillState(); + + const iconMap = { + ['Interconnect']: , + } + + return ( +
+ + {pills.map((pill, index) => ( + { + setPills(pills.filter((_, i) => i !== index)); + if (selectedSet.has(pill)) { + const newSelectedSet = new Set(selectedSet); + newSelectedSet.delete(pill); + updateSelectedSet(newSelectedSet); + } + }} + selected={selectedSet.has(pill)} + onSelect={() => { + const newSelectedSet = new Set(selectedSet); + if (newSelectedSet.has(pill)) { + newSelectedSet.delete(pill); + } else { + newSelectedSet.add(pill); + } + updateSelectedSet(newSelectedSet); + }} + > + {iconMap[pill]} + {pill} + + ))} + +
+ ); +}; + +render( + ) `.trim(); @@ -104,14 +166,13 @@ const I18nFormPillGroup = () => { aria-label="Votre sports favoris:" i18nKeyboardControls="Appuyez sur Supprimer ou Retour arrière pour supprimer. Appuyez sur Entrée pour basculer la sélection." > - - + Le tennis Le football américain - + Le ski Le football @@ -124,3 +185,200 @@ render( ) `.trim(); + +export const selectedStateExample = ` +const SelectedStateExample = () => { + const pillState = useFormPillState(); + + return ( +
+ + + Voice + + + + Video + + + + Verify + + +
+ ); +}; + +render( + +) +`.trim(); + +export const errorStateExample = ` +const ErrorStateExample = () => { + const pillState = useFormPillState(); + + return ( +
+ + + Voice + + + + Video + + + + Verify + + + Usage + + +
+ ); +}; + +render( + +) +`.trim(); + +export const disabledStateExample = ` +const DisabledStateExample = () => { + const pillState = useFormPillState(); + + return ( +
+ + + Voice + +<<<<<<< HEAD + +======= + +>>>>>>> 172de0b3a (chore: implement feedback) + + Video + + + + Verify + +<<<<<<< HEAD + +======= + +>>>>>>> 172de0b3a (chore: implement feedback) + Usage + + +
+ ); +}; + +render( + +) +`.trim(); + +export const truncateExample = ` +const TruncateFormPillGroup = () => { + const pillState = useFormPillState(); + + return ( +
+ + + + + Internet of Things + + + + + + Marketing Campaigns + + + + + + CodeExchange Partner + + + + + + Engagement Intelligence Platform + + + +
+ ); +}; + +render( + +) +`.trim(); + +export const avatarExample = ` +const AvatarFormPillGroup = () => { + const pillState = useFormPillState(); + + return ( +
+ + + + Customer + + + + Agent + + +
+ ); +}; + +render( + +) +`.trim(); + +export const iconExample = ` +const IconFormPillGroup = () => { + const pillState = useFormPillState(); + + return ( +
+ + + + Messaging + + + + Billing + + + + Lookup + + + + Conversations + + +
+ ); +}; + +render( + +) +`.trim(); diff --git a/packages/paste-website/src/components/FormPillVsDisplayPillTable.tsx b/packages/paste-website/src/components/FormPillVsDisplayPillTable.tsx index ff2105292e..2b6edd5768 100644 --- a/packages/paste-website/src/components/FormPillVsDisplayPillTable.tsx +++ b/packages/paste-website/src/components/FormPillVsDisplayPillTable.tsx @@ -1,19 +1,119 @@ import * as React from 'react'; import {Box} from '@twilio-paste/box'; import {Table, Tr, THead, Th, TBody, Td} from '@twilio-paste/table'; +import {ScreenReaderOnly} from '@twilio-paste/screen-reader-only'; import {SuccessIcon} from '@twilio-paste/icons/esm/SuccessIcon'; -export interface FormPillVsDisplayPillTableProps { - children: NonNullable; -} +const FormPillVsDisplayPillTable: React.FC = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Functionality + Display PillForm Pill
Used to edit data in a form  + + + +
Uses Listbox, Option semantic  + + + +
Provides advanced keyboard navigation  + + + +
Pills can be selected  + + + +
Pills can perform an action  + + + +
Pills can be dismissed  + + + +
Used to view data + + + +  
Uses List, List Item semantic + + + +  
Pills can link to a page + + + +  
+
+ ); +}; -const FormPillVsDisplayPillTable: React.FC = () => { +const DisplayPillVsFormPillTable: React.FC = () => { return ( - + @@ -65,7 +165,7 @@ const FormPillVsDisplayPillTable: React.FC = () - + - + - +
  + Functionality + Display Pill Form Pill
Provides keyboard navigationProvides advanced keyboard navigation   @@ -74,7 +174,7 @@ const FormPillVsDisplayPillTable: React.FC = ()
Pills are selectablePills can be selected   @@ -92,7 +192,7 @@ const FormPillVsDisplayPillTable: React.FC = ()
Pills can be removedPills can be dismissed   @@ -106,4 +206,4 @@ const FormPillVsDisplayPillTable: React.FC = () ); }; -export {FormPillVsDisplayPillTable}; +export {FormPillVsDisplayPillTable, DisplayPillVsFormPillTable}; diff --git a/packages/paste-website/src/pages/components/display-pill-group/index.mdx b/packages/paste-website/src/pages/components/display-pill-group/index.mdx index 467a23cd75..2df1967dc6 100644 --- a/packages/paste-website/src/pages/components/display-pill-group/index.mdx +++ b/packages/paste-website/src/pages/components/display-pill-group/index.mdx @@ -1,20 +1,47 @@ --- title: Display Pill Group package: '@twilio-paste/display-pill-group' -description: A Display Pill Group is a set of Pills used to visually represent a collection of entities outside of form-based UIs. +description: A Display Pill Group is a non-editable set of Pills that represent a collection of static objects. slug: /components/display-pill-group/ --- import {graphql} from 'gatsby'; -import {Box} from '@twilio-paste/box'; -import {Paragraph} from '@twilio-paste/paragraph'; import {Avatar} from '@twilio-paste/avatar'; +import {Box} from '@twilio-paste/box'; import {DisplayPill, DisplayPillGroup} from '@twilio-paste/display-pill-group'; import Changelog from '@twilio-paste/display-pill-group/CHANGELOG.md'; -import {CalendarIcon} from '@twilio-paste/icons/esm/CalendarIcon'; +import {Truncate} from '@twilio-paste/truncate'; + +import {AgentIcon} from '@twilio-paste/icons/esm/AgentIcon'; +import {LogoTwilioIcon} from '@twilio-paste/icons/esm/LogoTwilioIcon'; +import {MMSCapableIcon} from '@twilio-paste/icons/esm/MMSCapableIcon'; +import {ProductBillingIcon} from '@twilio-paste/icons/esm/ProductBillingIcon'; +import {ProductCodeExchangePartnerIcon} from '@twilio-paste/icons/esm/ProductCodeExchangePartnerIcon'; +import {ProductConversationsIcon} from '@twilio-paste/icons/esm/ProductConversationsIcon'; +import {ProductEngagementIntelligencePlatformIcon} from '@twilio-paste/icons/esm/ProductEngagementIntelligencePlatformIcon'; +import {ProductFrontlineIcon} from '@twilio-paste/icons/esm/ProductFrontlineIcon'; +import {ProductInterconnectIcon} from '@twilio-paste/icons/esm/ProductInterconnectIcon'; +import {ProductInternetOfThingsIcon} from '@twilio-paste/icons/esm/ProductInternetOfThingsIcon'; +import {ProductLookupIcon} from '@twilio-paste/icons/esm/ProductLookupIcon'; +import {ProductMarketingCampaignsIcon} from '@twilio-paste/icons/esm/ProductMarketingCampaignsIcon'; +import {ProductMessagingIcon} from '@twilio-paste/icons/esm/ProductMessagingIcon'; +import {ProductPhoneNumbersIcon} from '@twilio-paste/icons/esm/ProductPhoneNumbersIcon'; +import {ProductSegmentIcon} from '@twilio-paste/icons/esm/ProductSegmentIcon'; +import {ProductVerifyIcon} from '@twilio-paste/icons/esm/ProductVerifyIcon'; +import {SMSCapableIcon} from '@twilio-paste/icons/esm/SMSCapableIcon'; + import {DoDont, Do, Dont} from '../../../components/DoDont'; -import {FormPillVsDisplayPillTable} from '../../../components/FormPillVsDisplayPillTable'; +import {DisplayPillVsFormPillTable} from '../../../components/FormPillVsDisplayPillTable'; import {SidebarCategoryRoutes} from '../../../constants'; +import { + mainExample, + basicExample, + linkedExample, + groupExample, + truncateExample, + avatarExample, + iconExample, +} from '../../../component-examples/DisplayPillGroup.ts'; export const pageQuery = graphql` { @@ -33,6 +60,7 @@ export const pageQuery = graphql` frontmatter { slug title + description } headings { depth @@ -61,6 +89,7 @@ export const pageQuery = graphql` githubUrl="https://github.com/twilio-labs/paste/tree/main/packages/paste-core/components/display-pill-group" storybookUrl="/?path=/story/components-display-pill-group--basic" data={props.data} + description={props.pageContext.frontmatter.description} /> --- @@ -71,104 +100,146 @@ export const pageQuery = graphql` - - {` - - - Tennis - - Football - - - Baseball - - Basketball - Soccer -`} + + {mainExample} ## Guidelines ### About Display Pill Group - - {props.pageContext.frontmatter.description} Display Pills are static and should be used in situations where data is - not being modified. - +A Display Pill Group represents a collection of static objects. Display Pills are static text and should be used where Pills aren’t in a state where they’re actively being modified. -Display Pills can link to other pages. This can useful when the entity the pill represents has its own detail page within the application. +### Accessibility -#### Accessibility +A label helps explain what a collection of data objects represents. Set a non-visual label on a Display Pill Group using `aria-label`. -A list of data entities benefits from a label that explains what the collection represents. The Display Pill Group requires that a non-visual label be set on it using `aria-label`. +#### Keyboard navigation -#### Display Pill vs. Form Pill +A [linked Display Pill](/components/display-pill-group#linked) is a focusable element and a single tab stop to a keyboard user. Once a user focuses on a linked Display Pill, pressing the enter key will open the link. -There are some very important semantic differences between a Display Pill and Form Pill Group. Below is a table explaining the main differences to get a better understanding of when to use each type of pill. +### Display Pill vs. Form Pill - +Display Pill Group creates a list of static items, whereas [Form Pill Group](/components/form-pill-group) creates a list from which a user may select items. + +Use the table below to get a better understanding of when to use Display Pill or Form Pill. + + ## Examples -### Basic Display Pill Group - -Display Pills can be linked, or unlinked by passing an `href` prop to the Display Pill. A Display Pill Group can have a mixture of linked and unlinked Pills. - -When provided with an `href` the `DisplayPill` will render itself as an HTML Anchor element, and will respond to any anchor based events and accept any event handlers. - - - {` - { - console.log('Focused Tennis!'); - }} - onBlur={() => { - console.log('Blurred Tennis!'); - }} - href="https://google.com" - > - - Tennis - - Football - - - Baseball - - Basketball - Soccer -`} +### Basic + +Use a Basic Display Pill to display read-only text, such as a list of email addresses or keywords. + +A Display Pill can have an optional [Avatar](/components/avatar) or [Icon](/components/icons). Use no more than one icon before or after the text. + + + {basicExample} + + +### Linked + +A Display Pill can link to other pages. This can be useful when the entity the pill represents has its own detail page. To do so, pass an `href` prop to the Display Pill. + +When provided with an `href` the `DisplayPill` will render itself as an HTML Anchor element, and will respond to any anchor-based events and accept any event handlers. + + + {linkedExample} + + +### Display Pill Group + +A Display Pill Group wraps a collection of basic and linked Display Pills with a common group component. + +`DisplayPillGroup` takes `DisplayPill`s as children. Don’t place any other type of child component directly inside of a `DisplayPillGroup`. + + + {groupExample} ## Composition Notes -`DisplayPillGroup` takes `DisplayPill`s as children. You should not place any other type of child component directly inside of a `DisplayPillGroup`. +### Truncating text + +Pill text should never wrap to the next line. If the text length is longer than the max width of the pill group’s container, consider moving the Pill to a new row or use [Truncate](/components/truncate) to truncate the Display Pill text. + + + {truncateExample} + + +### Adding an avatar + +A Display Pill can have an optional [Avatar](/components/avatar). Considering the size of a Display Pill, it is recommended to use either an image or icon within an Avatar, and to avoid using initials as some initials may get cut off if the characters are particularly wide. + +We recommend placing the Avatar ahead of the pill text. Use no more than one before or after the text. -### When to use a Display Pill Group + + {avatarExample} + + +### Adding an icon + +A Display Pill can have an optional [Icon](/components/icons). We recommend placing the Icon ahead of the pill text. Use no more than one before or after the text. + + + {iconExample} + + +## When to use a Display Pill Group - - + + - + - + ---- - ## Usage Guide ### API @@ -182,27 +253,33 @@ yarn add @twilio-paste/display-pill-group - or - yarn add @twilio-paste/core #### Usage ```jsx +import {AgentIcon} from '@twilio-paste/icons/esm/AgentIcon'; +import {Avatar} from '@twilio-paste/core/display-pill-group'; import {DisplayPillGroup, DisplayPill} from '@twilio-paste/core/display-pill-group'; +import {MMSCapableIcon} from '@twilio-paste/icons/esm/MMSCapableIcon'; +import {SMSCapableIcon} from '@twilio-paste/icons/esm/SMSCapableIcon'; -export const Basic = () => { +const DisplayPillGroupExample = () => { return ( - - { - console.log('Focused Tennis!'); - }} - onBlur={() => { - console.log('Blurred Tennis!'); - }} - href="https://google.com" - > - - Tennis + + Voice + Studio + + + SMS + + + + MMS + + + + Customer + + + + Agent - Football - Baseball - Basketball - Soccer ); }; @@ -214,27 +291,47 @@ export const Basic = () => { `DisplayPillGroup` will take any global HTML attribute for an HTML List element, plus the following: -###### `aria-label: string` +| Prop | Type | Description | Default | +| ------------ | -------- | ----------------------------------------------------------------------------------------- | ---------------------- | +| `aria-label` | `string` | Defines a string value that labels the DisplayPillGroup | | +| `element?` | `string` | Overrides the default element name to apply unique styles with the Customization Provider | `'DISPLAY_PILL_GROUP'` | -Defines a string value that labels the current element. +##### DisplayPill -###### `element?: string` +`DisplayPill` will take any global HTML attribute for an HTML Anchor element, plus the following: -Overrides the default element name ('DISPLAY_PILL_GROUP') to apply unique styles with the Customization Provider +| Prop | Type | Description | Default | +| ---------- | -------- | ----------------------------------------------------------------------------------------- | ---------------- | +| `href?` | `string` | URL the pill links to | | +| `element?` | `string` | Overrides the default element name to apply unique styles with the Customization Provider | `'DISPLAY_PILL'` | ---- +### Figma -##### DisplayPill +#### Usage -`DisplayPill` will take any global HTML attribute for an HTML Anchor element, plus the following: +Display Pill Group is available in the Paste Components Figma library. It has 1 base component, Display Pill, with a ⚙️ emoji in its layer name to indicate there are nested configurable properties available. + +In code, when there are more pills than the max width, they will wrap to the next row. To accomplish the same behavior in Figma, please use the `Row count` property to select a number of rows, or place multiple Display Pill Group components under one another. + +Display Pill Group also contains other Paste components as nested components, including [Avatar](/components/icons) and [Icon](/components/icons). + +#### Properties -###### `element?: string` +Here is a properties table for Display Pill Group: -Overrides the default element name ('DISPLAY_PILL') to apply unique styles with the Customization Provider +| Property | Variants | Description | Default | +| --------- | ---------- | -------------------------------------------------- | ------- | +| Row count | 1, 2, 3, 4 | Displays a number of rows in a Display Pill Group. | 1 | -###### `href?: string` +Here is a properties table for its nested component, Display Pill: -URL the pill is to link to. +| Property | Variants | Description | Default | +| -------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| Variant | `Basic`, `Linked` | The variant of the Display Pill. | `Basic` | +| State | `Default`, `Hover`, `Focus` | The state of the Display Pill. If `Variant=Basic`, the state can only be set to `Default`. | `Default` | +| Prefix | `None`, `Icon`, `Avatar` | Adds a prefix ahead of the Display Pill label. If set to `Avatar`, manage the nested properties of Avatar in the layer marked with a ⚙️ emoji. | `None` | +| Icon | | Swaps the icon within a Display Pill. This property is visible in the Figma side panel if `Prefix=Icon`. | | +| Label | | Sets the label of a Display Pill | "Label" | diff --git a/packages/paste-website/src/pages/components/form-pill-group/index.mdx b/packages/paste-website/src/pages/components/form-pill-group/index.mdx index 874e86ae50..716b62b73f 100644 --- a/packages/paste-website/src/pages/components/form-pill-group/index.mdx +++ b/packages/paste-website/src/pages/components/form-pill-group/index.mdx @@ -1,33 +1,53 @@ --- title: Form Pill Group package: '@twilio-paste/form-pill-group' -description: A Form Pill Group is an editable set of Pills used to visually represent a collection of entities inside a form field. +description: A Form Pill Group is an editable set of Pills that represent a collection of selectable or removable objects. slug: /components/form-pill-group/ --- import {graphql} from 'gatsby'; -import {Box} from '@twilio-paste/box'; -import {Text} from '@twilio-paste/text'; import {AspectRatio} from '@twilio-paste/aspect-ratio'; -import {Paragraph} from '@twilio-paste/paragraph'; +import {Avatar} from '@twilio-paste/avatar'; +import {Box} from '@twilio-paste/box'; import {FormPill, FormPillGroup, useFormPillState} from '@twilio-paste/form-pill-group'; import Changelog from '@twilio-paste/form-pill-group/CHANGELOG.md'; -import {CalendarIcon} from '@twilio-paste/icons/esm/CalendarIcon'; -import {useUID, useUIDSeed} from '@twilio-paste/uid-library'; -import {ComboboxListbox, ComboboxListboxGroup, ComboboxListboxOption} from '@twilio-paste/combobox'; -import {useComboboxPrimitive, useMultiSelectPrimitive} from '@twilio-paste/combobox-primitive'; -import {Label} from '@twilio-paste/label'; -import {Input} from '@twilio-paste/input'; +import {Paragraph} from '@twilio-paste/paragraph'; +import {Text} from '@twilio-paste/text'; +import {Truncate} from '@twilio-paste/truncate'; + +import {AgentIcon} from '@twilio-paste/icons/esm/AgentIcon'; +import {FaxCapableIcon} from '@twilio-paste/icons/esm/FaxCapableIcon'; +import {ProductBillingIcon} from '@twilio-paste/icons/esm/ProductBillingIcon'; +import {ProductChatIcon} from '@twilio-paste/icons/esm/ProductChatIcon'; +import {ProductCodeExchangePartnerIcon} from '@twilio-paste/icons/esm/ProductCodeExchangePartnerIcon'; +import {ProductConversationsIcon} from '@twilio-paste/icons/esm/ProductConversationsIcon'; +import {ProductEngagementIntelligencePlatformIcon} from '@twilio-paste/icons/esm/ProductEngagementIntelligencePlatformIcon'; +import {ProductInterconnectIcon} from '@twilio-paste/icons/esm/ProductInterconnectIcon'; +import {ProductInternetOfThingsIcon} from '@twilio-paste/icons/esm/ProductInternetOfThingsIcon'; +import {ProductLookupIcon} from '@twilio-paste/icons/esm/ProductLookupIcon'; +import {ProductMarketingCampaignsIcon} from '@twilio-paste/icons/esm/ProductMarketingCampaignsIcon'; +import {ProductMessagingIcon} from '@twilio-paste/icons/esm/ProductMessagingIcon'; +import {ProductPhoneNumbersIcon} from '@twilio-paste/icons/esm/ProductPhoneNumbersIcon'; +import {ProductVerifyIcon} from '@twilio-paste/icons/esm/ProductVerifyIcon'; +import {ProductVideoIcon} from '@twilio-paste/icons/esm/ProductVideoIcon'; +import {ProductVoiceIcon} from '@twilio-paste/icons/esm/ProductVoiceIcon'; + import {FormPillVsDisplayPillTable} from '../../../components/FormPillVsDisplayPillTable'; import {DoDont, Do, Dont} from '../../../components/DoDont'; import {SidebarCategoryRoutes} from '../../../constants'; import { - defaultExample, + basicExample, selectableExample, - removableExample, + dismissableExample, + selectableAndDismissableExample, i18nExample, + selectedStateExample, + errorStateExample, + disabledStateExample, + truncateExample, + avatarExample, + iconExample, } from '../../../component-examples/FormPillGroup'; -import {multiSelectExample} from '../../../component-examples/ComboboxPrimitiveExamples'; export const pageQuery = graphql` { @@ -46,6 +66,7 @@ export const pageQuery = graphql` frontmatter { slug title + description } headings { depth @@ -74,6 +95,7 @@ export const pageQuery = graphql` githubUrl="https://github.com/twilio-labs/paste/tree/main/packages/paste-core/components/form-pill-group" storybookUrl="/?path=/story/components-form-pill-group--basic" data={props.data} + description={props.pageContext.frontmatter.description} /> --- @@ -84,32 +106,30 @@ export const pageQuery = graphql` - - {defaultExample} - - ## Guidelines ### About Form Pill Group - - {props.pageContext.frontmatter.description} They are used almost exclusively in multi-select editing situations. - +A Form Pill Group is an editable set of Pills that represents a collection of selectable and/or removable objects. They are used almost exclusively in multi-select editing situations. -A Form Pill Group can be used on its own to represent selection across a number of fields, such as displaying currently applied filters when filtering data across a number of different fields. Alternatively, a Form Pill Group can be paired directly to a single input field, such as a Combobox, to represent multiple selection. +A Form Pill Group can be used on its own to represent selection across a number of fields, such as showing currently applied filters. It can also be paired directly with an input field, such as a [Combobox](/components/combobox), to represent multiple selections. #### Accessibility -The Form Pill Group provides a number of accessibility features. The only requirement is providing the Pill Group a descriptive label via the `aria-label` React prop. +The only accessibility requirement is providing the Pill Group a descriptive label via the `aria-label` React prop. ##### Keyboard navigation -The Form Pill Group is focusable, but only one pill is focusable at a time. This means the Pill Group is a single tab stop to a keyboard user. Once a user is focused within the Pill Group, the following keyboard interactions apply: +The Form Pill Group is focusable, but only one pill is focusable at a time. This means the Pill Group is a single tab stop to a keyboard user. The dismiss button in a Form Pill is not a focusable element, but can be clickable. -- Left and right arrow keys move focus within the group -- If a pill is selectable, spacebar and enter will toggle pill selection -- If a pill has a supplied action via `onClick`, enter key will trigger that action -- If a pill is dismissible, the pill can be removed by pressing the delete or backspace key +Once a user focuses within the Form Pill Group, they can use these keyboard interactions: + +| Keyboard interaction | Action | +| ------------------------- | ---------------------------------------- | +| Left and right arrow keys | Moving focus within the group | +| Enter key | Triggers the supplied action via onClick | +| Spacebar or enter keys | Selects a pill | +| Delete or backspace keys | Dismisses a pill | @@ -124,88 +144,183 @@ The Form Pill Group is focusable, but only one pill is focusable at a time. This -#### Display Pill vs. Form Pill +#### Form Pill vs. Display Pill + +Form Pill Group creates a list from which a user may select items, whereas a Display Pill Group creates a list of static items. -There are some very important semantic differences between a Display Pill and Form Pill Group. Below is a table explaining the main differences to get a better understanding of when to use each type of pill. +Use the table below to get a better understanding of when to use Form Pill or Display Pill. ## Examples -### Basic Form Pill Group +### Basic + +This Form Pill example shows the basic static component that's exported by Paste. A Form Pill can have an optional Avatar or Icon placed before the text. -The basic Form Pill Group can be composed by adding `FormPill` components as children of `FormPillGroup`. These must be used with the `useFormPillState` hook, and the returned state should be spread onto each component. +Interaction states on Form Pills need to be managed separately as shown in the examples after this one by using the `useFormPillState` hook. The returned state should be spread onto each component. This will provide you with some internal state management and keyboard navigation. -This will provide you with some internal state management and keyboard navigation. +A Form Pill can have an optional [Avatar](/components/avatar) or [Icon](/components/icons) placed before the text. - - {defaultExample} + + {basicExample} -### Selectable Form Pill Group +### Selectable -To make pills inside the Pill Group selectable, manage the selection state yourself and combine it with the state returned from the `useFormPillState` hook. +Use a Selectable Form Pill to show an option that a user can select or deselect. -To do so, track which pill is selected in a separate store of state. When rendering the Pill Group, cross reference the rendered pills with the selection state, and conditoinally set `selected` on each `FormPill` that requires it. +To make pills inside the Pill Group selectable, you can manage the selection state yourself and combine it with the state returned from the `useFormPillState` hook. To do so, track which pill is selected in a separate store of state. When rendering the Form Pill Group, cross reference the rendered pills with the selection state, and conditionally set `selected` on each `FormPill` that requires it. -The `onSelect` event handler will fire when ever the pill is clicked, or the spacebar or enter key is pressed. Use this to respond to your users' selection interactions. +The `onSelect` event handler will fire whenever the pill is clicked, or the spacebar or enter key is pressed. Use this to respond to your users' selection interactions. - + {selectableExample} -### Removable Form Pill Group +### Dismissible + +Use a Dismissible Form Pill to show an option that a user can remove from the group. Once the pill is dismissed, it can’t be rerendered. + +Form Pills are given a close button when provided an `onDismiss` event handler. Because the Form Pill Group is largely presentational, provide dismissible functionality by managing the state of the rendered pills. By responding to user interactions and hooking into the `onDismiss` event handler, the rendered state of the Form Pill Group can be updated and pills can be selectively removed from the collection. + +The `onDismiss` event handler will fire when a user clicks on the close button, or presses their backspace or delete key when focused on a pill. + + + {dismissableExample} + + +### Selectable and dismissible + +Use a Selectable and Dismissible Form Pill to show an option that a user can select, deselect, or dismiss. Once the pill is dismissed, it can’t be re-rendered. + +#### Mouse navigation + +The `onSelect` event handler will fire when a user clicks on the pill. The `onDismiss` event handler will fire when a user clicks on the close button. + +#### Keyboard navigation + +The `onSelect` event handler will fire when a user presses the spacebar or enter keys. The `onDismiss` event handler will fire when a user presses the delete or backspace keys. + + + {selectableAndDismissableExample} + + +### Internationalization + +To internationalize the form pill group, simply pass different text as children to the pills. The only exceptions to this are the visually hidden text that explains how to dismiss and select pills and the error label for the error variant. To change these, pass the `i18nKeyboardControls` and `i18nErrorLabel` props. + + + {i18nExample} + + +## States + +### Default or unselected + +The default state of a Form Pill indicates that the control is static or not selected. + + + {basicExample} + + +### Selected -Form Pills are given a closing "x" button when provided an `onDismiss` event handler. Because the Form Pill Group is largely presentational, you can provide "removable" functionality by managing the state of the rendered pills. By responding to user interactions and hooking into the `onDismiss` event handler, the rendered state of the Form Pill Group can be updated and pills can be selectively removed from the collection. +A Form Pill can be placed into a selected state by setting the `selected` property. -The `onDismiss` event handler will fire when a user clicks on the close x, or presses their backspace or delete key when foused on a pill. + + {selectedStateExample} + + +### Error + +Use an Error Form Pill to highlight an object that the user must be made aware of because it’s considered to be in a bad or broken state and should be addressed. An error icon will display as a prefix to the rest of the children in the pill. + + + {errorStateExample} + + +### Disabled - - {removableExample} +Use a disabled Form Pill to indicate that a particular option cannot be interacted with or can be safely ignored. + + + {disabledStateExample} -### Simple Multiselect Combobox Example +## Composition notes + +### Truncating text -Using Form Pill Group, along with the [Combobox Primitive](/primitives/combobox-primitive#multiselect-combobox-example) you can easily compose an interactive multiple select form field. +Pill text should never wrap to the next line. If the text length is longer than the max width of the pill group’s container, consider moving the Pill to a new row or use [Truncate](/components/truncate) to truncate the Form Pill text. - {multiSelectExample} + {truncateExample} -### Internationalization +### Adding an avatar -To internationalize the form pill group, simply pass different text as children to the pills. The only exception is the visually hidden text that explains how to remove and select pills with a keyboard–to change this, use the `i18nKeyboardControls` prop. +A Form Pill can have an optional [Avatar](/components/avatar). Considering the size of a Form Pill, it is recommended to use either an image or icon within an Avatar, and to avoid using initials as some initials may get cut off if the characters are particularly wide. - - {i18nExample} +We recommend placing the Avatar ahead of the pill text. Use no more than one before or after the text. + + + {avatarExample} -## Composition Notes +### Adding an icon + +A Form Pill can have an optional [Icon](/components/icons). We recommend placing the Icon ahead of the pill text. Use no more than one before or after the text. + + + {iconExample} + ### When to use a Form Pill Group -Use Form Pill Groups when you are editing a collection of data within a form. It can be used to represent selection across multiple fields such as filtering, or from a single field like a combobox. +Use a Form Pill Group when you’re editing a collection of data within a form. It can be used to represent selection across multiple fields such as filtering, or from a single field like a [Combobox](/components/combobox). - - + + @@ -232,23 +347,23 @@ yarn add @twilio-paste/form-pill-group - or - yarn add @twilio-paste/core ```jsx import {useFormPillState, FormPillGroup, FormPill} from '@twilio-paste/core/form-pill-group'; +import {ProductVideoIcon} from '@twilio-paste/icons/esm/ProductVideoIcon'; +import {ProductVerifyIcon} from '@twilio-paste/icons/esm/ProductVerifyIcon'; -export const Basic = () => { +export const BasicFormPillGroup = () => { const pillState = useFormPillState(); return (
- - - - Tennis + + Voice + + + Video - - Baseball - - Football - - Soccer + + + Verify @@ -262,258 +377,100 @@ export const Basic = () => { **Note:** Most required props are provided by spreading the returned state from `useFormPillState`. -###### `aria-label: string` - -Defines a string value that labels the current element. - -###### `move: (id: string | null) => void` - -Moves focus to a given item ID. - -###### `first: () => void` - -Moves focus to the first item. - -###### `last: () => void` - -Moves focus to the first item. - -###### `items: Item[]` - -Lists all the pill items with their id, DOM ref, disabled state and groupId if any. This state is automatically updated when registerItem and unregisterItem are called. - -###### `setCurrentId: Dispatch>` - -Lists all the pill items with their id, DOM ref, disabled state and groupId if any. This state is automatically updated when registerItem and unregisterItem are called. - -###### `element?: string` - -Overrides the default element name ('FORM_PILL_GROUP') to apply unique styles with the Customization Provider - -###### `focusable?: boolean` - -When an element is disabled, it may still be focusable. It works similarly to readOnly on form elements. In this case, only aria-disabled will be set. - -###### `disabled?: boolean` - -Same as the HTML attribute. - -###### `baseId?: string` - -ID that will serve as a base for all the items IDs. - -###### `currentId?: string` - -The current focused item id. - -- undefined will automatically focus the first enabled composite item. -- null will focus the base composite element and users will be able to navigate out of it using arrow keys. -- If currentId is initially set to null, the base composite element itself will have focus and users will be able to navigate to it using arrow keys. - -###### `groups?: Group[]` - -Lists all the composite groups with their id and DOM ref. This state is automatically updated when registerGroup and unregisterGroup are called. - -###### `i18nKeyboardControls?: string` - -Visually hidden string that has instructions for how to remove and select pills with a keyboard. Default value is "Press Delete or Backspace to remove. Press Enter to toggle selection." - ---- +| Prop | Type | Description | Default | +| ----------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- | +| `aria-label` | `string` | The visually hidden label of the Form Pill Group | `''` | +| `move` | `(id: string \| null) => void` | Moves focus to a given item ID | | +| `first` | `() => void` | Moves focus to the first item | | +| `last` | `() => void` | Moves focus to the last item | | +| `items` | `Item[]` | Lists all the pill items with their id, DOM ref, disabled state and groupId if any. It is updated when registerItem and unregisterItem are called. | | +| `setCurrentId` | `Dispatch>` | Sets currentId. It only updates the currentId state without moving focus. | | +| `element?` | `string` | Overrides the default element name to apply unique styles with the Customization Provider | `'FORM_PILL_GROUP'` | +| `focusable?` | `boolean` | When an element is disabled, it may still be focusable. It works similarly to readOnly on form elements. In this case, only aria-disabled will be set. | | +| `disabled?` | `boolean` | Same as the HTML attribute | | +| `baseId?` | `string` | ID that will serve as a base for all the items IDs | | +| `currentId?` | `string` | The current focused item id | | +| `groups?` | `Group[]` | Lists all the composite groups with their id and DOM ref. It is updated when registerGroup and unregisterGroup are called. | | +| `i18nKeyboardControls?` | `string` | Visually hidden string that has instructions for how to remove and select pills with a keyboard. | `"Press Delete or Backspace to remove. Press Enter to toggle selection."` | ##### FormPill **Note:** Most required props are provided by spreading the returned state from `useFormPillState`. -###### `baseId: string` - -ID that will serve as a base for all the items IDs. - -###### `setBaseId: string` - -Sets baseId - -###### `rtl: boolean` - -Determines how next and previous functions will behave. If rtl is set to true, they will be inverted. This only affects the composite widget behavior. You still need to set dir="rtl" on HTML/CSS. - -###### `items: Item[]` - -Lists all the composite items with their id, DOM ref, disabled state and groupId if any. This state is automatically updated when registerItem and unregisterItem are called. - -###### `groups: Group[]` - -Lists all the composite groups with their id and DOM ref. This state is automatically updated when registerGroup and unregisterGroup are called. - -###### `loop: boolean` - -On one-dimensional composites: - -- true loops from the last item to the first item and vice-versa. -- horizontal loops only if orientation is horizontal or not set. -- vertical loops only if orientation is vertical or not set. -- If currentId is initially set to null, the composite element will be focused in between the last and first items. - -###### `wrap: boolean` - -Has effect only on two-dimensional composites. If enabled, moving to the next item from the last one in a row or column will focus the first item in the next row or column and vice-versa. - -- true wraps between rows and columns. -- horizontal wraps only between rows. -- vertical wraps only between columns. -- If loop matches the value of wrap, it'll wrap between the last item in the last row or column and the first item in the first row or column and vice-versa. - -###### `shift: boolean` - -Has effect only on two-dimensional composites. If enabled, moving up or down when there's no next item or the next item is disabled will shift to the item right before it. - -###### `registerItem: (item: Item) => void` - -Registers a composite item. - -###### `unregisterItem: (item: Item) => void` - -Unregisters a composite item. - -###### `registerGroup: (group: Group) => void` - -Registers a composite group. - -###### `unregisterGroup: (group: Group) => void` - -Unregisters a composite group. - -###### `move: (id: string | null) => void` - -Moves focus to a given item ID. - -###### `next: () => void` - -Moves focus to the next item. - -###### `previous: () => void` - -Moves focus to the previous item. - -###### `up: () => void` - -Moves focus to the item above. - -###### `down: () => void` - -Moves focus to the item below. - -###### `first: () => void` - -Moves focus to the first item. - -###### `last: () => void` - -Moves focus to the last item. - -###### `sort: () => void` - -Sorts the composite.items based on the items position in the DOM. This is especially useful after modifying the composite items order in the DOM. Most of the time, though, you don't need to manually call this function as the re-ordering happens automatically. - -###### `setRTL: boolean>` - -Sets rtl. - -###### `setOrientation: "horizontal" | "vertical" | undefined` - -Sets orientation. - -###### `setCurrentId: string | null | undefined` - -Sets currentId. This is different from composite.move as this only updates the currentId state without moving focus. When the composite widget gets focused by the user, the item referred by the currentId state will get focus. - -###### `setLoop: boolean | "horizontal" | "vertical"` - -Sets loop. - -###### `setWrap: boolean | "horizontal" | "vertical"` - -Sets wrap. - -###### `seShift: boolean>` - -Sets shift. - -###### `reset: () => void` - -Resets to initial state. - -###### `selected?: boolean` - -Set if a pill is in a selected state. - -###### `element?: string` - -Overrides the default element name ('FORM_PILL') to apply unique styles with the Customization Provider - -###### `onSelect?: () => void` - -Event handler called when a pill is selected. - -###### `onDismiss?: () => void` - -Event handler called when a pill is dismised. - -###### `onFocus?: () => void` - -Event handler called when a pill is focused. - -###### `onBlur?: () => void` - -Event handler called when a pill is blurred. - -###### `currentId?: string` - -The current focused item id. - -- undefined will automatically focus the first enabled composite item. -- null will focus the base composite element and users will be able to navigate out of it using arrow keys. -- If currentId is initially set to null, the base composite element itself will have focus and users will be able to navigate to it using arrow keys. - -###### `orientation?: horizontal | vertical` - -Defines the orientation of the composite widget. If the composite has a single row or column (one-dimensional), the orientation value determines which arrow keys can be used to move focus: - -- undefined: all arrow keys work. -- horizontal: only left and right arrow keys work. -- vertical: only up and down arrow keys work. - ---- +| Prop | Type | Description | Default | +| ----------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | +| `baseId` | `string` | ID that will serve as a base for all the items IDs | | +| `setBaseId` | `Dispatch>` | Sets the baseId | | +| `items` | `Item[]` | Lists all the pill items with their id, DOM ref, disabled state and groupId if any. It is updated when registerItem and unregisterItem are called. | | +| `groups` | `Group[]` | Lists all the composite groups with their id and DOM ref. It is updated when registerGroup and unregisterGroup are called. | | +| `rtl` | `boolean` | Determines how next and previous functions will behave | | +| `loop` | `boolean \| "horizontal" \| "vertical"` | Determines how the keyboard navigation loops through the items | `'horizontal'` | +| `registerItem` | `(item: Item) => void` | Registers a composite item. | | +| `unregisterItem` | `(item: Item) => void` | Unregisters a composite item. | | +| `registerGroup` | `(group: Group) => void` | Registers a composite group. | | +| `unregisterGroup` | `(group: Group) => void` | Unregisters a composite group. | | +| `move` | `(id: string \| null) => void` | Moves focus to a given item ID | | +| `next` | `() => void` | Moves focus to the next item | | +| `previous` | `() => void` | Moves focus to the previous item | | +| `up` | `() => void` | Moves focus to the item above | | +| `down` | `() => void` | Moves focus to the item below | | +| `first` | `() => void` | Moves focus to the first item | | +| `last` | `() => void` | Moves focus to the last item | | +| `sort` | `() => void` | Sorts the items based on their position in the DOM | | +| `setRTL` | `Dispatch>` | Sets rtl | | +| `setOrientation` | `Dispatch>` | Sets orientation | | +| `setCurrentId` | `Dispatch>` | Sets currentId. It only updates the currentId state without moving focus. | | +| `setLoop` | `Dispatch>` | Sets loop | | +| `reset` | `() => void` | Resets to the initial state | | +| `variant?` | `"default" \| "error"` | Sets the variant of the pill | `"default"` | +| `selected?` | `boolean` | Set if a pill is in a selected state | | +| `element?` | `string` | Overrides the default element name to apply unique styles with the Customization Provider | `'FORM_PILL'` | +| `disabled?` | `boolean` | Set if a pill is disabled | | +| `onSelect?` | `() => void` | Event handler called when a pill is selected | | +| `onDismiss?` | `() => void` | Event handler called when a pill is dismissed | | +| `onFocus?` | `() => void` | Event handler called when a pill is focused | | +| `onBlur?` | `() => void` | Event handler called when a pill is blurred | | +| `currentId?` | `string` | The current focused item id | | +| `orientation?` | `"horizontal" \| "vertifcal"` | Defines the orientation of the pill | | +| `i18nErrorLabel?` | `string` | Alternative text for the error icon in the error variant | `"(error)"` | ##### useFormPillState -###### `baseId?: string` +| Prop | Type | Description | Default | +| ------------ | --------- | ------------------------------------------------------ | ------- | +| `baseId?` | `string` | ID that will serve as a base for all the items IDs | | +| `rtl?` | `boolean` | Determines how next and previous functions will behave | | +| `currentId?` | `string` | The current focused item id | | -ID that will serve as a base for all the items IDs. +### Figma -###### `rtl?: boolean` - -Determines how next and previous functions will behave. If rtl is set to true, they will be inverted. This only affects the composite widget behavior. You still need to set dir="rtl" on HTML/CSS. +#### Usage -###### `wrap?: boolean` +Form Pill Group is available in the Paste Components Figma library. It has 1 base component, Form Pill, with a ⚙️ emoji in its layer name to indicate there are nested configurable properties available. -Has effect only on two-dimensional composites. If enabled, moving to the next item from the last one in a row or column will focus the first item in the next row or column and vice-versa. +In code, when there are more pills than the max width, they will wrap to the next row. To accomplish the same behavior in Figma, please use the `Row count` property to select a number of rows, or place multiple Form Pill Group components under one another. -- true wraps between rows and columns. -- horizontal wraps only between rows. -- vertical wraps only between columns. -- If loop matches the value of wrap, it'll wrap between the last item in the last row or column and the first item in the first row or column and vice-versa. +Form Pill Group also contains other Paste components as nested components, including [Avatar](/components/avatar) and [Icon](/components/icons). -###### `shift?: boolean` +#### Properties -Has effect only on two-dimensional composites. If enabled, moving up or down when there's no next item or the next item is disabled will shift to the item right before it. +Here is a properties table for Form Pill Group: -###### `currentId?: string` +| Property | Variants | Description | Default | +| --------- | ---------- | ----------------------------------------------- | ------- | +| Row count | 1, 2, 3, 4 | Displays a number of rows in a Form Pill Group. | 1 | -The current focused item id. +Here is a properties table for its nested component, Form Pill: -- undefined will automatically focus the first enabled composite item. -- null will focus the base composite element and users will be able to navigate out of it using arrow keys. -- If currentId is initially set to null, the base composite element itself will have focus and users will be able to navigate to it using arrow keys. +| Property | Variants | Description | Default | +| -------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| Variant | `Basic`, `Selectable`, `Dismissible`, `Selectable + Dismissible` | The variant of the Display Pill. | `Basic` | +| State | `Default`, `Hover (Select)`, `Hover (Dismiss)`, `Focus`, `Disabled` | The state of the Display Pill. | `Default` | +| Selected | "Yes", "No" | Toggles to show the selected state of a Form Pill. This property is only configurable if `Variant=Selectable` or `Variant=Selectable + Dismissible`. | "No" | +| Error | "Yes", "No" | Toggles to show the error state of a Form Pill. | "No" | +| Prefix | `None`, `Icon`, `Avatar` | Adds a prefix ahead of the Form Pill label. If set to `Avatar`, manage the nested properties of Avatar in the layer marked with a ⚙️ emoji. | `None` | +| Icon | | Swaps the icon within a Form Pill. This property is visible in the Figma side panel if `Prefix=Icon`. | | +| Label | | Sets the label of a Form Pill | "Label" | diff --git a/yarn.lock b/yarn.lock index dbe73c9b9f..cc7bb24640 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9830,6 +9830,7 @@ __metadata: prop-types: ^15.7.2 react: ^17.0.2 react-dom: ^17.0.2 + typescript: ^4.6.4 peerDependencies: "@twilio-paste/animation-library": ^0.3.2 "@twilio-paste/customization": ^4.0.0