Skip to content

Commit

Permalink
Jia/QUILL-1942/bottom-navigation-bar (#375)
Browse files Browse the repository at this point in the history
* feat: add bottom navigation and storybook

* chore: update storybook

* chore: update storybook

* chore: update token

* chore: update label color

* chore: update test case

* chore: update comment
  • Loading branch information
jia-deriv authored Aug 21, 2024
1 parent 1b36283 commit cef2883
Show file tree
Hide file tree
Showing 15 changed files with 840 additions and 35 deletions.
2 changes: 1 addition & 1 deletion .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { withoutVitePlugins } from "@storybook/builder-vite";
import type { StorybookConfig } from "@storybook/react-vite";

const config: StorybookConfig = {
stories: ["../lib/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
stories: ["../lib/**/*.mdx", "../lib/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
Expand Down
1 change: 1 addition & 0 deletions lib/components/Button/button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
display: inline-flex;
align-items: center;
justify-content: center;
outline-offset: 3px;

&:disabled {
cursor: auto;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { fireEvent, render } from "@testing-library/react";
import BottomAction from "..";

describe("BottomAction", () => {
it("renders BottomAction with correct props", () => {
const { getByText, container } = render(
<BottomAction
icon={<svg />}
activeIcon={<svg />}
label="Test Label"
showLabel={true}
selected={false}
/>,
);

expect(
container.querySelector(
".quill-navigation-bottom-bar__action-icon",
),
).toBeInTheDocument();

expect(getByText("Test Label")).toBeInTheDocument();

expect(container.firstChild).toHaveClass(
"quill-navigation-bottom-bar__action-selected--false",
);
});

it("triggers onClick and onChange events", () => {
const handleClick = jest.fn();
const handleChange = jest.fn();
const { getByRole } = render(
<BottomAction
icon={<svg />}
activeIcon={<svg />}
onClick={handleClick}
onChange={handleChange}
as="button"
/>,
);

const actionElement = getByRole("button");

fireEvent.click(actionElement);

expect(handleClick).toHaveBeenCalled();
expect(handleChange).toHaveBeenCalled();
});

it("shows activeIcon when selected is true", () => {
const { queryByTestId } = render(
<BottomAction
icon={<svg data-testid="icon" />}
activeIcon={<svg data-testid="active-icon" />}
selected={true}
/>,
);

expect(queryByTestId("active-icon")).toBeInTheDocument();
expect(queryByTestId("icon")).not.toBeInTheDocument();
});

it("shows icon when selected is false", () => {
const { queryByTestId } = render(
<BottomAction
icon={<svg data-testid="icon" />}
activeIcon={<svg data-testid="active-icon" />}
selected={false}
/>,
);

expect(queryByTestId("active-icon")).not.toBeInTheDocument();
expect(queryByTestId("icon")).toBeInTheDocument();
});

it("shows label when showLabel is true", () => {
const { getByText } = render(
<BottomAction
icon={<svg />}
activeIcon={<svg />}
label="Test Label"
showLabel={true}
/>,
);

expect(getByText("Test Label")).toBeInTheDocument();
});

it("hides label when showLabel is false", () => {
const { queryByText } = render(
<BottomAction
icon={<svg />}
activeIcon={<svg />}
label="Test Label"
showLabel={false}
/>,
);

expect(queryByText("Test Label")).not.toBeInTheDocument();
});

it("renders with custom element", () => {
const { container } = render(
<BottomAction as="button" icon={<svg />} activeIcon={<svg />} />,
);

const wrapper = container.firstChild as HTMLElement;

expect(wrapper.tagName).toBe("BUTTON");
});

it("applies custom class names", () => {
const { container } = render(
<BottomAction
icon={<svg />}
activeIcon={<svg />}
className="custom-class"
/>,
);

expect(container.firstChild).toHaveClass("custom-class");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from "react";
import { BottomBarProps } from "../bottom-bar";
import clsx from "clsx";
import { CaptionText } from "@components/Typography";

export interface BottomActionProps extends Omit<BottomBarProps, "showLabels"> {
selected?: boolean;
label?: string | React.ReactNode;
showLabel?: boolean;
icon: React.ReactNode;
activeIcon: React.ReactNode;
}

const BottomAction = (props: BottomActionProps) => {
const {
as: Element = "div",
label,
children,
icon,
activeIcon,
className,
showLabel,
selected,
onChange,
onClick,
value,
...rest
} = props;

const handleChange = (event: React.ChangeEvent<HTMLElement>) => {
onChange?.(event, value);
onClick?.(event);
};

return (
<Element
className={clsx(
"quill-navigation-bottom-bar__action",
`quill-navigation-bottom-bar__action-selected--${selected}`,
className,
)}
onClick={handleChange}
{...rest}
>
{(icon || activeIcon) && (
<span className="quill-navigation-bottom-bar__action-icon">
{selected ? activeIcon : icon}
</span>
)}
{showLabel && (
<CaptionText color="quill-navigation-bottom-bar__action-label">
{label}
</CaptionText>
)}
{children}
</Element>
);
};

BottomAction.displayName = "Navigation.BottomAction";

export default BottomAction;
120 changes: 120 additions & 0 deletions lib/components/Navigation/bottom-navigation/bottom-bar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React from "react";
import type { Meta, StoryObj } from "@storybook/react";
import {
StandaloneGearFillIcon,
StandaloneGearRegularIcon,
StandaloneHouseBlankFillIcon,
StandaloneHouseBlankRegularIcon,
StandalonePlaceholderFillIcon,
StandalonePlaceholderRegularIcon,
StandaloneUserFillIcon,
StandaloneUserRegularIcon,
} from "@deriv/quill-icons";
import Navigation from "..";
import { BottomBar, BottomAction } from ".";
import { Text } from "@components/Typography";
import "@deriv-com/quill-tokens/dist/quill.css";

type Template = React.ComponentProps<typeof BottomBar & typeof BottomAction>;

const icons: Record<string, object> = {
placeholder: {
active: <StandalonePlaceholderFillIcon iconSize="sm" />,
default: <StandalonePlaceholderRegularIcon iconSize="sm" />,
},
home: {
active: <StandaloneHouseBlankFillIcon iconSize="sm" />,
default: <StandaloneHouseBlankRegularIcon iconSize="sm" />,
},
profile: {
active: <StandaloneUserFillIcon iconSize="sm" />,
default: <StandaloneUserRegularIcon iconSize="sm" />,
},
settings: {
active: <StandaloneGearFillIcon iconSize="sm" />,
default: <StandaloneGearRegularIcon iconSize="sm" />,
},
};

const meta = {
title: "Components/Navigation/BottomBar",
args: {
label: "Label",
icon: <StandalonePlaceholderRegularIcon iconSize="sm" />,
activeIcon: <StandalonePlaceholderFillIcon iconSize="sm" />,
showLabels: false,
value: 0,
customIcon: "home",
},
argTypes: {
customIcon: {
description: "icon in each bottom bar action",
options: Object.keys(icons),
mapping: icons,
control: {
type: "radio",
},
},
showLabels: {
control: "boolean",
},
},
} satisfies Meta<typeof BottomBar & typeof BottomAction>;

export default meta;
type Story = StoryObj<typeof meta>;

const Template: React.FC<Template> = ({ length = 4, ...args }: Template) => {
const { value, showLabels, customIcon, ...rest } = args;
const [index, setIndex] = React.useState(value);

const Content = () => (
<div
style={{
backgroundColor:
"var(--semantic-color-slate-solid-surface-frame-low)",
height: "500px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Text>This is content in Menu {index + 1}</Text>
</div>
);

React.useEffect(() => {
setIndex(value);
}, [value]);

return (
<>
<Content />
<Navigation.Bottom
value={index}
showLabels={showLabels}
onChange={(_, newValue) => {
setIndex(newValue);
}}
>
{Array.from({ length }, () => {
return (
<Navigation.BottomAction
{...rest}
activeIcon={customIcon.active}
icon={customIcon.default}
/>
);
})}
</Navigation.Bottom>
</>
);
};

const BottomNavigationBarDefault = Template.bind(this) as Story;
BottomNavigationBarDefault.args = { ...meta.args };

const BottomNavigationBarWithLabels = Template.bind(this) as Story;
BottomNavigationBarWithLabels.args = { ...meta.args, showLabels: true };

export { BottomNavigationBarDefault, BottomNavigationBarWithLabels };
Loading

0 comments on commit cef2883

Please sign in to comment.