From 6a7025856ffbd7be302fa0096246e8b4dfae9efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cnora?= Date: Thu, 1 Aug 2024 12:21:11 -0400 Subject: [PATCH 01/11] feat(popover): add form pill button --- .../paste-codemods/tools/.cache/mappings.json | 1 + .../components/popover/package.json | 2 + .../popover/src/PopoverFormPillButton.tsx | 27 +++++++++++ .../components/popover/src/index.tsx | 3 +- .../components/popover/src/types.ts | 13 +++++ .../popover/stories/index.stories.tsx | 48 ++++++++++++++++++- yarn.lock | 2 + 7 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 packages/paste-core/components/popover/src/PopoverFormPillButton.tsx diff --git a/packages/paste-codemods/tools/.cache/mappings.json b/packages/paste-codemods/tools/.cache/mappings.json index eadc2c00d9..7b7400e115 100644 --- a/packages/paste-codemods/tools/.cache/mappings.json +++ b/packages/paste-codemods/tools/.cache/mappings.json @@ -207,6 +207,7 @@ "PopoverBadgeButton": "@twilio-paste/core/popover", "PopoverButton": "@twilio-paste/core/popover", "PopoverContainer": "@twilio-paste/core/popover", + "PopoverFormPillButton": "@twilio-paste/core/popover", "usePopoverState": "@twilio-paste/core/popover", "ProductSwitcher": "@twilio-paste/core/product-switcher", "ProductSwitcherButton": "@twilio-paste/core/product-switcher", diff --git a/packages/paste-core/components/popover/package.json b/packages/paste-core/components/popover/package.json index 132b3a75bd..cc16abf144 100644 --- a/packages/paste-core/components/popover/package.json +++ b/packages/paste-core/components/popover/package.json @@ -33,6 +33,7 @@ "@twilio-paste/color-contrast-utils": "^5.0.0", "@twilio-paste/customization": "^8.0.0", "@twilio-paste/design-tokens": "^10.0.0", + "@twilio-paste/form-pill-group": "^8.0.1", "@twilio-paste/icons": "^12.0.0", "@twilio-paste/non-modal-dialog-primitive": "^2.0.0", "@twilio-paste/reakit-library": "^2.0.0", @@ -59,6 +60,7 @@ "@twilio-paste/color-contrast-utils": "^5.0.0", "@twilio-paste/customization": "^8.1.0", "@twilio-paste/design-tokens": "^10.2.0", + "@twilio-paste/form-pill-group": "^8.0.1", "@twilio-paste/icons": "^12.2.0", "@twilio-paste/non-modal-dialog-primitive": "^2.0.1", "@twilio-paste/reakit-library": "^2.1.0", diff --git a/packages/paste-core/components/popover/src/PopoverFormPillButton.tsx b/packages/paste-core/components/popover/src/PopoverFormPillButton.tsx new file mode 100644 index 0000000000..d363a5357c --- /dev/null +++ b/packages/paste-core/components/popover/src/PopoverFormPillButton.tsx @@ -0,0 +1,27 @@ +import { FormPill } from "@twilio-paste/form-pill-group"; +import { NonModalDialogDisclosurePrimitive } from "@twilio-paste/non-modal-dialog-primitive"; +import * as React from "react"; + +import { PopoverContext } from "./PopoverContext"; +import type { PopoverFormPillButtonProps } from "./types"; + +const PopoverFormPillButton = React.forwardRef( + ({ children, element = "POPOVER_FORM_PILL", ...popoverButtonProps }, ref) => { + const popover = React.useContext(PopoverContext); + + return ( + + {children} + + ); + }, +); + +PopoverFormPillButton.displayName = "PopoverFormPillButton"; +export { PopoverFormPillButton }; diff --git a/packages/paste-core/components/popover/src/index.tsx b/packages/paste-core/components/popover/src/index.tsx index 4f22197fcd..0954d2a858 100644 --- a/packages/paste-core/components/popover/src/index.tsx +++ b/packages/paste-core/components/popover/src/index.tsx @@ -10,4 +10,5 @@ export { Popover } from "./Popover"; export type { PopoverProps } from "./Popover"; export { PopoverButton } from "./PopoverButton"; export { PopoverBadgeButton } from "./PopoverBadgeButton"; -export type { PopoverButtonProps, PopoverBadgeButtonProps } from "./types"; +export { PopoverFormPillButton } from "./PopoverFormPillButton"; +export type { PopoverButtonProps, PopoverBadgeButtonProps, PopoverFormPillButtonProps } from "./types"; diff --git a/packages/paste-core/components/popover/src/types.ts b/packages/paste-core/components/popover/src/types.ts index 417b5b5ff1..f8b25b5689 100644 --- a/packages/paste-core/components/popover/src/types.ts +++ b/packages/paste-core/components/popover/src/types.ts @@ -1,6 +1,7 @@ import type { BadgeBaseProps, BadgeButtonProps } from "@twilio-paste/badge"; import type { BoxProps } from "@twilio-paste/box"; import type { ButtonProps } from "@twilio-paste/button"; +import type { FormPillProps } from "@twilio-paste/form-pill-group"; export type ButtonBadgeProps = BadgeBaseProps & Omit & { @@ -34,3 +35,15 @@ export type PopoverBadgeButtonProps = PopoverButtonBaseProps & */ element?: BoxProps["element"]; }; + +export type PopoverFormPillButtonProps = PopoverButtonBaseProps & + FormPillProps & { + /** + * Overrides the default element name to apply unique styles with the Customization Provider + * + * @default 'POPOVER_FORM_PILL' + * @type {BoxProps['element']} + * @memberof PopoverFormPillButtonProps + */ + element?: BoxProps["element"]; + }; diff --git a/packages/paste-core/components/popover/stories/index.stories.tsx b/packages/paste-core/components/popover/stories/index.stories.tsx index 8d6647c8b6..03d2ca2f9b 100644 --- a/packages/paste-core/components/popover/stories/index.stories.tsx +++ b/packages/paste-core/components/popover/stories/index.stories.tsx @@ -3,6 +3,7 @@ import { Box } from "@twilio-paste/box"; import { Button } from "@twilio-paste/button"; import { CustomizationProvider } from "@twilio-paste/customization"; import { DatePicker } from "@twilio-paste/date-picker"; +import { FormPillGroup, useFormPillState } from "@twilio-paste/form-pill-group"; import { Label } from "@twilio-paste/label"; import { Separator } from "@twilio-paste/separator"; import { Stack } from "@twilio-paste/stack"; @@ -12,7 +13,14 @@ import { TimePicker } from "@twilio-paste/time-picker"; import { useUID } from "@twilio-paste/uid-library"; import * as React from "react"; -import { Popover, PopoverBadgeButton, PopoverButton, PopoverContainer, usePopoverState } from "../src"; +import { + Popover, + PopoverBadgeButton, + PopoverButton, + PopoverContainer, + PopoverFormPillButton, + usePopoverState, +} from "../src"; // eslint-disable-next-line import/no-default-export export default { @@ -224,6 +232,44 @@ export const BadgePopover = (): JSX.Element => { ); }; +export const FormPillPopover = (): JSX.Element => { + const uniqueBaseID = useUID(); + const pillState = useFormPillState(); + const [selected, setSelected] = React.useState(true); + return ( + + + + { + console.log("pill selected"); + setSelected(!selected); + }} + onDismiss={() => { + console.log("pill dismissed"); + }} + > + Open popover + + + This is the Twilio styled popover that you can use in all your applications. + + + + + + ); +}; + export const StateHookExample = (): JSX.Element => { const uniqueBaseID = useUID(); const popover = usePopoverState({ baseId: uniqueBaseID }); diff --git a/yarn.lock b/yarn.lock index 59b6c3105d..8350a7bce6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14059,6 +14059,7 @@ __metadata: "@twilio-paste/color-contrast-utils": ^5.0.0 "@twilio-paste/customization": ^8.1.0 "@twilio-paste/design-tokens": ^10.2.0 + "@twilio-paste/form-pill-group": ^8.0.1 "@twilio-paste/icons": ^12.2.0 "@twilio-paste/non-modal-dialog-primitive": ^2.0.1 "@twilio-paste/reakit-library": ^2.1.0 @@ -14086,6 +14087,7 @@ __metadata: "@twilio-paste/color-contrast-utils": ^5.0.0 "@twilio-paste/customization": ^8.0.0 "@twilio-paste/design-tokens": ^10.0.0 + "@twilio-paste/form-pill-group": ^8.0.1 "@twilio-paste/icons": ^12.0.0 "@twilio-paste/non-modal-dialog-primitive": ^2.0.0 "@twilio-paste/reakit-library": ^2.0.0 From bbe60643c4d6f8e2043895a700b4c98793d9f455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cnora?= Date: Thu, 1 Aug 2024 15:21:18 -0400 Subject: [PATCH 02/11] chore: kristians fix --- .../popover/src/PopoverFormPillButton.tsx | 2 ++ .../popover/stories/index.stories.tsx | 22 ++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/paste-core/components/popover/src/PopoverFormPillButton.tsx b/packages/paste-core/components/popover/src/PopoverFormPillButton.tsx index d363a5357c..f1b8dc7a8c 100644 --- a/packages/paste-core/components/popover/src/PopoverFormPillButton.tsx +++ b/packages/paste-core/components/popover/src/PopoverFormPillButton.tsx @@ -16,6 +16,8 @@ const PopoverFormPillButton = React.forwardRef', but it is there. This is necessary because Form Pill uses onSelect as the onClick handler. + onSelect={popover.toggle} > {children} diff --git a/packages/paste-core/components/popover/stories/index.stories.tsx b/packages/paste-core/components/popover/stories/index.stories.tsx index 03d2ca2f9b..d1561f691d 100644 --- a/packages/paste-core/components/popover/stories/index.stories.tsx +++ b/packages/paste-core/components/popover/stories/index.stories.tsx @@ -244,8 +244,8 @@ export const FormPillPopover = (): JSX.Element => { {...pillState} selected={selected} onSelect={() => { + // note: because we're passing onClick from PopoverFormPillButton, this does not currently run console.log("pill selected"); - setSelected(!selected); }} onDismiss={() => { console.log("pill dismissed"); @@ -254,15 +254,17 @@ export const FormPillPopover = (): JSX.Element => { Open popover - This is the Twilio styled popover that you can use in all your applications. - + + + From f977cd52b849b928f5ddd38e82f4948315243217 Mon Sep 17 00:00:00 2001 From: Kristian Antrobus Date: Mon, 5 Aug 2024 15:52:51 -0500 Subject: [PATCH 03/11] feat(popover): form pill final impl --- .changeset/many-paws-walk.md | 7 + .../popover/__tests__/index.spec.tsx | 35 +- .../popover/src/PopoverFormPillButton.tsx | 11 +- .../popover/stories/index.stories.tsx | 13 +- .../components/popover/type-docs.json | 333 ++++++++++++++++++ .../src/component-examples/PopoverExamples.ts | 39 ++ .../src/pages/components/popover/index.mdx | 100 ++++-- 7 files changed, 490 insertions(+), 48 deletions(-) create mode 100644 .changeset/many-paws-walk.md diff --git a/.changeset/many-paws-walk.md b/.changeset/many-paws-walk.md new file mode 100644 index 0000000000..9a56423943 --- /dev/null +++ b/.changeset/many-paws-walk.md @@ -0,0 +1,7 @@ +--- +"@twilio-paste/codemods": minor +"@twilio-paste/popover": minor +"@twilio-paste/core": minor +--- + +[Popover] Added a new button variant to trigger the popover PopoverFormPillButton, only to be used as part of complex filters pattern diff --git a/packages/paste-core/components/popover/__tests__/index.spec.tsx b/packages/paste-core/components/popover/__tests__/index.spec.tsx index a17a7ae21b..1b4d5e829e 100644 --- a/packages/paste-core/components/popover/__tests__/index.spec.tsx +++ b/packages/paste-core/components/popover/__tests__/index.spec.tsx @@ -6,7 +6,7 @@ import { Theme } from "@twilio-paste/theme"; import * as React from "react"; import { Popover, PopoverButton, PopoverContainer } from "../src"; -import { BadgePopover, InitialFocus, PopoverTop, StateHookExample } from "../stories/index.stories"; +import { BadgePopover, FormPillPopover, InitialFocus, PopoverTop, StateHookExample } from "../stories/index.stories"; describe("Popover", () => { describe("Render", () => { @@ -140,6 +140,39 @@ describe("Popover", () => { }); }); + describe("PopoverFormPill", () => { + it("renders PopoverFormPillButton as a FormPill", () => { + render( + + + , + ); + const popoverButton = screen.getByText("Open popover").closest("button"); + expect(popoverButton).toHaveAttribute("data-paste-element", "POPOVER_FORM_PILL"); + }); + + it("should render a popover badge button with aria attributes", async () => { + render( + + + , + ); + const renderedPopoverButton = screen.getByText("Open popover").closest("button"); + const renderedPopover = screen.getByTestId("form-pill-popover"); + expect(renderedPopoverButton?.getAttribute("aria-haspopup")).toEqual("dialog"); + expect(renderedPopoverButton?.getAttribute("aria-controls")).toEqual(renderedPopover.id); + expect(renderedPopoverButton?.getAttribute("aria-expanded")).toEqual("false"); + expect(renderedPopover).not.toBeVisible(); + await waitFor(() => { + if (renderedPopoverButton) { + userEvent.click(renderedPopoverButton); + } + }); + expect(renderedPopoverButton?.getAttribute("aria-expanded")).toEqual("true"); + expect(renderedPopover).toBeVisible(); + }); + }); + describe("Customization", () => { it("should set default data-paste-element attribute on Popover and customizable children and respect custom styles", (): void => { render( diff --git a/packages/paste-core/components/popover/src/PopoverFormPillButton.tsx b/packages/paste-core/components/popover/src/PopoverFormPillButton.tsx index f1b8dc7a8c..a1bacf6dfc 100644 --- a/packages/paste-core/components/popover/src/PopoverFormPillButton.tsx +++ b/packages/paste-core/components/popover/src/PopoverFormPillButton.tsx @@ -16,8 +16,15 @@ const PopoverFormPillButton = React.forwardRef', but it is there. This is necessary because Form Pill uses onSelect as the onClick handler. - onSelect={popover.toggle} + onSelect={(e: React.MouseEvent) => { + // @ts-expect-error Property 'toggle' does not exist on type 'Partial', but it is there as it comes form DialogActions prop. + popover.toggle(); + // Call the actual onsSelect function passed to the component + if (popoverButtonProps.onSelect) { + popoverButtonProps.onSelect(e); + } + }} + baseId={popover.baseId} > {children} diff --git a/packages/paste-core/components/popover/stories/index.stories.tsx b/packages/paste-core/components/popover/stories/index.stories.tsx index d1561f691d..2207359b76 100644 --- a/packages/paste-core/components/popover/stories/index.stories.tsx +++ b/packages/paste-core/components/popover/stories/index.stories.tsx @@ -236,21 +236,12 @@ export const FormPillPopover = (): JSX.Element => { const uniqueBaseID = useUID(); const pillState = useFormPillState(); const [selected, setSelected] = React.useState(true); + return ( - { - // note: because we're passing onClick from PopoverFormPillButton, this does not currently run - console.log("pill selected"); - }} - onDismiss={() => { - console.log("pill dismissed"); - }} - > + {}}> Open popover diff --git a/packages/paste-core/components/popover/type-docs.json b/packages/paste-core/components/popover/type-docs.json index 3d84791579..288edfceca 100644 --- a/packages/paste-core/components/popover/type-docs.json +++ b/packages/paste-core/components/popover/type-docs.json @@ -5230,5 +5230,338 @@ "required": false, "externalProp": true } + }, + "PopoverFormPillButton": { + "baseId": { + "type": "string", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "ID that will serve as a base for all the items IDs." + }, + "down": { + "type": "(unstable_allTheWay?: boolean | undefined) => void", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Moves focus to the item below." + }, + "first": { + "type": "() => void", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Moves focus to the first item." + }, + "groups": { + "type": "Group[]", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Lists all the composite groups with their `id` and DOM `ref`. This state\nis automatically updated when `registerGroup` and `unregisterGroup` are\ncalled." + }, + "items": { + "type": "Item[]", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Lists all the composite items with their `id`, DOM `ref`, `disabled` state\nand `groupId` if any. This state is automatically updated when\n`registerItem` and `unregisterItem` are called." + }, + "last": { + "type": "() => void", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Moves focus to the last item." + }, + "loop": { + "type": "boolean | Orientation", + "defaultValue": false, + "required": true, + "externalProp": true, + "description": "On one-dimensional composites:\n - `true` loops from the last item to the first item and vice-versa.\n - `horizontal` loops only if `orientation` is `horizontal` or not set.\n - `vertical` loops only if `orientation` is `vertical` or not set.\n - If `currentId` is initially set to `null`, the composite element will\nbe focused in between the last and first items.\n\nOn two-dimensional composites:\n - `true` loops from the last row/column item to the first item in the\nsame row/column and vice-versa. If it's the last item in the last row, it\nmoves to the first item in the first row and vice-versa.\n - `horizontal` loops only from the last row item to the first item in\nthe same row.\n - `vertical` loops only from the last column item to the first item in\nthe column row.\n - If `currentId` is initially set to `null`, vertical loop will have no\neffect as moving down from the last row or up from the first row will\nfocus the composite element.\n - If `wrap` matches the value of `loop`, it'll wrap between the last\nitem in the last row or column and the first item in the first row or\ncolumn and vice-versa." + }, + "move": { + "type": "(id: string | null) => void", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Moves focus to a given item ID." + }, + "next": { + "type": "(unstable_allTheWay?: boolean | undefined) => void", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Moves focus to the next item." + }, + "previous": { + "type": "(unstable_allTheWay?: boolean | undefined) => void", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Moves focus to the previous item." + }, + "registerGroup": { + "type": "(group: Group) => void", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Registers a composite group." + }, + "registerItem": { + "type": "(item: Item) => void", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Registers a composite item." + }, + "reset": { + "type": "() => void", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Resets to initial state." + }, + "rtl": { + "type": "boolean", + "defaultValue": false, + "required": true, + "externalProp": true, + "description": "Determines how `next` and `previous` functions will behave. If `rtl` is\nset to `true`, they will be inverted. This only affects the composite\nwidget behavior. You still need to set `dir=\"rtl\"` on HTML/CSS." + }, + "setBaseId": { + "type": "Dispatch>", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Sets `baseId`." + }, + "setCurrentId": { + "type": "Dispatch>", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Sets `currentId`. This is different from `composite.move` as this only\nupdates the `currentId` state without moving focus. When the composite\nwidget gets focused by the user, the item referred by the `currentId`\nstate will get focus." + }, + "setLoop": { + "type": "Dispatch>", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Sets `loop`." + }, + "setOrientation": { + "type": "Dispatch>", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Sets `orientation`." + }, + "setRTL": { + "type": "Dispatch>", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Sets `rtl`." + }, + "setShift": { + "type": "Dispatch>", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Sets `shift`." + }, + "setWrap": { + "type": "Dispatch>", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Sets `wrap`." + }, + "shift": { + "type": "boolean", + "defaultValue": false, + "required": true, + "externalProp": true, + "description": "**Has effect only on two-dimensional composites**. If enabled, moving up\nor down when there's no next item or the next item is disabled will shift\nto the item right before it." + }, + "sort": { + "type": "() => void", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Sorts the `composite.items` based on the items position in the DOM. This\nis especially useful after modifying the composite items order in the DOM.\nMost of the time, though, you don't need to manually call this function as\nthe re-ordering happens automatically." + }, + "unregisterGroup": { + "type": "(id: string) => void", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Unregisters a composite group." + }, + "unregisterItem": { + "type": "(id: string) => void", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Unregisters a composite item." + }, + "unstable_hasActiveWidget": { + "type": "boolean", + "defaultValue": false, + "required": true, + "externalProp": true + }, + "unstable_idCountRef": { + "type": "MutableRefObject", + "defaultValue": null, + "required": true, + "externalProp": true + }, + "unstable_includesBaseElement": { + "type": "boolean", + "defaultValue": false, + "required": true, + "externalProp": true + }, + "unstable_moves": { + "type": "number", + "defaultValue": "0", + "required": true, + "externalProp": true, + "description": "Stores the number of moves that have been performed by calling `move`,\n`next`, `previous`, `up`, `down`, `first` or `last`." + }, + "unstable_setHasActiveWidget": { + "type": "Dispatch>", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Sets `hasActiveWidget`." + }, + "unstable_setIncludesBaseElement": { + "type": "Dispatch>", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Sets `includesBaseElement`." + }, + "unstable_setVirtual": { + "type": "Dispatch>", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Sets `virtual`." + }, + "unstable_virtual": { + "type": "boolean", + "defaultValue": false, + "required": true, + "externalProp": true, + "description": "If enabled, the composite element will act as an\n[aria-activedescendant](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_focus_activedescendant)\ncontainer instead of\n[roving tabindex](https://www.w3.org/TR/wai-aria-practices/#kbd_roving_tabindex).\nDOM focus will remain on the composite while its items receive virtual focus." + }, + "up": { + "type": "(unstable_allTheWay?: boolean | undefined) => void", + "defaultValue": null, + "required": true, + "externalProp": true, + "description": "Moves focus to the item above." + }, + "wrap": { + "type": "boolean | Orientation", + "defaultValue": false, + "required": true, + "externalProp": true, + "description": "**Has effect only on two-dimensional composites**. If enabled, moving to\nthe next item from the last one in a row or column will focus the first\nitem in the next row or column and vice-versa.\n - `true` wraps between rows and columns.\n - `horizontal` wraps only between rows.\n - `vertical` wraps only between columns.\n - If `loop` matches the value of `wrap`, it'll wrap between the last\nitem in the last row or column and the first item in the first row or\ncolumn and vice-versa." + }, + "currentId": { + "type": "string", + "defaultValue": "undefined", + "required": false, + "externalProp": true, + "description": "The current focused item `id`.\n - `undefined` will automatically focus the first enabled composite item.\n - `null` will focus the base composite element and users will be able to\nnavigate out of it using arrow keys.\n - If `currentId` is initially set to `null`, the base composite element\nitself will have focus and users will be able to navigate to it using\narrow keys." + }, + "disabled": { + "type": "boolean", + "defaultValue": null, + "required": false, + "externalProp": false, + "description": "Set if a pill is disabled" + }, + "element": { + "type": "string", + "defaultValue": "'FORM_PILL'", + "required": false, + "externalProp": false, + "description": "Overrides the default element name to apply unique styles with the Customization Provider" + }, + "i18nErrorLabel": { + "type": "string", + "defaultValue": "'(error)'", + "required": false, + "externalProp": false, + "description": "Alternative text for the error icon in the error variant" + }, + "id": { + "type": "string", + "defaultValue": null, + "required": false, + "externalProp": false + }, + "onBlur": { + "type": "() => void", + "defaultValue": null, + "required": false, + "externalProp": false, + "description": "Event handler called when a pill is blurred" + }, + "onDismiss": { + "type": "(\n event: MouseEvent | KeyboardEvent\n) => void", + "defaultValue": null, + "required": false, + "externalProp": false, + "description": "Event handler called when a pill is dismissed" + }, + "onFocus": { + "type": "() => void", + "defaultValue": null, + "required": false, + "externalProp": false, + "description": "Event handler called when a pill is focused" + }, + "onSelect": { + "type": "(\n event: MouseEvent\n) => void", + "defaultValue": null, + "required": false, + "externalProp": false, + "description": "Event handler called when a pill is selected" + }, + "orientation": { + "type": "Orientation", + "defaultValue": "undefined", + "required": false, + "externalProp": true, + "description": "Defines the orientation of the composite widget. If the composite has a\nsingle row or column (one-dimensional), the `orientation` value determines\nwhich arrow keys can be used to move focus:\n - `undefined`: all arrow keys work.\n - `horizontal`: only left and right arrow keys work.\n - `vertical`: only up and down arrow keys work.\n\nIt doesn't have any effect on two-dimensional composites." + }, + "selected": { + "type": "boolean", + "defaultValue": null, + "required": false, + "externalProp": false, + "description": "Set if a pill is in a selected state" + }, + "toggle": { + "type": "() => void", + "defaultValue": null, + "required": false, + "externalProp": false + }, + "variant": { + "type": "PillVariant", + "defaultValue": "'default'", + "required": false, + "externalProp": false, + "description": "Sets the variant of the pill" + } } } diff --git a/packages/paste-website/src/component-examples/PopoverExamples.ts b/packages/paste-website/src/component-examples/PopoverExamples.ts index a9079d1a9a..89179b4163 100644 --- a/packages/paste-website/src/component-examples/PopoverExamples.ts +++ b/packages/paste-website/src/component-examples/PopoverExamples.ts @@ -435,3 +435,42 @@ render( ) `.trim(); + +export const popoverFormPillExample = ` +const PopoverFormPillExample = () => { + const uniqueBaseID = useUID(); + const pillState = useFormPillState(); + const [selected, setSelected] = useState(true); + + return ( + + + {}} + > + Open popover + + + + + + + + + ); +}; + +render( + +) +`.trim(); diff --git a/packages/paste-website/src/pages/components/popover/index.mdx b/packages/paste-website/src/pages/components/popover/index.mdx index b158a2ed95..4556d3afca 100644 --- a/packages/paste-website/src/pages/components/popover/index.mdx +++ b/packages/paste-website/src/pages/components/popover/index.mdx @@ -1,33 +1,42 @@ export const meta = { - title: 'Popover', - package: '@twilio-paste/popover', - description: 'An accessible and styled popover component.', - slug: '/components/popover/', + title: "Popover", + package: "@twilio-paste/popover", + description: "An accessible and styled popover component.", + slug: "/components/popover/", }; -import {usePopoverState, Popover, PopoverContainer, PopoverButton, PopoverBadgeButton} from '@twilio-paste/popover'; -import Changelog from '@twilio-paste/popover/CHANGELOG.md'; -import packageJson from '@twilio-paste/popover/package.json'; -import {AspectRatio} from '@twilio-paste/aspect-ratio'; -import {Box} from '@twilio-paste/box'; -import {Button} from '@twilio-paste/button'; -import {ButtonGroup} from '@twilio-paste/button-group'; -import {CheckboxGroup, Checkbox} from '@twilio-paste/checkbox'; -import {Form, FormActions, FormControl} from '@twilio-paste/form'; -import {Heading} from '@twilio-paste/heading'; -import {Input} from '@twilio-paste/input'; -import {Label} from '@twilio-paste/label'; -import {Paragraph} from '@twilio-paste/paragraph'; -import {Select, Option} from '@twilio-paste/select'; -import {Separator} from '@twilio-paste/separator'; -import {Text} from '@twilio-paste/text'; -import {useUIDSeed} from '@twilio-paste/uid-library'; -import {PlusIcon} from '@twilio-paste/icons/esm/PlusIcon'; -import {NewIcon} from '@twilio-paste/icons/esm/NewIcon'; -import {WarningIcon} from '@twilio-paste/icons/esm/WarningIcon'; -import {InformationIcon} from '@twilio-paste/icons/esm/InformationIcon'; - -import {SidebarCategoryRoutes} from '../../../constants'; +import { + PopoverFormPillButton, + usePopoverState, + Popover, + PopoverContainer, + PopoverButton, + PopoverBadgeButton, +} from "@twilio-paste/popover"; +import Changelog from "@twilio-paste/popover/CHANGELOG.md"; +import packageJson from "@twilio-paste/popover/package.json"; +import { AspectRatio } from "@twilio-paste/aspect-ratio"; +import { Box } from "@twilio-paste/box"; +import { Button } from "@twilio-paste/button"; +import { ButtonGroup } from "@twilio-paste/button-group"; +import { CheckboxGroup, Checkbox } from "@twilio-paste/checkbox"; +import { Form, FormActions, FormControl } from "@twilio-paste/form"; +import { FormPillGroup, useFormPillState } from "@twilio-paste/form-pill-group"; +import { Heading } from "@twilio-paste/heading"; +import { Input } from "@twilio-paste/input"; +import { Label } from "@twilio-paste/label"; +import { Paragraph } from "@twilio-paste/paragraph"; +import { Select, Option } from "@twilio-paste/select"; +import { Separator } from "@twilio-paste/separator"; +import { Text } from "@twilio-paste/text"; +import { useUIDSeed, useUID } from "@twilio-paste/uid-library"; +import { PlusIcon } from "@twilio-paste/icons/esm/PlusIcon"; +import { NewIcon } from "@twilio-paste/icons/esm/NewIcon"; +import { WarningIcon } from "@twilio-paste/icons/esm/WarningIcon"; +import { InformationIcon } from "@twilio-paste/icons/esm/InformationIcon"; +import { useState } from "react"; + +import { SidebarCategoryRoutes } from "../../../constants"; import { defaultExample, postionExample, @@ -38,16 +47,17 @@ import { i18nExample, interactiveContent, initialFocus, -} from '../../../component-examples/PopoverExamples'; -import ComponentPageLayout from '../../../layouts/ComponentPageLayout'; -import {getFeature, getNavigationData} from '../../../utils/api'; -import {DoDont, Do, Dont} from '../../../components/DoDont'; + popoverFormPillExample +} from "../../../component-examples/PopoverExamples"; +import ComponentPageLayout from "../../../layouts/ComponentPageLayout"; +import { getFeature, getNavigationData } from "../../../utils/api"; +import { DoDont, Do, Dont } from "../../../components/DoDont"; export default ComponentPageLayout; export const getStaticProps = async () => { const navigationData = await getNavigationData(); - const feature = await getFeature('Popover'); + const feature = await getFeature("Popover"); return { props: { data: { @@ -58,8 +68,8 @@ export const getStaticProps = async () => { mdxHeadings, pageHeaderData: { categoryRoute: SidebarCategoryRoutes.COMPONENTS, - githubUrl: 'https://github.com/twilio-labs/paste/tree/main/packages/paste-core/components/popover', - storybookUrl: '/?path=/story/components-popover--default', + githubUrl: "https://github.com/twilio-labs/paste/tree/main/packages/paste-core/components/popover", + storybookUrl: "/?path=/story/components-popover--default", }, }, }; @@ -298,6 +308,27 @@ To launch a Popover from a Badge component, use the PopoverBadgeButton component {badgeExample} +### PopoverFormPillButton + +This component is to **only be used as part of the Filters pattern**. It renders a FormPill and accepts all of its props, which are listed [on the FormPillGroup page](/components/form-pill-group#basic). + + + {popoverFormPillExample} + + ### Using state hooks Popover comes with the option of "hooking" into the internal state by using the state hook originally provided by @@ -406,3 +437,4 @@ Popover content should use full sentences and punctuation. Titles are optional. body="Don’t use a Popover to guide users through a complex workflow with a series of steps or for presenting critical information. Use a Modal for workflows, and an Alert Dialog for critical information." /> +import {useState} from "react" From ae312371e1a23e4cc6f8d154b8ddbf8558d080e0 Mon Sep 17 00:00:00 2001 From: Kristian Antrobus Date: Tue, 13 Aug 2024 11:05:41 -0500 Subject: [PATCH 04/11] feat(popover): added variant to FormPillGroup and created PopoverFormPillButton --- .changeset/five-points-change.md | 6 +++ .../form-pill-group/__tests__/index.spec.tsx | 29 +++++++++++- .../form-pill-group/src/FormPillButton.tsx | 10 ++--- .../form-pill-group/src/FormPillGroup.tsx | 17 ++++--- .../components/form-pill-group/src/types.ts | 1 + .../form-pill-group/src/useFormPillState.tsx | 4 +- .../form-pill-group/stories/index.stories.tsx | 45 +++++++++++++++++++ .../components/form-pill-group/type-docs.json | 9 +++- .../popover/__tests__/index.spec.tsx | 26 ++++++----- .../popover/stories/index.stories.tsx | 25 +++++++++-- 10 files changed, 144 insertions(+), 28 deletions(-) create mode 100644 .changeset/five-points-change.md diff --git a/.changeset/five-points-change.md b/.changeset/five-points-change.md new file mode 100644 index 0000000000..984fdb302c --- /dev/null +++ b/.changeset/five-points-change.md @@ -0,0 +1,6 @@ +--- +"@twilio-paste/form-pill-group": minor +"@twilio-paste/core": minor +--- + +[FormPillGroup] added a new variant 'tree' to support different interactions for FormPill where selecting the item triggers other flows instead of updating state directly. Reference Filters Pattern for in depth use case. 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 c685d7c9dc..eb5e09dc7a 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 @@ -4,7 +4,7 @@ import * as React from "react"; import { FormPill, FormPillGroup, useFormPillState } from "../src"; import { CustomFormPillGroup } from "../stories/customization.stories"; -import { Basic, SelectableAndDismissable } from "../stories/index.stories"; +import { Basic, FormPillTreeVariant, SelectableAndDismissable } from "../stories/index.stories"; const CustomElementFormPillGroup = (): JSX.Element => { const pillState = useFormPillState(); @@ -210,4 +210,31 @@ describe("FormPillGroup", () => { expect(errorLabel).toBeDefined(); }); }); + + describe("tree variant", () => { + it("should have the correct role for tree variant", () => { + render(); + + const group = screen.getByTestId("form-pill-group"); + expect(group.getAttribute("role")).toBe("tree"); + + const pill = screen.getByTestId("form-pill-1"); + expect(pill.getAttribute("role")).toBe("treeitem"); + }); + + it("should be dismissable and selectable", () => { + const { container } = render(); + + const pill = screen.getByTestId("form-pill-0"); + fireEvent.click(pill); + expect(pill.getAttribute("aria-selected")).toBe("true"); + + fireEvent.click(pill); + expect(pill.getAttribute("aria-selected")).toBe("false"); + + const pillX = container.querySelector('[data-paste-element="FORM_PILL_CLOSE"]'); + fireEvent.click(pillX as Element); + expect(pill).not.toBeInTheDocument(); + }); + }); }); diff --git a/packages/paste-core/components/form-pill-group/src/FormPillButton.tsx b/packages/paste-core/components/form-pill-group/src/FormPillButton.tsx index f06f1582d5..383815a112 100644 --- a/packages/paste-core/components/form-pill-group/src/FormPillButton.tsx +++ b/packages/paste-core/components/form-pill-group/src/FormPillButton.tsx @@ -51,7 +51,7 @@ export const FormPillButton = React.forwardRef const hasHoverStyles = isHoverable && !isDisabled; return hasHoverStyles ? { ...pillStyles[variant], ...hoverPillStyles[variant] } : pillStyles[variant]; }, [isHoverable, isDisabled, variant]); - const { size } = React.useContext(FormPillGroupContext); + const { size, variant: groupVariant } = React.useContext(FormPillGroupContext); const { height, fontSize } = sizeStyles[size]; return ( @@ -61,9 +61,9 @@ export const FormPillButton = React.forwardRef ref={ref} aria-selected={selected} aria-disabled={isDisabled} - role="option" - type="button" - as="button" + role={groupVariant === "tree" ? "treeitem" : "option"} + type={groupVariant === "tree" ? undefined : "button"} + as={groupVariant === "tree" ? "div" : "button"} margin="space0" position="relative" borderRadius="borderRadiusPill" @@ -79,7 +79,7 @@ export const FormPillButton = React.forwardRef transition="background-color 150ms ease-in, border-color 150ms ease-in, box-shadow 150ms ease-in, color 150ms ease-in" {...computedStyles} > - + {variant === "error" ? ( <> 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 7b5bfa21e4..f305eaf214 100644 --- a/packages/paste-core/components/form-pill-group/src/FormPillGroup.tsx +++ b/packages/paste-core/components/form-pill-group/src/FormPillGroup.tsx @@ -6,7 +6,7 @@ import { ScreenReaderOnly } from "@twilio-paste/screen-reader-only"; import { useUID } from "@twilio-paste/uid-library"; import * as React from "react"; -import type { FormPillGroupSizeVariant } from "./types"; +import type { FormPillGroupSizeVariant, FormPillGroupUsageVariants } from "./types"; import { FormPillGroupContext } from "./useFormPillState"; export interface FormPillGroupProps @@ -49,6 +49,13 @@ export interface FormPillGroupProps * @memberof FormPillGroupProps */ size?: FormPillGroupSizeVariant; + /** + * The variant of the FormPillGroup to use. The 'tree' option allows for more data to be displayed on select and still allows for select states. + * + * @default 'default' + * @memberof FormPillGroupProps + */ + variant?: FormPillGroupUsageVariants; } /** @@ -66,14 +73,14 @@ const SizeStyles: Record( - ({ element = "FORM_PILL_GROUP", display = "flex", size = "default", ...props }, ref) => { + ({ element = "FORM_PILL_GROUP", display = "flex", size = "default", variant = "default", ...props }, ref) => { return ( - + + {i18nKeyboardControls} {props.children} - {i18nKeyboardControls} ); }, diff --git a/packages/paste-core/components/form-pill-group/src/types.ts b/packages/paste-core/components/form-pill-group/src/types.ts index 5d39660b83..d734486473 100644 --- a/packages/paste-core/components/form-pill-group/src/types.ts +++ b/packages/paste-core/components/form-pill-group/src/types.ts @@ -4,3 +4,4 @@ export type PillVariant = "error" | "default"; export type VariantStyles = Record; /** The size variants for the FormPillGroup component. */ export type FormPillGroupSizeVariant = "default" | "large"; +export type FormPillGroupUsageVariants = "default" | "tree"; diff --git a/packages/paste-core/components/form-pill-group/src/useFormPillState.tsx b/packages/paste-core/components/form-pill-group/src/useFormPillState.tsx index 424dffa8fd..f34c8cc0ce 100644 --- a/packages/paste-core/components/form-pill-group/src/useFormPillState.tsx +++ b/packages/paste-core/components/form-pill-group/src/useFormPillState.tsx @@ -2,7 +2,7 @@ import { useCompositeState } from "@twilio-paste/reakit-library"; import type { CompositeInitialState, CompositeStateReturn } from "@twilio-paste/reakit-library"; import { createContext } from "react"; -import type { FormPillGroupSizeVariant } from "./types"; +import type { FormPillGroupSizeVariant, FormPillGroupUsageVariants } from "./types"; export type FormPillInitialState = Omit; @@ -18,8 +18,10 @@ export const useFormPillState = (config: FormPillInitialState = {}): CompositeSt export interface FormPillGroupContextState { size: FormPillGroupSizeVariant; + variant?: FormPillGroupUsageVariants; } export const FormPillGroupContext = createContext({ size: "default", + variant: "default", }); 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 360c1b21bd..6fe794b0b0 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 @@ -132,6 +132,51 @@ export const I18nProp = (): React.ReactNode => { I18nProp.storyName = "I18n Prop"; +export const FormPillTreeVariant = (): JSX.Element => { + 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)); + }} + onSelect={() => { + const newSelectedSet = new Set(selectedSet); + if (newSelectedSet.has(pill)) { + newSelectedSet.delete(pill); + } else { + newSelectedSet.add(pill); + } + updateSelectedSet(newSelectedSet); + }} + > + {index % 3 === 2 ? : null} + {index % 3 === 1 ? : null} + {pill} + + ))} + +
+ ); +}; + +FormPillTreeVariant.storyName = "FormPillGroup Tree Variant"; + // eslint-disable-next-line import/no-default-export export default { title: "Components/Form Pill Group", diff --git a/packages/paste-core/components/form-pill-group/type-docs.json b/packages/paste-core/components/form-pill-group/type-docs.json index a072ae0b71..74925cd975 100644 --- a/packages/paste-core/components/form-pill-group/type-docs.json +++ b/packages/paste-core/components/form-pill-group/type-docs.json @@ -810,7 +810,7 @@ "description": "Indicates an element's \"grabbed\" state in a drag-and-drop operation." }, "aria-haspopup": { - "type": "| boolean\n | \"true\"\n | \"false\"\n | \"dialog\"\n | \"grid\"\n | \"listbox\"\n | \"menu\"\n | \"tree\"", + "type": "| boolean\n | \"tree\"\n | \"true\"\n | \"false\"\n | \"dialog\"\n | \"grid\"\n | \"listbox\"\n | \"menu\"", "defaultValue": null, "required": false, "externalProp": true, @@ -2326,6 +2326,13 @@ "required": false, "externalProp": true }, + "variant": { + "type": "FormPillGroupUsageVariants", + "defaultValue": "'default'", + "required": false, + "externalProp": false, + "description": "The variant of the FormPillGroup to use. The 'tree' option allows for more data to be displayed on select and still allows for select states." + }, "vocab": { "type": "string", "defaultValue": null, diff --git a/packages/paste-core/components/popover/__tests__/index.spec.tsx b/packages/paste-core/components/popover/__tests__/index.spec.tsx index 1b4d5e829e..ccb0d1df8c 100644 --- a/packages/paste-core/components/popover/__tests__/index.spec.tsx +++ b/packages/paste-core/components/popover/__tests__/index.spec.tsx @@ -140,15 +140,17 @@ describe("Popover", () => { }); }); - describe("PopoverFormPill", () => { + describe("PopoverFormPillButton", () => { it("renders PopoverFormPillButton as a FormPill", () => { render( , ); - const popoverButton = screen.getByText("Open popover").closest("button"); - expect(popoverButton).toHaveAttribute("data-paste-element", "POPOVER_FORM_PILL"); + const popoverControl = screen + .getAllByText("Open popover")[0] + ?.closest('[data-paste-element="POPOVER_FORM_PILL"]'); + expect(popoverControl).toBeInTheDocument(); }); it("should render a popover badge button with aria attributes", async () => { @@ -157,18 +159,20 @@ describe("Popover", () => { , ); - const renderedPopoverButton = screen.getByText("Open popover").closest("button"); - const renderedPopover = screen.getByTestId("form-pill-popover"); - expect(renderedPopoverButton?.getAttribute("aria-haspopup")).toEqual("dialog"); - expect(renderedPopoverButton?.getAttribute("aria-controls")).toEqual(renderedPopover.id); - expect(renderedPopoverButton?.getAttribute("aria-expanded")).toEqual("false"); + const renderedPopoverControl = screen + .getAllByText("Open popover")[0] + ?.closest('[data-paste-element="POPOVER_FORM_PILL"]'); + const renderedPopover = screen.getAllByTestId("form-pill-popover")[0]; + expect(renderedPopoverControl?.getAttribute("aria-haspopup")).toEqual("dialog"); + expect(renderedPopoverControl?.getAttribute("aria-controls")).toEqual(renderedPopover.id); + expect(renderedPopoverControl?.getAttribute("aria-expanded")).toEqual("false"); expect(renderedPopover).not.toBeVisible(); await waitFor(() => { - if (renderedPopoverButton) { - userEvent.click(renderedPopoverButton); + if (renderedPopoverControl) { + userEvent.click(renderedPopoverControl); } }); - expect(renderedPopoverButton?.getAttribute("aria-expanded")).toEqual("true"); + expect(renderedPopoverControl?.getAttribute("aria-expanded")).toEqual("true"); expect(renderedPopover).toBeVisible(); }); }); diff --git a/packages/paste-core/components/popover/stories/index.stories.tsx b/packages/paste-core/components/popover/stories/index.stories.tsx index 2207359b76..1587d9ef0f 100644 --- a/packages/paste-core/components/popover/stories/index.stories.tsx +++ b/packages/paste-core/components/popover/stories/index.stories.tsx @@ -3,7 +3,7 @@ import { Box } from "@twilio-paste/box"; import { Button } from "@twilio-paste/button"; import { CustomizationProvider } from "@twilio-paste/customization"; import { DatePicker } from "@twilio-paste/date-picker"; -import { FormPillGroup, useFormPillState } from "@twilio-paste/form-pill-group"; +import { FormPill, FormPillGroup, useFormPillState } from "@twilio-paste/form-pill-group"; import { Label } from "@twilio-paste/label"; import { Separator } from "@twilio-paste/separator"; import { Stack } from "@twilio-paste/stack"; @@ -233,14 +233,13 @@ export const BadgePopover = (): JSX.Element => { }; export const FormPillPopover = (): JSX.Element => { - const uniqueBaseID = useUID(); const pillState = useFormPillState(); const [selected, setSelected] = React.useState(true); return ( - - + + {}}> Open popover @@ -258,6 +257,24 @@ export const FormPillPopover = (): JSX.Element => {
+ + {}}> + Open popover + + + + + + +
); From 015c7254e8a3e8102f8815c41f1625660882024f Mon Sep 17 00:00:00 2001 From: Kristian Antrobus Date: Tue, 13 Aug 2024 11:21:26 -0500 Subject: [PATCH 05/11] feat(docs): update popover and FormPill docs for new variants --- .../src/component-examples/FormPillGroup.tsx | 28 +++++++++++++++++++ .../src/component-examples/PopoverExamples.ts | 2 +- .../components/form-pill-group/index.mdx | 9 ++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/paste-website/src/component-examples/FormPillGroup.tsx b/packages/paste-website/src/component-examples/FormPillGroup.tsx index ddb3707fbb..25f5ffb654 100644 --- a/packages/paste-website/src/component-examples/FormPillGroup.tsx +++ b/packages/paste-website/src/component-examples/FormPillGroup.tsx @@ -54,6 +54,34 @@ render( ) `.trim(); +export const treeVariantExample = ` +const TreeVariantExample = () => { + const pillState = useFormPillState(); + + return ( +
+ + + Voice + + + + Video + + + + Verify + + +
+ ); +}; + +render( + +) +`.trim(); + export const selectableExample = ` const SelectableFormPillGroup = () => { const [pills] = React.useState(['SMS', 'MMS', 'Fax', 'Voice', 'Messaging', 'Chat']); diff --git a/packages/paste-website/src/component-examples/PopoverExamples.ts b/packages/paste-website/src/component-examples/PopoverExamples.ts index 89179b4163..1e0b68db2c 100644 --- a/packages/paste-website/src/component-examples/PopoverExamples.ts +++ b/packages/paste-website/src/component-examples/PopoverExamples.ts @@ -443,7 +443,7 @@ const PopoverFormPillExample = () => { const [selected, setSelected] = useState(true); return ( - + +### Tree Variant + +There is a `tree` variant for FormPillGroup. This changes the accessibility roles and DOM elements of the FormPill to be more tree-like where selecting the pill expands more options. This variant of Form Pills only for specific and approved use cases, such as in the filter group pattern (link coming soon!). + + + {treeVariantExample} + + ### Selectable Use a Selectable Form Pill to show an option that a user can select or deselect. From 04220a9707ff62be882f6e56f00abba5a35d6231 Mon Sep 17 00:00:00 2001 From: krisantrobus <55083528+krisantrobus@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:33:42 -0500 Subject: [PATCH 06/11] Apply suggestions from code review Co-authored-by: Sarah --- .../src/pages/components/form-pill-group/index.mdx | 4 ++-- packages/paste-website/src/pages/components/popover/index.mdx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 109a8b1602..4de7cfaef0 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 @@ -147,9 +147,9 @@ Use `size="large"` Form Pills only for specific and approved use cases, such as {largeExample} -### Tree Variant +### Tree variant for filters -There is a `tree` variant for FormPillGroup. This changes the accessibility roles and DOM elements of the FormPill to be more tree-like where selecting the pill expands more options. This variant of Form Pills only for specific and approved use cases, such as in the filter group pattern (link coming soon!). +The `tree` variant for FormPillGroup changes the accessibility roles and DOM elements of the FormPill to be more tree-like where selecting the pill expands more options. This variant of Form Pills is only for specific and approved use cases, such as in the filter pattern (link coming soon!). {treeVariantExample} diff --git a/packages/paste-website/src/pages/components/popover/index.mdx b/packages/paste-website/src/pages/components/popover/index.mdx index 4556d3afca..0503ab337f 100644 --- a/packages/paste-website/src/pages/components/popover/index.mdx +++ b/packages/paste-website/src/pages/components/popover/index.mdx @@ -310,7 +310,7 @@ To launch a Popover from a Badge component, use the PopoverBadgeButton component ### PopoverFormPillButton -This component is to **only be used as part of the Filters pattern**. It renders a FormPill and accepts all of its props, which are listed [on the FormPillGroup page](/components/form-pill-group#basic). +This component should **only be used as part of the filter pattern**. It renders a FormPill and accepts all of its props, which are listed [on the Form Pill Group page](/components/form-pill-group#basic). Date: Tue, 13 Aug 2024 13:35:02 -0500 Subject: [PATCH 07/11] chore(docs): pr structure suggestion --- .../pages/components/form-pill-group/index.mdx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 4de7cfaef0..fe201dcaf2 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 @@ -147,14 +147,6 @@ Use `size="large"` Form Pills only for specific and approved use cases, such as {largeExample} -### Tree variant for filters - -The `tree` variant for FormPillGroup changes the accessibility roles and DOM elements of the FormPill to be more tree-like where selecting the pill expands more options. This variant of Form Pills is only for specific and approved use cases, such as in the filter pattern (link coming soon!). - - - {treeVariantExample} - - ### Selectable Use a Selectable Form Pill to show an option that a user can select or deselect. @@ -206,6 +198,14 @@ The `onSelect` event handler will fire when a user presses the spacebar or enter {selectableAndDismissableExample} +### Tree variant for filters + +The `tree` variant for FormPillGroup changes the accessibility roles and DOM elements of the FormPill to be more tree-like where selecting the pill expands more options. This variant of Form Pills is only for specific and approved use cases, such as in the filter pattern (link coming soon!). + + + {treeVariantExample} + + ### 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. From 18cdf7ec1ac482b524cff79a7b8657e9fbcf1fb3 Mon Sep 17 00:00:00 2001 From: Kristian Antrobus Date: Tue, 13 Aug 2024 14:09:46 -0500 Subject: [PATCH 08/11] chore(docs): addressing pr comments --- .../components/form-pill-group/src/FormPillGroup.tsx | 6 ++++-- packages/paste-core/components/form-pill-group/src/types.ts | 2 +- .../components/form-pill-group/src/useFormPillState.tsx | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) 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 f305eaf214..217eead602 100644 --- a/packages/paste-core/components/form-pill-group/src/FormPillGroup.tsx +++ b/packages/paste-core/components/form-pill-group/src/FormPillGroup.tsx @@ -51,8 +51,10 @@ export interface FormPillGroupProps size?: FormPillGroupSizeVariant; /** * The variant of the FormPillGroup to use. The 'tree' option allows for more data to be displayed on select and still allows for select states. + * It changes the aria roles from listbox/option to tree/treeitem and underlying DOM elements form button to div so that the FormPill can be used to trigger other DOM elements such as a dialog. + * The existing keyboard functionality remains uneffected. * - * @default 'default' + * @default 'listbox' * @memberof FormPillGroupProps */ variant?: FormPillGroupUsageVariants; @@ -73,7 +75,7 @@ const SizeStyles: Record( - ({ element = "FORM_PILL_GROUP", display = "flex", size = "default", variant = "default", ...props }, ref) => { + ({ element = "FORM_PILL_GROUP", display = "flex", size = "default", variant = "listbox", ...props }, ref) => { return ( ; /** The size variants for the FormPillGroup component. */ export type FormPillGroupSizeVariant = "default" | "large"; -export type FormPillGroupUsageVariants = "default" | "tree"; +export type FormPillGroupUsageVariants = "listbox" | "tree"; diff --git a/packages/paste-core/components/form-pill-group/src/useFormPillState.tsx b/packages/paste-core/components/form-pill-group/src/useFormPillState.tsx index f34c8cc0ce..d3ab059c04 100644 --- a/packages/paste-core/components/form-pill-group/src/useFormPillState.tsx +++ b/packages/paste-core/components/form-pill-group/src/useFormPillState.tsx @@ -23,5 +23,5 @@ export interface FormPillGroupContextState { export const FormPillGroupContext = createContext({ size: "default", - variant: "default", + variant: "listbox", }); From 55ded59b5ed9656c752b41c94b762d64e2584e7c Mon Sep 17 00:00:00 2001 From: Kristian Antrobus Date: Tue, 13 Aug 2024 14:37:36 -0500 Subject: [PATCH 09/11] chore(docs): addressing pr comments --- .../paste-core/components/popover/stories/index.stories.tsx | 4 ++-- .../paste-website/src/component-examples/PopoverExamples.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/paste-core/components/popover/stories/index.stories.tsx b/packages/paste-core/components/popover/stories/index.stories.tsx index 1587d9ef0f..75ada97c83 100644 --- a/packages/paste-core/components/popover/stories/index.stories.tsx +++ b/packages/paste-core/components/popover/stories/index.stories.tsx @@ -252,7 +252,7 @@ export const FormPillPopover = (): JSX.Element => { setSelected(!selected); }} > - Select Form Pill + Toggle Form Pill selection @@ -270,7 +270,7 @@ export const FormPillPopover = (): JSX.Element => { setSelected(!selected); }} > - Select Form Pill + Toggle Form Pill selection diff --git a/packages/paste-website/src/component-examples/PopoverExamples.ts b/packages/paste-website/src/component-examples/PopoverExamples.ts index 1e0b68db2c..3038ecca2a 100644 --- a/packages/paste-website/src/component-examples/PopoverExamples.ts +++ b/packages/paste-website/src/component-examples/PopoverExamples.ts @@ -461,7 +461,7 @@ const PopoverFormPillExample = () => { setSelected(!selected); }} > - Select Form Pill + Toggle Form Pill selection From 8ddb8b4efa81236a4fa881d54348c343bbf999c4 Mon Sep 17 00:00:00 2001 From: Kristian Antrobus Date: Tue, 13 Aug 2024 14:50:08 -0500 Subject: [PATCH 10/11] chore(docs): typedocs --- .../paste-core/components/form-pill-group/type-docs.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/paste-core/components/form-pill-group/type-docs.json b/packages/paste-core/components/form-pill-group/type-docs.json index 74925cd975..98cf50948b 100644 --- a/packages/paste-core/components/form-pill-group/type-docs.json +++ b/packages/paste-core/components/form-pill-group/type-docs.json @@ -810,7 +810,7 @@ "description": "Indicates an element's \"grabbed\" state in a drag-and-drop operation." }, "aria-haspopup": { - "type": "| boolean\n | \"tree\"\n | \"true\"\n | \"false\"\n | \"dialog\"\n | \"grid\"\n | \"listbox\"\n | \"menu\"", + "type": "| boolean\n | \"listbox\"\n | \"tree\"\n | \"true\"\n | \"false\"\n | \"dialog\"\n | \"grid\"\n | \"menu\"", "defaultValue": null, "required": false, "externalProp": true, @@ -2328,10 +2328,10 @@ }, "variant": { "type": "FormPillGroupUsageVariants", - "defaultValue": "'default'", + "defaultValue": "'listbox'", "required": false, "externalProp": false, - "description": "The variant of the FormPillGroup to use. The 'tree' option allows for more data to be displayed on select and still allows for select states." + "description": "The variant of the FormPillGroup to use. The 'tree' option allows for more data to be displayed on select and still allows for select states.\nIt changes the aria roles from listbox/option to tree/treeitem and underlying DOM elements form button to div so that the FormPill can be used to trigger other DOM elements such as a dialog.\nThe existing keyboard functionality remains uneffected." }, "vocab": { "type": "string", From 86cc0b152debeb1420784385b2aa43b0e019867d Mon Sep 17 00:00:00 2001 From: Kristian Antrobus Date: Tue, 13 Aug 2024 16:37:24 -0500 Subject: [PATCH 11/11] chore(docs): mention coming soon links --- packages/paste-website/src/pages/components/popover/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/paste-website/src/pages/components/popover/index.mdx b/packages/paste-website/src/pages/components/popover/index.mdx index 0503ab337f..de0f97b156 100644 --- a/packages/paste-website/src/pages/components/popover/index.mdx +++ b/packages/paste-website/src/pages/components/popover/index.mdx @@ -310,7 +310,7 @@ To launch a Popover from a Badge component, use the PopoverBadgeButton component ### PopoverFormPillButton -This component should **only be used as part of the filter pattern**. It renders a FormPill and accepts all of its props, which are listed [on the Form Pill Group page](/components/form-pill-group#basic). +This component should **only be used as part of the filter pattern** (link coming soon!). It renders a FormPill and accepts all of its props, which are listed [on the Form Pill Group page](/components/form-pill-group#basic).