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

New header for overlay #4282

Merged
merged 19 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
29 changes: 29 additions & 0 deletions .changeset/quiet-rice-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
"@salt-ds/lab": minor
---

Added `OverlayHeader` component to lab.

```tsx
<Overlay {...args}>
<OverlayTrigger>
<Button>Show Overlay</Button>
</OverlayTrigger>
<OverlayPanel aria-labelledby={id}>
<OverlayHeader
id={id}
header="Title"
actions={
<Button
aria-label="Close overlay"
appearance="transparent"
sentiment="neutral"
>
<CloseIcon aria-hidden />
</Button>
}
/>
<OverlayPanelContent>Content of Overlay</OverlayPanelContent>
</OverlayPanel>
</Overlay>
```
1 change: 1 addition & 0 deletions packages/lab/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
31 changes: 31 additions & 0 deletions packages/lab/src/overlay/OverlayHeader.css
Original file line number Diff line number Diff line change
@@ -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;
}
66 changes: 66 additions & 0 deletions packages/lab/src/overlay/OverlayHeader.tsx
Original file line number Diff line number Diff line change
@@ -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;
joshwooding marked this conversation as resolved.
Show resolved Hide resolved
/**
* Header text
*/
header?: ReactNode;
/**
* Preheader text is displayed just above the header
**/
preheader?: ReactNode;
}

export const OverlayHeader = forwardRef<HTMLDivElement, OverlayHeaderProps>(
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 (
<div className={clsx(withBaseName(), className)} {...rest} ref={ref}>
<div className={withBaseName("container")}>
<div className={withBaseName("header")}>
{preheader && (
<Text className={withBaseName("preheader")}>{preheader}</Text>
)}
{header}
</div>
{description && (
<Text color="secondary" className={withBaseName("description")}>
{description}
</Text>
)}
</div>
{actions && (
<div className={withBaseName("actionsContainer")}>{actions}</div>
)}
</div>
);
},
);
1 change: 1 addition & 0 deletions packages/lab/src/overlay/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./OverlayHeader";
73 changes: 73 additions & 0 deletions packages/lab/stories/overlay/overlay.qa.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof Overlay>;

export const Default: StoryFn<QAContainerProps> = (props) => {
Fercas123 marked this conversation as resolved.
Show resolved Hide resolved
const CloseButton = () => (
<Button
aria-label="Close overlay"
appearance="transparent"
sentiment="neutral"
>
<CloseIcon aria-hidden />
</Button>
);
return (
<QAContainer
height={800}
cols={5}
itemPadding={50}
itemWidthAuto
width={1200}
{...props}
>
<Overlay open>
<OverlayTrigger>
<Button>Show Overlay</Button>
</OverlayTrigger>
<OverlayPanel>
<OverlayHeader
preheader="Preheader"
description="Description"
header={<H4>Header block</H4>}
actions={<CloseButton />}
/>
<OverlayPanelContent>
<div>Content of Overlay</div>
</OverlayPanelContent>
</OverlayPanel>
</Overlay>
</QAContainer>
);
};

Default.parameters = {
chromatic: {
disableSnapshot: false,
modes: {
theme: {
themeNext: "disable",
},
themeNext: {
themeNext: "enable",
corner: "rounded",
accent: "teal",
// Ignore headingFont given font is not loaded
},
},
},
};
123 changes: 123 additions & 0 deletions packages/lab/stories/overlay/overlay.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof Overlay>;

export const Header = ({ onOpenChange }: OverlayProps) => {
const [open, setOpen] = useState(false);
const id = useId();

const onChange = (newOpen: boolean) => {
setOpen(newOpen);
onOpenChange?.(newOpen);
};

return (
<Overlay open={open} onOpenChange={onChange}>
<OverlayTrigger>
<Button>Show Overlay</Button>
</OverlayTrigger>
<OverlayPanel
aria-labelledby={id}
style={{
width: 500,
}}
>
<OverlayHeader header={<H4 id={id}>Header block</H4>} />
<OverlayPanelContent>
<StackLayout gap={1}>
<Text>
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.
</Text>
<div>
<Tooltip content={"I'm a tooltip"}>
<Button>hover me</Button>
</Tooltip>
</div>
</StackLayout>
</OverlayPanelContent>
</OverlayPanel>
</Overlay>
);
};

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 = () => (
<Button
aria-label="Close overlay"
appearance="transparent"
sentiment="neutral"
onClick={handleClose}
>
<CloseIcon aria-hidden />
</Button>
);

return (
<Overlay open={open} onOpenChange={onChange}>
<OverlayTrigger>
<Button>Show Overlay</Button>
</OverlayTrigger>
<OverlayPanel
aria-labelledby={id}
style={{
width: 500,
}}
>
<OverlayHeader
preheader="Preheader"
description="Description"
header={<H4 id={id}>Header block</H4>}
actions={<CloseButton />}
/>
<OverlayPanelContent>
<StackLayout gap={1}>
<Text>
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.
</Text>
<div>
<Tooltip content={"I'm a tooltip"}>
<Button>hover me</Button>
</Tooltip>
</div>
</StackLayout>
</OverlayPanelContent>
</OverlayPanel>
</Overlay>
);
};
9 changes: 9 additions & 0 deletions site/docs/components/overlay/examples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

</LivePreview>
<LivePreview componentName="overlay" exampleName="WithHeader">

## 🚧 With header
Fercas123 marked this conversation as resolved.
Show resolved Hide resolved

`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.

</LivePreview>

</LivePreviewControls>
46 changes: 46 additions & 0 deletions site/src/examples/overlay/WithHeader.tsx
Original file line number Diff line number Diff line change
@@ -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 = (
<Button
aria-label="Close overlay"
appearance="transparent"
onClick={handleClose}
>
<CloseIcon aria-hidden />
</Button>
);
return (
<Overlay placement="right" open={open} onOpenChange={onOpenChange}>
<OverlayTrigger>
<Button>Show Overlay</Button>
</OverlayTrigger>
<OverlayPanel aria-labelledby={id}>
<OverlayHeader
header={<h4 id={id}>Title</h4>}
actions={headerActions}
/>
<OverlayPanelContent>
<div>Content of Overlay</div>
</OverlayPanelContent>
</OverlayPanel>
</Overlay>
);
};
Loading
Loading