Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ListItemContainer, ListHeaderContainer } from "./styles";
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import styled from "styled-components";
import { Flex } from "../../Flex";

export const ListItemContainer = styled(Flex)`
& .t--entity-item {
grid-template-columns: 0 auto 1fr auto auto auto auto auto;
height: 32px;

& .t--entity-name {
padding-left: var(--ads-v2-spaces-3);
}
}
`;

export const ListHeaderContainer = styled.div`
padding: var(--ads-v2-spaces-3);
padding-right: var(--ads-v2-spaces-2);
display: flex;
justify-content: space-between;
align-items: center;
height: 40px;

span {
line-height: 20px;
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import styled from "styled-components";
import { PopoverContent } from "../../../Popover";

export const SwitchTrigger = styled.div<{ active: boolean }>`
display: flex;
border-radius: var(--ads-v2-border-radius);
background-color: ${(props) =>
props.active ? `var(--ads-v2-color-bg-subtle)` : "unset"};
cursor: pointer;
padding: var(--ads-v2-spaces-2);

:hover {
background-color: var(--ads-v2-color-bg-subtle);
}
`;

export const ContentContainer = styled(PopoverContent)`
padding: 0;
padding-bottom: 0.25em;
`;
Original file line number Diff line number Diff line change
@@ -1,77 +1,78 @@
import React from "react";
import { render, fireEvent } from "@testing-library/react";
import HeaderEditorSwitcher from "./HeaderEditorSwitcher";
import { render, fireEvent, screen } from "@testing-library/react";
import { IDEHeaderSwitcher } from "./IDEHeaderSwitcher";
import "@testing-library/jest-dom";

describe("HeaderEditorSwitcher", () => {
describe("HeaderSwitcher", () => {
const mockOnClick = jest.fn();
const mockSetActive = jest.fn();
const defaultProps = {
prefix: "Prefix",
title: "Title",
titleTestId: "titleTestId",
active: false,
onClick: mockOnClick,
setActive: mockSetActive,
children: <span>Test</span>,
};

it("renders with correct props", () => {
const { getByText } = render(<HeaderEditorSwitcher {...defaultProps} />);
render(<IDEHeaderSwitcher {...defaultProps} />);

// eslint-disable-next-line testing-library/no-node-access
const testIdElement = document.getElementsByClassName(
defaultProps.titleTestId,
);
Comment thread
hetunandu marked this conversation as resolved.

expect(getByText("Prefix /")).toBeInTheDocument();
expect(getByText(defaultProps.title)).toBeInTheDocument();
expect(screen.getByText("Prefix /")).toBeInTheDocument();
expect(screen.getByText(defaultProps.title)).toBeInTheDocument();
expect(testIdElement).toBeDefined();
});

it("renders active state correctly", () => {
const { getByText } = render(
<HeaderEditorSwitcher {...defaultProps} active />,
);
render(<IDEHeaderSwitcher {...defaultProps} active />);

expect(getByText("Prefix /")).toHaveStyle(
expect(screen.getByText("Prefix /")).toHaveStyle(
"background-color: var(--ads-v2-color-bg-subtle)",
);
});

it("calls onClick handler when clicked", () => {
const { getByText } = render(<HeaderEditorSwitcher {...defaultProps} />);
render(<IDEHeaderSwitcher {...defaultProps} />);

fireEvent.click(getByText("Title"));
fireEvent.click(screen.getByText("Title"));

expect(mockOnClick).toHaveBeenCalled();
});

it("forwards ref correctly", () => {
const ref = React.createRef();
const ref = React.createRef<HTMLDivElement>();

render(<HeaderEditorSwitcher {...defaultProps} ref={ref} />);
render(<IDEHeaderSwitcher {...defaultProps} ref={ref} />);
expect(ref.current).toBeTruthy();
});

it("does not crash when onClick is not provided", () => {
const { getByText } = render(
<HeaderEditorSwitcher {...defaultProps} onClick={undefined} />,
);
render(<IDEHeaderSwitcher {...defaultProps} onClick={undefined} />);

fireEvent.click(getByText("Title")); // Should not throw error
fireEvent.click(screen.getByText("Title")); // Should not throw error
});
Comment thread
hetunandu marked this conversation as resolved.

it("does not show separator and applies different inactive color to icon", () => {
const ref = React.createRef();
const { container, getByTestId } = render(
<HeaderEditorSwitcher
const ref = React.createRef<HTMLDivElement>();
const { container } = render(
<IDEHeaderSwitcher
{...defaultProps}
data-testid="root-div"
ref={ref}
title={undefined}
/>,
);

// eslint-disable-next-line testing-library/no-container,testing-library/no-node-access
const icon = container.querySelector(".remixicon-icon"); // Get chevron icon

Comment thread
hetunandu marked this conversation as resolved.
expect(getByTestId("root-div")).toHaveTextContent("Prefix");
expect(screen.getByTestId("root-div")).toHaveTextContent("Prefix");
expect(icon).toHaveAttribute(
"fill",
"var(--ads-v2-colors-content-label-inactive-fg)",
Expand All @@ -83,16 +84,20 @@ describe("HeaderEditorSwitcher", () => {
const className = "custom-class";

const { container } = render(
<HeaderEditorSwitcher
<IDEHeaderSwitcher
active
className={className}
data-testid={testId} // Additional prop
prefix="Prefix"
setActive={mockSetActive}
title="Title"
titleTestId="titleTestId"
/>,
>
<span>Test</span>
</IDEHeaderSwitcher>,
);

// eslint-disable-next-line testing-library/no-container,testing-library/no-node-access
const firstDiv = container.querySelector("div"); // Get the first div element
const classNames = firstDiv?.getAttribute("class")?.split(" ") || [];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React, { type ForwardedRef, useCallback } from "react";
import { Flex } from "../../../Flex";
import { Icon } from "../../../Icon";
import { Popover, PopoverTrigger } from "../../../Popover";
import { Text } from "../../../Text";
import * as Styled from "./HeaderSwitcher.styles";

interface Props {
prefix: string;
title?: string;
titleTestId: string;
active: boolean;
setActive: (active: boolean) => void;
onClick?: React.MouseEventHandler<HTMLDivElement>;
className?: string;
children: React.ReactNode;
}

export const IDEHeaderSwitcher = React.forwardRef(
(props: Props, ref: ForwardedRef<HTMLDivElement>) => {
const {
active,
children,
className,
onClick,
prefix,
setActive,
title,
titleTestId,
...rest
} = props;

const separator = title ? " /" : "";

const closeSwitcher = useCallback(() => {
return setActive(false);
}, [setActive]);

return (
<Popover onOpenChange={setActive} open={active}>
<PopoverTrigger>
<Styled.SwitchTrigger
active={active}
className={`flex align-center items-center justify-center ${className}`}
data-testid={titleTestId}
onClick={onClick}
ref={ref}
{...rest}
>
<Text
color="var(--ads-v2-colors-content-label-inactive-fg)"
kind="body-m"
>
{prefix + separator}
</Text>
<Flex
alignItems="center"
className={titleTestId}
data-active={active}
gap="spaces-1"
height="100%"
justifyContent="center"
paddingLeft="spaces-2"
>
<Text isBold kind="body-m">
{title}
</Text>
<Icon
color={
title
? undefined
: "var(--ads-v2-colors-content-label-inactive-fg)"
}
name={active ? "arrow-up-s-line" : "arrow-down-s-line"}
size="md"
/>
</Flex>
</Styled.SwitchTrigger>
</PopoverTrigger>
<Styled.ContentContainer align="start" onEscapeKeyDown={closeSwitcher}>
{children}
</Styled.ContentContainer>
</Popover>
);
},
);

IDEHeaderSwitcher.displayName = "IDEHeaderSwitcher";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { IDEHeaderSwitcher } from "./IDEHeaderSwitcher";
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const IDE_HEADER_HEIGHT = 40;
export const LOGO_WIDTH = 50;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Canvas, Meta } from "@storybook/blocks";
import * as IDEHeaderStories from "./IDEHeader.stories";

<Meta of={IDEHeaderStories} />

# IDEHeader

IDEHeader sets the stage for the IDE experience. It is the topmost section of the IDE that contains the Appsmith logo, the app name, and the user profile.

<Canvas of={IDEHeaderStories.Default} />

## Anatomy

### Left Section options

The local title

#### Header Title

A title that is specific to the app state. It is displayed on the left side of the header.

<Canvas of={IDEHeaderStories.WithHeaderTitle} />

#### Header Dropdown

A dropdown that allows the user to switch between different pages.

<Canvas of={IDEHeaderStories.WithHeaderDropdown} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React from "react";
import type { Meta } from "@storybook/react";
import { IDEHeader } from "./IDEHeader";
import { IDEHeaderTitle } from "./IDEHeaderTitle";
import { IDEHeaderSwitcher } from "./HeaderSwitcher";
Comment thread
hetunandu marked this conversation as resolved.
import { noop } from "lodash";
import { Icon } from "../../Icon";
import { Button } from "../../Button";
import { List } from "../../List";
import { Flex } from "../../Flex";
import { Text } from "../../Text";
import { ListHeaderContainer } from "../EntityExplorer/styles";

const meta: Meta = {
title: "ADS/Templates/IDEHeader",
component: IDEHeader,
parameters: {
layout: "fullscreen",
},
decorators: [
(Story: () => React.ReactNode) => (
<div style={{ width: "100%" }}>{Story()}</div>
),
],
};

export default meta;

export const Default = () => (
<IDEHeader>
<IDEHeader.Left logo={<Icon name="upload-cloud" size="md" />}>
<span>Left Content</span>
</IDEHeader.Left>
<IDEHeader.Center>
<span>Center Content</span>
</IDEHeader.Center>
<IDEHeader.Right>
<span>Right Content</span>
</IDEHeader.Right>
</IDEHeader>
);

export const WithHeaderTitle = () => {
return (
<IDEHeader>
<IDEHeader.Left logo={<Icon name="upload-cloud" size="md" />}>
<IDEHeaderTitle title="Settings" />
</IDEHeader.Left>
<IDEHeader.Center />
<IDEHeader.Right />
</IDEHeader>
);
};

export const WithHeaderDropdown = () => {
const [open, setOpen] = React.useState(false);

return (
<IDEHeader>
<IDEHeader.Left logo={<Icon name="upload-cloud" size="md" />}>
<IDEHeaderSwitcher
active={open}
prefix={"Pages"}
setActive={setOpen}
title="Page1"
titleTestId={"testId"}
>
<Flex
flexDirection="column"
justifyContent="center"
maxHeight={"300px"}
overflow="hidden"
>
<ListHeaderContainer>
<Text kind="heading-xs">Pages</Text>
<Button isIconButton kind="tertiary" startIcon="plus" />
</ListHeaderContainer>
Comment thread
hetunandu marked this conversation as resolved.
<List
items={[
{
title: "Page1",
onClick: noop,
description: "",
descriptionType: "inline",
},
{
title: "Page2",
onClick: noop,
description: "",
descriptionType: "inline",
},
]}
/>
Comment thread
hetunandu marked this conversation as resolved.
</Flex>
</IDEHeaderSwitcher>
</IDEHeader.Left>
<IDEHeader.Center />
<IDEHeader.Right />
</IDEHeader>
);
};
Loading