Skip to content
Open
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
233 changes: 151 additions & 82 deletions skills/carbon-react/components/sidebar.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from "react";
import { PaddingProps } from "styled-system";
import Typography from "../../../typography";
import StyledSidebarHeader, {
StyledSidebarHeaderDivider,
StyledSidebarSubHeader,
} from "./sidebar-header.style";

Expand All @@ -9,10 +11,12 @@ export interface SidebarHeaderProps extends PaddingProps {
children?: React.ReactNode;
/** A custom id. */
id: string;
/** Close icon button to be rendered */
closeIcon?: React.ReactNode;
/** Close button to be rendered. */
closeButton?: React.ReactNode;
/** Header background variant for the sidebar. */
headerVariant?: "light" | "dark";
headerVariant?: "typical" | "inverse" | "light" | "dark";
/** Adds the Carbon AI gradient keyline to the header. */
gradientKeyLine?: boolean;
}

export interface SidebarSubHeaderProps extends PaddingProps {
Expand All @@ -25,32 +29,45 @@ export interface SidebarSubHeaderProps extends PaddingProps {
const SidebarHeader = ({
children,
id,
closeIcon,
closeButton,
headerVariant,
gradientKeyLine,
...rest
}: SidebarHeaderProps) => (
<StyledSidebarHeader
hasClose={!!closeIcon}
$hasCloseButton={!!closeButton}
data-component="sidebar-header"
data-role="sidebar-header"
p="27px 32px 32px"
headerVariant={headerVariant}
$headerVariant={headerVariant}
$gradientKeyLine={gradientKeyLine}
{...rest}
>
<div data-element="sidebar-heading" id={id}>
{children}
{typeof children === "string" ? (
<Typography
as="h1"
data-element="sidebar-title"
variant="h2"
wordBreak="normal"
wordWrap="break-word"
>
{children}
</Typography>
) : (
children
)}
</div>
{closeIcon}
{closeButton}
<StyledSidebarHeaderDivider
aria-hidden="true"
data-element="sidebar-header-divider"
$gradientKeyLine={gradientKeyLine}
/>
</StyledSidebarHeader>
);

const SidebarSubHeader = ({ children, id, ...rest }: SidebarSubHeaderProps) => (
<StyledSidebarSubHeader
data-component="sidebar-subheader"
p="var(--sizing100) var(--sizing400)"
id={id}
{...rest}
>
<StyledSidebarSubHeader data-component="sidebar-subheader" id={id} {...rest}>
{children}
</StyledSidebarSubHeader>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,49 +5,108 @@ import StyledIconButton from "../../../icon-button/icon-button.style";
import StyledIcon from "../../../icon/icon.style";

const StyledSidebarHeader = styled.div.attrs(applyBaseTheme)<{
hasClose?: boolean;
headerVariant?: "light" | "dark";
$hasCloseButton?: boolean;
$headerVariant?: "typical" | "inverse" | "light" | "dark";
$gradientKeyLine?: boolean;
}>`
background-color: ${({ headerVariant }) =>
headerVariant === "light"
? "var(--colorsUtilityYang100)"
: "var(--colorsUtilityYin100)"};
box-shadow: inset 0 -1px 0 0 var(--colorsUtilityMajor100);
${({ $headerVariant }) => {
const inverse = $headerVariant === "inverse" || $headerVariant === "dark";

return css`
background-color: ${inverse

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: you could do the ternary inside a template literal like so

var(--container-standard-${inverse ? "inverse" : ""}-bg-default)

or something alone those lines to save a little bit of lines here, but what you have is fine and completely acceptable

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion as literally written looks broken) Consider inverse = false => var(--container-standard**--**bg-default). The hyphens catch an eye and I think we accidentally can invite a morse code into our CSS)
If you insist on change, I can introduce const variant suffix = inverse ? "-inverse" : "". Do you think it worth it?

? "var(--container-standard-inverse-bg-default)"
: "var(--container-standard-bg-default)"};
color: ${inverse
? "var(--container-standard-inverse-txt-default)"
: "var(--container-standard-txt-default)"};
position: relative;
`;
}}
box-sizing: border-box;
flex: 0 0 auto;
min-height: calc((2 * var(--global-space-comp-xl)) + var(--global-size-s));
width: 100%;
color: var(--colorsActionMinorYin090);
transition: all 0.2s ease;
padding: var(--global-space-comp-xl);
${padding}
${({ hasClose, headerVariant }) =>
hasClose &&
${({ $hasCloseButton, $headerVariant }) =>
$hasCloseButton &&
css`
display: flex;
justify-content: space-between;
gap: var(--spacing200);
gap: var(--global-space-comp-l);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: should this gap be the same for all sizes, or should we not be using the component size to dictate this gap?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment applies to other fixed size usages in the PR 👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me double-check

> ${StyledIconButton}:first-of-type {
${headerVariant === "dark" &&
${($headerVariant === "inverse" || $headerVariant === "dark") &&
css`
${StyledIcon} {
color: var(--colorsUtilityYang080);
color: var(--container-standard-inverse-txt-default);
}

&:is(:hover, :active, :focus) ${StyledIcon} {
color: var(--container-standard-inverse-txt-default);
}

&:disabled ${StyledIcon} {
color: var(--container-standard-inverse-txt-alt);
}
`}
align-self: flex-start;
border-radius: var(--global-radius-action-circle);
flex: 0 0 auto;
height: var(--global-size-s);
min-width: var(--global-size-s);
width: var(--global-size-s);

${StyledIcon} {
height: var(--global-size-2-xs);
width: var(--global-size-2-xs);
}
}
`}
div[data-element="sidebar-heading"] {
display: flex;
flex: 1 1 auto;
flex-direction: column;
gap: var(--global-space-comp-xs);
min-width: 0;
width: 100%;

[data-element="sidebar-title"] {
color: inherit;
}
}
`;

const StyledSidebarHeaderDivider = styled.hr.attrs(applyBaseTheme)<{
$gradientKeyLine?: boolean;
}>`
background: ${({ $gradientKeyLine }) =>
$gradientKeyLine
? "var(--container-standard-border-ai-h)"
: "var(--container-standard-border-default)"};
border: var(--global-borderwidth-none);
bottom: 0;
height: ${({ $gradientKeyLine }) =>
$gradientKeyLine
? "var(--global-borderwidth-s)"
: "var(--global-borderwidth-xs)"};
left: 0;
margin: 0;
position: absolute;
right: 0;
`;

const StyledSidebarSubHeader = styled.div.attrs(applyBaseTheme)`
box-sizing: border-box;
width: 100%;
color: var(--colorsActionMinorYin090);
background-color: var(--colorsUtilityMajor050);
border-bottom: 1px solid var(--colorsUtilityMajor075);
color: var(--container-standard-txt-default);
background-color: var(--container-standard-bg-alt);
border-bottom: var(--global-borderwidth-xs) solid
var(--container-standard-border-default);
padding: var(--global-space-comp-s) var(--global-space-comp-2-xl);
${padding}
transition: all 0.2s ease;
`;

export default StyledSidebarHeader;
export { StyledSidebarSubHeader };
export { StyledSidebarHeaderDivider, StyledSidebarSubHeader };
79 changes: 38 additions & 41 deletions src/components/sidebar/components.test-pw.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import React, { useState, useRef } from "react";

import Typography from "../../../src/components/typography";
import Typography from "../typography";
import Button from "../button";
import Sidebar, { SidebarProps } from ".";
import Box from "../box";
import Form from "../form";
import Toast from "../toast";
import Textbox from "../textbox";

export const Default = ({
export const ControlledSidebar = ({
open = true,
restoreFocusOnClose,
onCancel: onCancelProp,
}: {
open?: boolean;
restoreFocusOnClose?: boolean;
...props
}: Partial<SidebarProps> & {
onCancel?: () => void;
}) => {
const [isOpen, setIsOpen] = useState(open);
Expand All @@ -29,6 +29,7 @@ export const Default = ({
open={isOpen}
onCancel={handleCancel}
restoreFocusOnClose={restoreFocusOnClose}
{...props}
>
<Box mb={2}>
<Button buttonType="primary">Test</Button>
Expand All @@ -42,7 +43,7 @@ export const Default = ({
);
};

export const DefaultNested = () => {
export const NestedSidebars = () => {
const [isFirstSidebarOpen, setIsFirstSidebarOpen] = useState(false);
const [isNestedSidebarOpen, setIsNestedSidebarOpen] = useState(false);
return (
Expand Down Expand Up @@ -73,34 +74,7 @@ export const DefaultNested = () => {
);
};

export const SidebarComponentWithOnCancel = (props: Partial<SidebarProps>) => {
const [isOpen, setIsOpen] = useState(true);
const handleOnCancel = () => {
setIsOpen(false);
};
return (
<>
<Sidebar
aria-label="sidebar"
open={isOpen}
position="right"
size="medium"
onCancel={handleOnCancel}
{...props}
>
<Box mb={2}>
<Button buttonType="primary">Test</Button>
<Button buttonType="secondary" ml={2}>
Last
</Button>
</Box>
<Box mb="3000px">Main content</Box>
</Sidebar>
</>
);
};

export const SidebarBackgroundScrollTestComponent = () => {
export const SidebarWithBackgroundScrollTarget = () => {
const [value, setValue] = useState("");

return (
Expand All @@ -126,7 +100,7 @@ export const SidebarBackgroundScrollTestComponent = () => {
);
};

export const SidebarBackgroundScrollWithOtherFocusableContainers = () => {
export const SidebarWithBackgroundScrollTargetAndFocusableContainers = () => {
const toast1Ref = useRef(null);
const toast2Ref = useRef(null);
const [value, setValue] = useState("");
Expand Down Expand Up @@ -164,19 +138,19 @@ export const SidebarBackgroundScrollWithOtherFocusableContainers = () => {
);
};

export const SidebarComponentFocusable = (props: Partial<SidebarProps>) => {
const [setIsDialogOpen] = React.useState(false);
const [isToastOpen, setIsToastOpen] = React.useState(false);
const toastRef = React.useRef(null);
export const SidebarWithFocusableContainer = (props: Partial<SidebarProps>) => {
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [isToastOpen, setIsToastOpen] = useState(false);
const toastRef = useRef(null);
const CUSTOM_SELECTOR = "button, .focusable-container input";
const [value, setValue] = useState("");
const [value2, setValue2] = useState("");

return (
<>
<Sidebar
open
onCancel={() => setIsDialogOpen}
open={isSidebarOpen}
onCancel={() => setIsSidebarOpen(false)}
header={<Typography variant="h3">Sidebar header</Typography>}
focusableContainers={[toastRef]}
focusableSelectors={CUSTOM_SELECTOR}
Expand Down Expand Up @@ -220,3 +194,26 @@ export const SidebarComponentFocusable = (props: Partial<SidebarProps>) => {
</>
);
};

export const SidebarWithStickyForm = () => (
<Sidebar open onCancel={() => {}} header="Sidebar with sticky footer">
<Form
saveButton={<Button buttonType="primary">Save</Button>}
stickyFooter
onSubmit={(event) => event.preventDefault()}
>
<Box height="1200px">Long content</Box>
</Form>
</Sidebar>
);

export const SidebarWithTallStickyFormFooter = () => (
<Sidebar open onCancel={() => {}} header="Sidebar with tall sticky footer">
<Form
footerChildren={<Box height="128px">Footer content</Box>}
stickyFooter
>
content
</Form>
</Sidebar>
);
Loading
Loading