diff --git a/.changeset/quiet-rice-prove.md b/.changeset/quiet-rice-prove.md new file mode 100644 index 0000000000..e84003a249 --- /dev/null +++ b/.changeset/quiet-rice-prove.md @@ -0,0 +1,29 @@ +--- +"@salt-ds/lab": minor +--- + +Added `OverlayHeader` component to lab. + +```tsx + + + + + + + + + } + /> + Content of Overlay + + +``` diff --git a/packages/lab/src/index.ts b/packages/lab/src/index.ts index 27bf2aebe0..9dc6f304c3 100644 --- a/packages/lab/src/index.ts +++ b/packages/lab/src/index.ts @@ -51,6 +51,7 @@ export * from "./list-next"; export * from "./logo"; export * from "./menu-button"; export * from "./metric"; +export * from "./overlay"; export * from "./portal"; export * from "./query-input"; export * from "./responsive"; diff --git a/packages/lab/src/overlay/OverlayHeader.css b/packages/lab/src/overlay/OverlayHeader.css new file mode 100644 index 0000000000..2a74f8fd5b --- /dev/null +++ b/packages/lab/src/overlay/OverlayHeader.css @@ -0,0 +1,31 @@ +.saltOverlayHeader { + padding: var(--salt-spacing-100); + width: 100%; + align-items: center; + display: flex; + flex-direction: row; + justify-content: stretch; + gap: var(--salt-spacing-100); + box-sizing: border-box; +} + +.saltOverlayHeader-container { + flex-grow: 1; + margin: 0; + display: flex; + flex-direction: column; + gap: var(--salt-spacing-50); +} + +.saltOverlayHeader-header > .saltText { + margin: 0; +} + +.saltOverlayHeader-actionsContainer { + align-self: flex-start; +} + +/* Overrides */ +.saltOverlayHeader ~ .saltOverlayPanelContent { + padding-top: 0; +} diff --git a/packages/lab/src/overlay/OverlayHeader.tsx b/packages/lab/src/overlay/OverlayHeader.tsx new file mode 100644 index 0000000000..0375929a12 --- /dev/null +++ b/packages/lab/src/overlay/OverlayHeader.tsx @@ -0,0 +1,66 @@ +import { Text, 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 overlayHeaderCss from "./OverlayHeader.css"; + +const withBaseName = makePrefixer("saltOverlayHeader"); + +export interface OverlayHeaderProps extends ComponentPropsWithoutRef<"div"> { + /** + * Description text is displayed just below the header + **/ + description?: ReactNode; + /** + * Actions to be displayed in header + */ + actions?: ReactNode; + /** + * Header text + */ + header?: ReactNode; + /** + * Preheader text is displayed just above the header + **/ + preheader?: ReactNode; +} + +export const OverlayHeader = forwardRef( + function OverlayHeader(props, ref) { + const targetWindow = useWindow(); + useComponentCssInjection({ + testId: "salt-overlay-header", + css: overlayHeaderCss, + window: targetWindow, + }); + + const { className, description, header, actions, preheader, ...rest } = + props; + + return ( +
+
+
+ {preheader && ( + {preheader} + )} + {header} +
+ {description && ( + + {description} + + )} +
+ {actions && ( +
{actions}
+ )} +
+ ); + }, +); diff --git a/packages/lab/src/overlay/index.ts b/packages/lab/src/overlay/index.ts new file mode 100644 index 0000000000..f9c8c290a1 --- /dev/null +++ b/packages/lab/src/overlay/index.ts @@ -0,0 +1 @@ +export * from "./OverlayHeader"; diff --git a/packages/lab/stories/overlay/overlay.qa.stories.tsx b/packages/lab/stories/overlay/overlay.qa.stories.tsx new file mode 100644 index 0000000000..1c7b5a5a0c --- /dev/null +++ b/packages/lab/stories/overlay/overlay.qa.stories.tsx @@ -0,0 +1,73 @@ +import { + Button, + H4, + Overlay, + OverlayPanel, + OverlayPanelContent, + OverlayTrigger, +} from "@salt-ds/core"; +import { CloseIcon } from "@salt-ds/icons"; +import { OverlayHeader } from "@salt-ds/lab"; +import type { Meta, StoryFn } from "@storybook/react"; +import { QAContainer, type QAContainerProps } from "docs/components"; + +export default { + title: "Lab/Overlay Header/Overlay Header QA", + component: Overlay, +} as Meta; + +export const Default: StoryFn = (props) => { + const CloseButton = () => ( + + ); + return ( + + + + + + + Header block} + actions={} + /> + +
Content of Overlay
+
+
+
+
+ ); +}; + +Default.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/overlay/overlay.stories.tsx b/packages/lab/stories/overlay/overlay.stories.tsx new file mode 100644 index 0000000000..cf64078111 --- /dev/null +++ b/packages/lab/stories/overlay/overlay.stories.tsx @@ -0,0 +1,123 @@ +import { + Button, + H4, + Overlay, + OverlayPanel, + OverlayPanelContent, + type OverlayProps, + OverlayTrigger, + StackLayout, + Text, + Tooltip, + useId, +} from "@salt-ds/core"; +import { CloseIcon } from "@salt-ds/icons"; +import { OverlayHeader } from "@salt-ds/lab"; +import type { Meta } from "@storybook/react"; +import { useState } from "react"; + +export default { + title: "Lab/Overlay Header", +} as Meta; + +export const Header = ({ onOpenChange }: OverlayProps) => { + const [open, setOpen] = useState(false); + const id = useId(); + + const onChange = (newOpen: boolean) => { + setOpen(newOpen); + onOpenChange?.(newOpen); + }; + + return ( + + + + + + Header block} /> + + + + Content of Overlay. Lorem Ipsum is simply dummy text of the + printing and typesetting industry. Lorem Ipsum has been the + industry's standard dummy text ever since the 1500s. When an + unknown printer took a galley of type and scrambled it to make a + type specimen book. + +
+ + + +
+
+
+
+
+ ); +}; + +export const HeaderWithCloseButton = ({ onOpenChange }: OverlayProps) => { + const [open, setOpen] = useState(false); + const id = useId(); + + const onChange = (newOpen: boolean) => { + setOpen(newOpen); + onOpenChange?.(newOpen); + }; + + const handleClose = () => setOpen(false); + + const CloseButton = () => ( + + ); + + return ( + + + + + + Header block} + actions={} + /> + + + + Content of Overlay. Lorem Ipsum is simply dummy text of the + printing and typesetting industry. Lorem Ipsum has been the + industry's standard dummy text ever since the 1500s. When an + unknown printer took a galley of type and scrambled it to make a + type specimen book. + +
+ + + +
+
+
+
+
+ ); +}; diff --git a/site/docs/components/overlay/examples.mdx b/site/docs/components/overlay/examples.mdx index 69cb0a8ddd..baa2a3160e 100644 --- a/site/docs/components/overlay/examples.mdx +++ b/site/docs/components/overlay/examples.mdx @@ -48,5 +48,14 @@ When overlay content extends beyond the set height, the content defaults to scro With actions, users can decide if interacting with content in the overlay should close the overlay. This example shows how activating the **Export** button triggers the export function, closes the overlay, and returns focus to the trigger element. + + + ## 🚧 With header + + `OverlayHeader`'s update follows our standardized header for container components and app regions, and it can be added as a child of the `OverlayPanel` component to provide a structured header for overlay. The header includes a title and actions that follows our Header Block pattern. This approach is recommended over using the `OverlayPanelCloseButton` separately. + + **Note:** This change is currently in Lab. + + diff --git a/site/src/examples/overlay/WithHeader.tsx b/site/src/examples/overlay/WithHeader.tsx new file mode 100644 index 0000000000..4a3b409995 --- /dev/null +++ b/site/src/examples/overlay/WithHeader.tsx @@ -0,0 +1,46 @@ +import { + Button, + Overlay, + OverlayPanel, + OverlayPanelContent, + OverlayTrigger, + useId, +} from "@salt-ds/core"; +import { CloseIcon } from "@salt-ds/icons"; +import { OverlayHeader } from "@salt-ds/lab"; +import { type ReactElement, useState } from "react"; + +export const WithHeader = (): ReactElement => { + const [open, setOpen] = useState(false); + const id = useId(); + + const onOpenChange = (newOpen: boolean) => setOpen(newOpen); + + const handleClose = () => setOpen(false); + + const headerActions = ( + + ); + return ( + + + + + + Title} + actions={headerActions} + /> + +
Content of Overlay
+
+
+
+ ); +}; diff --git a/site/src/examples/overlay/index.ts b/site/src/examples/overlay/index.ts index 397ef08d1f..4cfd414188 100644 --- a/site/src/examples/overlay/index.ts +++ b/site/src/examples/overlay/index.ts @@ -2,4 +2,5 @@ export * from "./Default"; export * from "./Placement"; export * from "./LongContent"; export * from "./WithActions"; +export * from "./WithHeader"; export * from "./CloseButton";