Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(popover): add form pill button #4018

Merged
merged 13 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/five-points-change.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions .changeset/many-paws-walk.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions packages/paste-codemods/tools/.cache/mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -210,4 +210,31 @@ describe("FormPillGroup", () => {
expect(errorLabel).toBeDefined();
});
});

describe("tree variant", () => {
it("should have the correct role for tree variant", () => {
render(<FormPillTreeVariant />);

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(<FormPillTreeVariant />);

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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const FormPillButton = React.forwardRef<HTMLElement, FormPillStylesProps>
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 (
Expand All @@ -61,9 +61,9 @@ export const FormPillButton = React.forwardRef<HTMLElement, FormPillStylesProps>
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"
Expand All @@ -79,7 +79,7 @@ export const FormPillButton = React.forwardRef<HTMLElement, FormPillStylesProps>
transition="background-color 150ms ease-in, border-color 150ms ease-in, box-shadow 150ms ease-in, color 150ms ease-in"
{...computedStyles}
>
<Box display="flex" alignItems="center" columnGap="space20" opacity={isDisabled ? 0.3 : 1}>
<Box display="flex" height="100%" alignItems="center" columnGap="space20" opacity={isDisabled ? 0.3 : 1}>
{variant === "error" ? (
<>
<ErrorIcon decorative size={size === "large" ? "sizeIcon20" : "sizeIcon10"} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -49,6 +49,15 @@ 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.
krisantrobus marked this conversation as resolved.
Show resolved Hide resolved
* 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 'listbox'
* @memberof FormPillGroupProps
*/
variant?: FormPillGroupUsageVariants;
}

/**
Expand All @@ -66,14 +75,14 @@ const SizeStyles: Record<FormPillGroupSizeVariant, Pick<BoxProps, "columnGap" |
};

const FormPillGroupStyles = React.forwardRef<HTMLUListElement, FormPillGroupProps>(
({ element = "FORM_PILL_GROUP", display = "flex", size = "default", ...props }, ref) => {
({ element = "FORM_PILL_GROUP", display = "flex", size = "default", variant = "listbox", ...props }, ref) => {
return (
<FormPillGroupContext.Provider value={{ size }}>
<FormPillGroupContext.Provider value={{ size, variant }}>
<Box
{...safelySpreadBoxProps(props)}
element={element}
ref={ref}
role="listbox"
role={variant === "tree" ? "tree" : "listbox"}
lineHeight="lineHeight30"
margin="space0"
padding="space0"
Expand Down Expand Up @@ -108,10 +117,10 @@ export const FormPillGroup = React.forwardRef<HTMLUListElement, FormPillGroupPro
const keyboardControlsId = useUID();
return (
<>
<ScreenReaderOnly id={keyboardControlsId}>{i18nKeyboardControls}</ScreenReaderOnly>
<Composite as={FormPillGroupStyles} ref={ref} aria-describedby={keyboardControlsId} {...props}>
{props.children}
</Composite>
<ScreenReaderOnly id={keyboardControlsId}>{i18nKeyboardControls}</ScreenReaderOnly>
</>
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export type PillVariant = "error" | "default";
export type VariantStyles = Record<PillVariant, BoxStyleProps>;
/** The size variants for the FormPillGroup component. */
export type FormPillGroupSizeVariant = "default" | "large";
export type FormPillGroupUsageVariants = "listbox" | "tree";
Original file line number Diff line number Diff line change
Expand Up @@ -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<CompositeInitialState, "orientation" | "loop">;

Expand All @@ -18,8 +18,10 @@ export const useFormPillState = (config: FormPillInitialState = {}): CompositeSt

export interface FormPillGroupContextState {
size: FormPillGroupSizeVariant;
variant?: FormPillGroupUsageVariants;
}

export const FormPillGroupContext = createContext<FormPillGroupContextState>({
size: "default",
variant: "listbox",
});
Original file line number Diff line number Diff line change
Expand Up @@ -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<Set<string>>(new Set([PILL_NAMES[1], PILL_NAMES[4]]));
const pillState = useFormPillState();

return (
<form>
<FormPillGroup
{...pillState}
data-testid="form-pill-group"
aria-label="Selectable and dismissable pills:"
variant="tree"
>
{pills.map((pill, index) => (
<FormPill
key={pill}
data-testid={`form-pill-${index}`}
{...pillState}
selected={selectedSet.has(pill)}
variant={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 ? <Avatar size="sizeIcon10" name="avatar example" src="./avatars/avatar4.png" /> : null}
{index % 3 === 1 ? <CalendarIcon decorative size="sizeIcon10" /> : null}
{pill}
</FormPill>
))}
</FormPillGroup>
</form>
);
};

FormPillTreeVariant.storyName = "FormPillGroup Tree Variant";

// eslint-disable-next-line import/no-default-export
export default {
title: "Components/Form Pill Group",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 | \"listbox\"\n | \"tree\"\n | \"true\"\n | \"false\"\n | \"dialog\"\n | \"grid\"\n | \"menu\"",
"defaultValue": null,
"required": false,
"externalProp": true,
Expand Down Expand Up @@ -2326,6 +2326,13 @@
"required": false,
"externalProp": true
},
"variant": {
"type": "FormPillGroupUsageVariants",
"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.\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",
"defaultValue": null,
Expand Down
39 changes: 38 additions & 1 deletion packages/paste-core/components/popover/__tests__/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -140,6 +140,43 @@ describe("Popover", () => {
});
});

describe("PopoverFormPillButton", () => {
it("renders PopoverFormPillButton as a FormPill", () => {
render(
<Theme.Provider theme="default">
<FormPillPopover />
</Theme.Provider>,
);
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 () => {
render(
<Theme.Provider theme="default">
<FormPillPopover />
</Theme.Provider>,
);
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 (renderedPopoverControl) {
userEvent.click(renderedPopoverControl);
}
});
expect(renderedPopoverControl?.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(
Expand Down
2 changes: 2 additions & 0 deletions packages/paste-core/components/popover/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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<HTMLElement, PopoverFormPillButtonProps>(
({ children, element = "POPOVER_FORM_PILL", ...popoverButtonProps }, ref) => {
const popover = React.useContext(PopoverContext);

return (
<NonModalDialogDisclosurePrimitive
element={element}
{...(popover as any)}
{...popoverButtonProps}
as={FormPill}
ref={ref}
onSelect={(e: React.MouseEvent<HTMLButtonElement>) => {
// @ts-expect-error Property 'toggle' does not exist on type 'Partial<PopoverState>', 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}
</NonModalDialogDisclosurePrimitive>
);
},
);

PopoverFormPillButton.displayName = "PopoverFormPillButton";
export { PopoverFormPillButton };
3 changes: 2 additions & 1 deletion packages/paste-core/components/popover/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
13 changes: 13 additions & 0 deletions packages/paste-core/components/popover/src/types.ts
Original file line number Diff line number Diff line change
@@ -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<BadgeButtonProps, "onClick"> & {
Expand Down Expand Up @@ -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"];
};
Loading
Loading