diff --git a/.changeset/added-static-list.md b/.changeset/added-static-list.md new file mode 100644 index 0000000000..de85847355 --- /dev/null +++ b/.changeset/added-static-list.md @@ -0,0 +1,20 @@ +--- +"@salt-ds/lab": minor +--- + +Added `StaticList`, `StaticListItem`, `StaticListItemContent` component to lab. + +```tsx + + + + New static list feature updates are available in lab + + + + + New static list feature updates are available in lab + + + +``` diff --git a/packages/lab/src/index.ts b/packages/lab/src/index.ts index 91cdff5c2f..0b511f8a3c 100644 --- a/packages/lab/src/index.ts +++ b/packages/lab/src/index.ts @@ -56,6 +56,7 @@ export * from "./responsive"; export * from "./search-input"; export * from "./skip-link"; export * from "./slider"; +export * from "./static-list"; export * from "./stepped-tracker"; export * from "./stepper-input"; export * from "./system-status"; diff --git a/packages/lab/src/static-list/StaticList.css b/packages/lab/src/static-list/StaticList.css new file mode 100644 index 0000000000..b9e25fcfd2 --- /dev/null +++ b/packages/lab/src/static-list/StaticList.css @@ -0,0 +1,3 @@ +.saltStaticList { + overflow-y: auto; +} diff --git a/packages/lab/src/static-list/StaticList.tsx b/packages/lab/src/static-list/StaticList.tsx new file mode 100644 index 0000000000..51d00bcaa2 --- /dev/null +++ b/packages/lab/src/static-list/StaticList.tsx @@ -0,0 +1,37 @@ +import { makePrefixer } from "@salt-ds/core"; +import { useComponentCssInjection } from "@salt-ds/styles"; +import { useWindow } from "@salt-ds/window"; +import { clsx } from "clsx"; +import { + type ComponentPropsWithoutRef, + type ReactNode, + forwardRef, +} from "react"; + +import staticListCss from "./StaticList.css"; + +const withBaseName = makePrefixer("saltStaticList"); + +export interface StaticListProps extends ComponentPropsWithoutRef<"ul"> { + /** + * The list items to be rendered within the StaticList. + */ + children: ReactNode; +} + +export const StaticList = forwardRef( + function StaticList({ children, className, ...rest }, ref) { + const targetWindow = useWindow(); + useComponentCssInjection({ + testId: "salt-static-list", + css: staticListCss, + window: targetWindow, + }); + + return ( +
    + {children} +
+ ); + }, +); diff --git a/packages/lab/src/static-list/StaticListItem.css b/packages/lab/src/static-list/StaticListItem.css new file mode 100644 index 0000000000..968a24ac8e --- /dev/null +++ b/packages/lab/src/static-list/StaticListItem.css @@ -0,0 +1,13 @@ +.saltStaticListItem { + list-style-type: none; + display: flex; + gap: var(--salt-spacing-100); + box-sizing: border-box; + padding: var(--salt-spacing-50) var(--salt-spacing-100); + min-height: calc(var(--salt-size-base) + var(--salt-spacing-100)); +} + +.saltStaticListItem > .saltIcon { + /* Workaround to ensure the icon and button are aligned.*/ + min-height: var(--salt-size-base); +} diff --git a/packages/lab/src/static-list/StaticListItem.tsx b/packages/lab/src/static-list/StaticListItem.tsx new file mode 100644 index 0000000000..b60c7e0f0b --- /dev/null +++ b/packages/lab/src/static-list/StaticListItem.tsx @@ -0,0 +1,30 @@ +import { makePrefixer } from "@salt-ds/core"; +import { useComponentCssInjection } from "@salt-ds/styles"; +import { useWindow } from "@salt-ds/window"; +import { clsx } from "clsx"; +import { type ComponentPropsWithoutRef, forwardRef } from "react"; + +import staticListItemCss from "./StaticListItem.css"; + +const withBaseName = makePrefixer("saltStaticListItem"); + +export interface StaticListItemProps extends ComponentPropsWithoutRef<"li"> {} + +export const StaticListItem = forwardRef( + function StaticListItem(props, ref) { + const { className, children, ...restProps } = props; + + const targetWindow = useWindow(); + useComponentCssInjection({ + testId: "salt-static-list-item", + css: staticListItemCss, + window: targetWindow, + }); + + return ( +
  • + {children} +
  • + ); + }, +); diff --git a/packages/lab/src/static-list/StaticListItemContent.css b/packages/lab/src/static-list/StaticListItemContent.css new file mode 100644 index 0000000000..23eea1703d --- /dev/null +++ b/packages/lab/src/static-list/StaticListItemContent.css @@ -0,0 +1,10 @@ +.saltStaticListItemContent { + flex: 1 0; + margin: var(--salt-spacing-75) 0; + color: var(--salt-content-primary-foreground); + font-size: var(--salt-text-fontSize); + font-weight: var(--salt-text-fontWeight); + font-family: var(--salt-text-fontFamily); + line-height: var(--salt-text-lineHeight); + letter-spacing: var(--salt-text-letterSpacing); +} diff --git a/packages/lab/src/static-list/StaticListItemContent.tsx b/packages/lab/src/static-list/StaticListItemContent.tsx new file mode 100644 index 0000000000..02c462bd02 --- /dev/null +++ b/packages/lab/src/static-list/StaticListItemContent.tsx @@ -0,0 +1,39 @@ +import { makePrefixer } from "@salt-ds/core"; +import { useComponentCssInjection } from "@salt-ds/styles"; +import { useWindow } from "@salt-ds/window"; +import { clsx } from "clsx"; +import { + type ComponentPropsWithoutRef, + type ForwardedRef, + type ReactNode, + forwardRef, +} from "react"; +import staticListItemContent from "./StaticListItemContent.css"; + +const withBaseName = makePrefixer("saltStaticListItemContent"); + +export interface StaticListItemContentProps + extends ComponentPropsWithoutRef<"div"> { + /** + * The content of Static List Item + */ + children?: ReactNode; +} + +export const StaticListItemContent = forwardRef< + HTMLDivElement, + StaticListItemContentProps +>(function StaticListItemContent({ children, className, ...restProps }, ref) { + const targetWindow = useWindow(); + useComponentCssInjection({ + testId: "salt-static-list-item-content", + css: staticListItemContent, + window: targetWindow, + }); + + return ( +
    + {children} +
    + ); +}); diff --git a/packages/lab/src/static-list/index.ts b/packages/lab/src/static-list/index.ts new file mode 100644 index 0000000000..410c1c4915 --- /dev/null +++ b/packages/lab/src/static-list/index.ts @@ -0,0 +1,3 @@ +export * from "./StaticList"; +export * from "./StaticListItem"; +export * from "./StaticListItemContent"; diff --git a/packages/lab/stories/assets/exampleData.ts b/packages/lab/stories/assets/exampleData.ts index 22951a4ccc..e8f6247ef8 100644 --- a/packages/lab/stories/assets/exampleData.ts +++ b/packages/lab/stories/assets/exampleData.ts @@ -158,3 +158,35 @@ export const objectOptionsExampleData: objectOptionType[] = [ { value: 30, text: "C Option", id: 3 }, { value: 40, text: "D Option", id: 4 }, ]; + +export const eventsData = [ + "Team meeting", + "Meeting with John", + "Optional meeting", + "External meeting", + "Team lunch", + "Training event", + "Coffee break", + "Conference", + "Meeting with Jane", +]; + +export type ListEvent = { + title: string; + time: string; + link?: string; +}; +export const complexEventsData = [ + { title: "Team meeting", time: "09:00 to 10:00", link: "#" }, + { + title: "Meeting with John", + time: "10:00 to 11:00", + link: "#", + }, + { + title: "Optional meeting", + time: "11:00 to 12:00", + link: "#", + }, + { title: "Team lunch", time: "12:00 to 13:00" }, +]; diff --git a/packages/lab/stories/static-list/static-list.qa.stories.tsx b/packages/lab/stories/static-list/static-list.qa.stories.tsx new file mode 100644 index 0000000000..bb74e179f7 --- /dev/null +++ b/packages/lab/stories/static-list/static-list.qa.stories.tsx @@ -0,0 +1,148 @@ +import { Button, Divider, StackLayout, Text, useId } from "@salt-ds/core"; +import { CalendarIcon, OverflowMenuIcon, VideoIcon } from "@salt-ds/icons"; +import { + StaticList, + StaticListItem, + StaticListItemContent, +} from "@salt-ds/lab"; +import type { Meta, StoryFn } from "@storybook/react"; +import { clsx } from "clsx"; +import { QAContainer, type QAContainerProps } from "docs/components"; +import React, { Fragment } from "react"; +import { complexEventsData, eventsData } from "../assets/exampleData"; + +export default { + title: "Lab/Static List/Static List QA", + component: StaticList, +} as Meta; + +export const AllExamples: StoryFn = () => ( + + + + Team meeting + + + + {eventsData.slice(0, 3).map((event, _index) => ( + + + {event} + + + ))} + + + {complexEventsData.map(({ title, time }) => ( + + + + {title} + + {time} + + + + + ))} + + + {complexEventsData.map(({ title, time }) => { + const id = useId(); + return ( + + + + + {title} + + + {time} + + + + + + + ); + })} + + + {complexEventsData.map(({ title, time }) => ( + + + + + {title} + + {time} + + + + + ))} + + + {complexEventsData.map(({ title, time }, _index) => ( + + + + + {title} + + {time} + + + + + {_index < complexEventsData.length - 1 && ( + + )} + + ))} + + +); + +AllExamples.parameters = { + chromatic: { + disableSnapshot: false, + modes: { + theme: { + themeNext: "disable", + }, + themeNext: { + themeNext: "enable", + corner: "rounded", + accent: "teal", + // Ignore headingFont given font is not loaded + }, + }, + }, +}; diff --git a/packages/lab/stories/static-list/static-list.stories.tsx b/packages/lab/stories/static-list/static-list.stories.tsx new file mode 100644 index 0000000000..b22c9011bd --- /dev/null +++ b/packages/lab/stories/static-list/static-list.stories.tsx @@ -0,0 +1,163 @@ +import { Button, Divider, StackLayout, Text, useId } from "@salt-ds/core"; +import { CalendarIcon, OverflowMenuIcon, VideoIcon } from "@salt-ds/icons"; +import { + StaticList, + StaticListItem, + StaticListItemContent, + type StaticListProps, +} from "@salt-ds/lab"; +import type { Meta, StoryFn } from "@storybook/react"; +import { clsx } from "clsx"; +import React, { Fragment, useState } from "react"; +import { complexEventsData, eventsData } from "../assets/exampleData"; + +export default { + title: "Lab/Static List", + component: StaticList, +} as Meta; + +export const Default: StoryFn = () => { + const [listCount, setListCount] = useState(3); + + const handleListItem = () => { + setListCount((prev) => prev + 1); + }; + const handleReset = () => { + setListCount(3); + }; + return ( + + + + + + + {eventsData.slice(0, listCount).map((event, _index) => ( + + + {event} + + + ))} + + + ); +}; + +export const ComplexLabel: StoryFn = () => { + return ( + + {complexEventsData.map(({ title, time }) => ( + + + + {title} + + {time} + + + + + ))} + + ); +}; + +export const WithIcons: StoryFn = () => { + return ( + + {complexEventsData.map(({ title, time }) => ( + + + + + {title} + + {time} + + + + + ))} + + ); +}; + +export const WithButtons: StoryFn = () => ( + + {complexEventsData.map(({ title, time }) => { + const id = useId(); + return ( + + + + + {title} + + + {time} + + + + + + + ); + })} + +); + +export const WithDividers: StoryFn = () => { + return ( + + {complexEventsData.map(({ title, time }, _index) => ( + + + + + {title} + + {time} + + + + + {_index < complexEventsData.length - 1 && ( + + )} + + ))} + + ); +}; diff --git a/site/docs/components/static-list/accessibility.mdx b/site/docs/components/static-list/accessibility.mdx new file mode 100644 index 0000000000..30ecb0de36 --- /dev/null +++ b/site/docs/components/static-list/accessibility.mdx @@ -0,0 +1,23 @@ +--- +title: + $ref: ./#/title +layout: DetailComponent +sidebar: + exclude: true +data: + $ref: ./#/data +--- + +## Best practices + +You must check and test any focusable elements inside the static list content accordingly. + +## Keyboard interactions + + + + +This action navigates through the interactive elements within the static list. + + + diff --git a/site/docs/components/static-list/examples.mdx b/site/docs/components/static-list/examples.mdx new file mode 100644 index 0000000000..04308d4e7f --- /dev/null +++ b/site/docs/components/static-list/examples.mdx @@ -0,0 +1,62 @@ +--- +title: + $ref: ./#/title +layout: DetailComponent +sidebar: + exclude: true +data: + $ref: ./#/data +--- + + + + + +## Default + +The default `Static list` allows users to show non-interactive list items. + + + + + +## Complex label + +For complex labels, you can add additional elements to provide further context or descriptions for each list item. + +**Note:** To ensure correct spacing when using buttons or icons, wrap your static content inside `StaticListItemContent`. + + + + + +## With icons + +[`Icons`](/salt/components/icon) can be used to help users quickly identify the related contents inside the `StaticListItem`. + +### Best practices + +- Icons should be displayed before labels. +- Use icons consistently across all static list items within the same group. + + + + + +## With buttons + +You can use [`Button`](/salt/components/button) to add actions to the `StaticListItem`s. + +**Note:** Buttons should be associated to the list item content using `aria-label` or `aria-labelledby`. + + + + + +## With dividers + +You can separate the list of `StaticListItem`s by adding a [`Divider`](../divider) component between items. + + + + diff --git a/site/docs/components/static-list/index.mdx b/site/docs/components/static-list/index.mdx new file mode 100644 index 0000000000..b8e1ed02a2 --- /dev/null +++ b/site/docs/components/static-list/index.mdx @@ -0,0 +1,13 @@ +--- +title: Static list +data: + description: "`StaticList` manages the layout of non-interactive list items, providing a structured and composable way to display content. It's ideal for use in patterns like file upload or checkout baskets." + sourceCodeUrl: "https://github.com/jpmorganchase/salt-ds/blob/main/packages/lab/src/static-list" + package: + name: "@salt-ds/lab" + initialVersion: "1.0.0-alpha.48" + alsoKnownAs: ["List"] + relatedComponents: [{ name: "Listbox", relationship: "similarTo" }] + +layout: DetailComponent +--- diff --git a/site/docs/components/static-list/usage.mdx b/site/docs/components/static-list/usage.mdx new file mode 100644 index 0000000000..c80f6428ad --- /dev/null +++ b/site/docs/components/static-list/usage.mdx @@ -0,0 +1,33 @@ +--- +title: + $ref: ./#/title +layout: DetailComponent +sidebar: + exclude: true +data: + $ref: ./#/data +--- + +## Using the component + +### When to use + +- To present a set of static list items. It can be used with a [`FileDropZone`](/salt/components/file-drop-zone) to show the list of files added. It can also be used to create a shopping cart list. + +### When not to use + +- If you need to create an interactive list, use the [`ListBox`](../list-box) component. + +## Content + +- Always use sentence case for static list item labels. +- Keep static list item labels short and concise. +- Keep static list secondary item labels short and concise. + +## Import + +To import `StaticList` and related components from the lab Salt package, use: + +``` +import { StaticList, StaticListItem, StaticListItemContent } from "@salt-ds/lab"; +``` diff --git a/site/src/examples/static-list/ComplexLabel.tsx b/site/src/examples/static-list/ComplexLabel.tsx new file mode 100644 index 0000000000..e26764214e --- /dev/null +++ b/site/src/examples/static-list/ComplexLabel.tsx @@ -0,0 +1,31 @@ +import { StackLayout, Text } from "@salt-ds/core"; +import { + StaticList, + StaticListItem, + StaticListItemContent, +} from "@salt-ds/lab"; +import React, { type ReactElement } from "react"; +import { type ListEvent, complexEventsData } from "./exampleData"; + +const ListItem = ({ title, time }: ListEvent) => ( + + + + {title} + + {time} + + + + +); + +export const ComplexLabel = (): ReactElement => ( +
    + + {complexEventsData.map((event) => ( + + ))} + +
    +); diff --git a/site/src/examples/static-list/Default.tsx b/site/src/examples/static-list/Default.tsx new file mode 100644 index 0000000000..24cccc0a7e --- /dev/null +++ b/site/src/examples/static-list/Default.tsx @@ -0,0 +1,39 @@ +import { Button, StackLayout, Text } from "@salt-ds/core"; +import { StaticList, StaticListItem } from "@salt-ds/lab"; +import React, { type ReactElement, useState } from "react"; +import { eventsData } from "./exampleData"; + +const ListItem = ({ event }: { event: string }) => ( + + {event} + +); + +export const Default = (): ReactElement => { + const [listCount, setListCount] = useState(3); + + const handleListItem = () => { + setListCount((prev) => prev + 1); + }; + const handleReset = () => { + setListCount(3); + }; + return ( + + + + + + + {eventsData.slice(0, listCount).map((event, _index) => ( + + ))} + + + ); +}; diff --git a/site/src/examples/static-list/WithButtons.tsx b/site/src/examples/static-list/WithButtons.tsx new file mode 100644 index 0000000000..6d055ce3ee --- /dev/null +++ b/site/src/examples/static-list/WithButtons.tsx @@ -0,0 +1,69 @@ +import { Button, StackLayout, Text, useId } from "@salt-ds/core"; +import { + EditIcon, + NoteIcon, + OverflowMenuIcon, + TearOutIcon, + VideoIcon, +} from "@salt-ds/icons"; +import { + StaticList, + StaticListItem, + StaticListItemContent, +} from "@salt-ds/lab"; +import { clsx } from "clsx"; +import React, { type ReactElement } from "react"; +import { type ListEvent, complexEventsData } from "./exampleData"; + +const ListItem = ({ title, time }: ListEvent) => { + const id = useId(); + + return ( + + + + + {title} + + + {time} + + + + + + + ); +}; + +export const WithButtons = (): ReactElement => ( +
    + + {complexEventsData.map((event) => ( + + ))} + +
    +); diff --git a/site/src/examples/static-list/WithDividers.tsx b/site/src/examples/static-list/WithDividers.tsx new file mode 100644 index 0000000000..9315f83dc1 --- /dev/null +++ b/site/src/examples/static-list/WithDividers.tsx @@ -0,0 +1,36 @@ +import { Divider, StackLayout, Text } from "@salt-ds/core"; +import { + StaticList, + StaticListItem, + StaticListItemContent, +} from "@salt-ds/lab"; +import React, { Fragment, type ReactElement } from "react"; +import { type ListEvent, complexEventsData } from "./exampleData"; + +const ListItem = ({ title, time }: ListEvent) => ( + + + + {title} + + {time} + + + + +); + +export const WithDividers = (): ReactElement => ( +
    + + {complexEventsData.map((event, _index) => ( + + + {_index < complexEventsData.length - 1 && ( + + )} + + ))} + +
    +); diff --git a/site/src/examples/static-list/WithIcons.tsx b/site/src/examples/static-list/WithIcons.tsx new file mode 100644 index 0000000000..f4a96432e2 --- /dev/null +++ b/site/src/examples/static-list/WithIcons.tsx @@ -0,0 +1,33 @@ +import { StackLayout, Text } from "@salt-ds/core"; +import { CalendarIcon, NotificationIcon } from "@salt-ds/icons"; +import { + StaticList, + StaticListItem, + StaticListItemContent, +} from "@salt-ds/lab"; +import React, { type ReactElement } from "react"; +import { type ListEvent, complexEventsData } from "./exampleData"; + +const ListItem = ({ title, time }: ListEvent) => ( + + + + + {title} + + {time} + + + + +); + +export const WithIcons = (): ReactElement => ( +
    + + {complexEventsData.map((event) => ( + + ))} + +
    +); diff --git a/site/src/examples/static-list/exampleData.ts b/site/src/examples/static-list/exampleData.ts new file mode 100644 index 0000000000..98bc4f474b --- /dev/null +++ b/site/src/examples/static-list/exampleData.ts @@ -0,0 +1,31 @@ +export const eventsData = [ + "Team meeting", + "Meeting with John", + "Optional meeting", + "External meeting", + "Team lunch", + "Training event", + "Coffee break", + "Conference", + "Meeting with Jane", +]; + +export type ListEvent = { + title: string; + time: string; + link?: string; +}; +export const complexEventsData = [ + { title: "Team meeting", time: "09:00 to 10:00", link: "#" }, + { + title: "Meeting with John", + time: "10:00 to 11:00", + link: "#", + }, + { + title: "Optional meeting", + time: "11:00 to 12:00", + link: "#", + }, + { title: "Team lunch", time: "12:00 to 13:00" }, +]; diff --git a/site/src/examples/static-list/index.ts b/site/src/examples/static-list/index.ts new file mode 100644 index 0000000000..8b46801be4 --- /dev/null +++ b/site/src/examples/static-list/index.ts @@ -0,0 +1,5 @@ +export * from "./WithDividers"; +export * from "./WithIcons"; +export * from "./Default"; +export * from "./WithButtons"; +export * from "./ComplexLabel";