diff --git a/.changeset/lemon-phones-rescue.md b/.changeset/lemon-phones-rescue.md new file mode 100644 index 00000000000..c96d4529290 --- /dev/null +++ b/.changeset/lemon-phones-rescue.md @@ -0,0 +1,12 @@ +--- +"@kaizen/components": minor +--- + +Update FilterMultiSelect ListBoxSection to avoid duplicate reading of sectionName as the accessible title. + +- Update sectionName to be optional if sectionHeader is provided + - This will solve the issue of sectionName and sectionHeader being read twice when they are the same +- Minor style change to ensure hide bullet lists as filtering +- Minor style changes to allow for default text styles for section headings with just text +- Add conditional check to render the sectionName only if provided +- Add tests to validate type accessible names are constructed as expected \ No newline at end of file diff --git a/packages/components/src/FilterMultiSelect/_docs/FilterMultiSelect.stories.tsx b/packages/components/src/FilterMultiSelect/_docs/FilterMultiSelect.stories.tsx index c9c557e0482..77f32d06efe 100644 --- a/packages/components/src/FilterMultiSelect/_docs/FilterMultiSelect.stories.tsx +++ b/packages/components/src/FilterMultiSelect/_docs/FilterMultiSelect.stories.tsx @@ -150,6 +150,7 @@ export const WithSections: StoryFn = () => { selectedKeys={selectedKeys} items={mockItems} label="Engineer" + isOpen={IS_CHROMATIC || undefined} trigger={(): JSX.Element => ( = () => {
Items: - - {JSON.stringify(mockItems, null, "\t")} - + {JSON.stringify(mockItems, null, "\t")}
) } +WithSections.parameters = { + chromatic: { disable: false }, +} export const TruncatedLabels: StoryFn = () => { const [selectedKeys, setSelectedKeys] = useState( @@ -606,7 +608,7 @@ export const Async: StoryFn = args => { } Async.decorators = [withQueryProvider] -export const WithSectionHeader: StoryFn = () => { +export const WithSectionHeaders: StoryFn = () => { const [selectedKeys, setSelectedKeys] = useState( new Set(["id-fe"]) ) @@ -620,6 +622,7 @@ export const WithSectionHeader: StoryFn = () => { selectedKeys={selectedKeys} items={mockItems} label="Engineer" + isOpen={IS_CHROMATIC || undefined} trigger={(): JSX.Element => ( = () => { unselectedItems, disabledItems, hasNoItems, - }): JSX.Element => ( - <> - {hasNoItems && ( - - No results found. - - )} - - {(item): JSX.Element => ( - + }): JSX.Element => + hasNoItems ? ( + + No results found. + + ) : ( + <> + {selectedItems.length > 0 && ( + + {(item): JSX.Element => ( + + )} + )} - - {unselectedItems.length > 0 && selectedItems.length > 0 && ( - - )} - - - {(item): JSX.Element => ( - + {unselectedItems.length > 0 && ( + + {(item): JSX.Element => ( + + )} + )} - - {disabledItems.length > 0 && - (selectedItems.length > 0 || - unselectedItems.length > 0) && ( - - )} - 0 && ( + - Results for these filters are hidden to protect - identities of individuals and small groups - - } - > - {(item): JSX.Element => ( - + {(item): JSX.Element => ( + + )} + )} - + + ) + } + + + + + + + )} + + + ) +} +WithSectionHeaders.parameters = { + chromatic: { disable: false }, +} + +export const WithSectionNotification: StoryFn< + typeof FilterMultiSelect +> = () => { + const [selectedKeys, setSelectedKeys] = useState( + new Set(["id-fe"]) + ) + + const handleSelectionChange = (keys: Selection): void => setSelectedKeys(keys) + + return ( + <> + ( + + )} + > + {(): JSX.Element => ( + <> + + + {({ + selectedItems, + unselectedItems, + disabledItems, + hasNoItems, + }): JSX.Element => ( + <> + {hasNoItems ? ( + + No results found. + + ) : ( + <> + {selectedItems.length > 0 && ( + + {(item): JSX.Element => ( + + )} + + )} + + {unselectedItems.length > 0 && ( + + {(item): JSX.Element => ( + + )} + + )} + + {disabledItems.length > 0 && ( + + Disabled items + + Results for these filters are hidden to protect + identities of individuals and small groups + + + } + > + {(item): JSX.Element => ( + + )} + + )} + + )} )} @@ -706,12 +822,9 @@ export const WithSectionHeader: StoryFn = () => { )} -
- Items: - - {JSON.stringify(mockItems, null, "\t")} - -
) } +WithSectionNotification.parameters = { + chromatic: { disable: false }, +} diff --git a/packages/components/src/FilterMultiSelect/subcomponents/ListBox/ListBox.module.scss b/packages/components/src/FilterMultiSelect/subcomponents/ListBox/ListBox.module.scss index e7277b3d9ea..eba898e545b 100644 --- a/packages/components/src/FilterMultiSelect/subcomponents/ListBox/ListBox.module.scss +++ b/packages/components/src/FilterMultiSelect/subcomponents/ListBox/ListBox.module.scss @@ -16,3 +16,8 @@ .hidden { display: none; } + +// this is a div but remove styles briefly flickering to a bullet list as the sections are removed +.noResultsWrapper { + list-style: none; +} diff --git a/packages/components/src/FilterMultiSelect/subcomponents/ListBox/ListBox.tsx b/packages/components/src/FilterMultiSelect/subcomponents/ListBox/ListBox.tsx index b9e120b17ea..60e2351f4a0 100644 --- a/packages/components/src/FilterMultiSelect/subcomponents/ListBox/ListBox.tsx +++ b/packages/components/src/FilterMultiSelect/subcomponents/ListBox/ListBox.tsx @@ -76,7 +76,7 @@ export const ListBox = ({ children }: ListBoxProps): JSX.Element => { if (hasNoItems) { return ( <> -
{children(itemsState)}
+
{children(itemsState)}
{/* This ul with the ref needs to exist otherwise it fatals */}
    diff --git a/packages/components/src/FilterMultiSelect/subcomponents/ListBoxSection/ListBoxSection.module.scss b/packages/components/src/FilterMultiSelect/subcomponents/ListBoxSection/ListBoxSection.module.scss index 77abe226818..f8dd21dc932 100644 --- a/packages/components/src/FilterMultiSelect/subcomponents/ListBoxSection/ListBoxSection.module.scss +++ b/packages/components/src/FilterMultiSelect/subcomponents/ListBoxSection/ListBoxSection.module.scss @@ -1,5 +1,18 @@ +@import "~@kaizen/design-tokens/sass/typography"; +@import "~@kaizen/design-tokens/sass/color"; +@import "~@kaizen/design-tokens/sass/spacing"; + .listBoxSection { display: grid; list-style: none; padding: 0; } + +.listBoxSectionHeader { + font-family: $typography-heading-6-font-family; + font-size: $typography-heading-6-font-size; + font-weight: $typography-heading-6-font-weight; + line-height: $typography-heading-6-line-height; + color: rgba($color-purple-800-rgb, 0.7); + margin: $spacing-6 0; +} diff --git a/packages/components/src/FilterMultiSelect/subcomponents/ListBoxSection/ListBoxSection.spec.tsx b/packages/components/src/FilterMultiSelect/subcomponents/ListBoxSection/ListBoxSection.spec.tsx new file mode 100644 index 00000000000..1c3c92f5644 --- /dev/null +++ b/packages/components/src/FilterMultiSelect/subcomponents/ListBoxSection/ListBoxSection.spec.tsx @@ -0,0 +1,50 @@ +import React from "react" +import { render } from "@testing-library/react" +import { ListBoxSection } from "./ListBoxSection" + +describe("", () => { + describe("sectionName only", () => { + it("will only have aria-label", () => { + const { getByRole } = render( + + {() => undefined} + + ) + const group = getByRole("group") + expect(group).toHaveAttribute("aria-label", "Test sectionName only") + expect(group).not.toHaveTextContent("Test sectionName only") + }) + }) + + describe("sectionHeader only", () => { + it("will have sectionHeader content", () => { + const { getByRole } = render( + + {() => undefined} + + ) + const group = getByRole("group", { name: "Test sectionHeader only" }) + expect(group).toBeInTheDocument() + expect(group).toHaveTextContent("Test sectionHeader only") + }) + }) + + describe("sectionHeader and sectionName", () => { + it("will have combined accessible name", () => { + const { getByRole } = render( + + {() => undefined} + + ) + const group = getByRole("group", { + name: "Hidden group name. sectionHeader name", + }) + expect(group).toBeInTheDocument() + expect(group).toHaveTextContent("Hidden group name. sectionHeader name") + }) + }) +}) diff --git a/packages/components/src/FilterMultiSelect/subcomponents/ListBoxSection/ListBoxSection.tsx b/packages/components/src/FilterMultiSelect/subcomponents/ListBoxSection/ListBoxSection.tsx index b4771db5472..3e14221bb83 100644 --- a/packages/components/src/FilterMultiSelect/subcomponents/ListBoxSection/ListBoxSection.tsx +++ b/packages/components/src/FilterMultiSelect/subcomponents/ListBoxSection/ListBoxSection.tsx @@ -3,41 +3,57 @@ import { v4 } from "uuid" import { VisuallyHidden } from "@kaizen/a11y" import { MultiSelectItem } from "../../types" import styles from "./ListBoxSection.module.scss" -export interface ListBoxSectionProps { - items: MultiSelectItem[] + +type SectionNameProps = { /** * Becomes an aria-label on the section, informing * unsighted users */ sectionName: string - /** If provided, will override the aria-label for this group */ - sectionHeader?: ReactNode - children: (item: MultiSelectItem) => React.ReactNode } +type SectionHeaderProps = { + /** + * Becomes an aria-label on the section, informing + * unsighted users + */ + sectionName?: string + /** + * Can be used for a visual title of the ListBoxSection or to provide addition information in a React node. + * If this is the same title as sectionName, you should only pass in a sectionHeader to avoid duplicate descriptions. + */ + sectionHeader: ReactNode +} + +export type ListBoxSectionProps = { + items: MultiSelectItem[] + children: (item: MultiSelectItem) => React.ReactNode +} & (SectionHeaderProps | SectionNameProps) + export const ListBoxSection = ({ items, - sectionName, children, - sectionHeader, + sectionName, + ...restProps }: ListBoxSectionProps): JSX.Element => { const [listSectionId] = useState(v4()) + const hasSectionHeader = "sectionHeader" in restProps return (
    • - {sectionHeader && ( + {hasSectionHeader && ( )} {/*