-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Jia/QUILL-1942/bottom-navigation-bar (#375)
* 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
Showing
15 changed files
with
840 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
123 changes: 123 additions & 0 deletions
123
lib/components/Navigation/bottom-navigation/bottom-action/__tests__/bottom-action.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
}); | ||
}); |
62 changes: 62 additions & 0 deletions
62
lib/components/Navigation/bottom-navigation/bottom-action/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
120
lib/components/Navigation/bottom-navigation/bottom-bar.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
Oops, something went wrong.