Skip to content

Commit

Permalink
chore(web): refactor breadcrumb (#1008)
Browse files Browse the repository at this point in the history
  • Loading branch information
mkumbobeaty authored Jun 16, 2024
1 parent 2f0c3a8 commit 5b50d83
Show file tree
Hide file tree
Showing 12 changed files with 498 additions and 5 deletions.
120 changes: 120 additions & 0 deletions web/src/beta/lib/reearth-ui/components/Breadcrumb/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { Meta, StoryObj } from "@storybook/react";

import { PopupMenu, PopupMenuItem as PopupItems } from "@reearth/beta/lib/reearth-ui";

import { Breadcrumb, BreadcrumbProp, BreadcrumbItem } from "./";

const meta: Meta<BreadcrumbProp> = {
component: Breadcrumb,
};

export default meta;

type Story = StoryObj<BreadcrumbProp>;

const defaultItems: BreadcrumbItem[] = [
{
title: "First",
},
{
title: "Second",
},
{
title: "Third",
},
];

const multiLevelItems: BreadcrumbItem[] = [
{
title: "First Item",
icon: "addStyle",
subItem: [
{
id: "one",
title: "Sub Item one",
subItem: [
{
id: "sub-one",
title: "Item one",
},
{
id: "sub-two",
title: "Item two",
},
],
},
{
id: "two",
title: "Sub Item two",
},
{
id: "three",
title: "Sub Item three",
},
],
},
{
title: "Second Item",
},
];

const itemMenu: BreadcrumbItem[] = [
{
title: "First Item",
subItem: [
{
id: "one-1",
title: "Sub Menu 1",
},
{
id: "two-2",
title: "Sub Menu 2",
},
],
},
{
title: "Second Item",
},
{
title: "Third Item",
},
];

const renderPopupMenu = (items: BreadcrumbItem[], level: number) => {
return items.map(item => {
if (item.subItem) {
return {
...item,
title: (
<PopupMenu label={item.title} menu={item.subItem as PopupItems[]} nested={level > 0} />
),
subItem: undefined,
};
}
return item;
});
};

export const Default: Story = {
render: () => (
<div style={{ margin: "5px", height: "10vh" }}>
<Breadcrumb items={defaultItems} />
</div>
),
};

export const DropdownMenu: Story = {
render: () => (
<div style={{ margin: "5px", height: "10vh" }}>
<Breadcrumb items={renderPopupMenu(itemMenu, 0)} />
</div>
),
};

export const MultiLevelMenu: Story = {
render: () => (
<div style={{ margin: "5px", height: "10vh" }}>
<Breadcrumb items={renderPopupMenu(multiLevelItems, 0)} />
</div>
),
};
68 changes: 68 additions & 0 deletions web/src/beta/lib/reearth-ui/components/Breadcrumb/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { FC, ReactNode } from "react";

import { Icon, IconName, Typography } from "@reearth/beta/lib/reearth-ui";
import { styled, useTheme } from "@reearth/services/theme";

import { PopupMenuItem } from "../PopupMenu";

export type BreadcrumbItem = {
title: string | ReactNode;
icon?: IconName;
subItem?: PopupMenuItem[];
};

export type BreadcrumbProp = {
items: BreadcrumbItem[];
separator?: ReactNode;
onClick?: () => void;
};

export const Breadcrumb: FC<BreadcrumbProp> = ({ items = [], separator = " / ", onClick }) => {
const theme = useTheme();
return (
<Wrapper>
{items.map((item, index) => (
<ItemWrapper key={index}>
<Item onClick={onClick}>
{typeof item.title === "string" ? (
<>
{item.icon && <Icon icon={item.icon} size="small" color={theme.content.weak} />}
<Typography weight="bold" size="body">
{item.title}
</Typography>
</>
) : (
item.title
)}
</Item>
{index < items.length - 1 && <Separator>{separator}</Separator>}
</ItemWrapper>
))}
</Wrapper>
);
};

const Wrapper = styled("div")(({ theme }) => ({
display: "flex",
gap: theme.spacing.smallest,
}));

const ItemWrapper = styled("div")(({ theme }) => ({
display: "flex",
alignItems: "center",
gap: theme.spacing.smallest,
cursor: "pointer",
}));

const Item = styled("div")(({ theme }) => ({
padding: `${theme.spacing.micro}px ${theme.spacing.small}px`,
borderRadius: theme.radius.smallest,
display: "flex",
gap: theme.spacing.smallest,
alignItems: "center",
}));

const Separator = styled("div")(({ theme }) => ({
color: theme.content.weak,
userSelect: "none",
}));
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const Default: Story = {
<Button
title="Secondary Icon Button"
icon="settingFilled"
iconRight="caretDown"
onClick={action("secondary-icon-button-click")}
/>
<Button
Expand Down Expand Up @@ -63,6 +64,7 @@ export const Primary: Story = {
appearance="primary"
icon="settingFilled"
onClick={action("primary-icon-button-click")}
iconRight="caretDown"
/>
<Button
title="Primary Extend"
Expand Down Expand Up @@ -113,6 +115,7 @@ export const Dangerous: Story = {
title="Dangerous Icon Button"
appearance="dangerous"
icon="settingFilled"
iconRight="caretDown"
onClick={action("dangerous-icon-button-click")}
/>
<Button
Expand Down Expand Up @@ -164,6 +167,7 @@ export const Simple: Story = {
title="Simple Icon Button"
appearance="simple"
icon="settingFilled"
iconRight="caretDown"
onClick={action("simple-icon-button-click")}
/>
<Button
Expand Down
3 changes: 3 additions & 0 deletions web/src/beta/lib/reearth-ui/components/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type ButtonProps = {
size?: "normal" | "small";
iconButton?: boolean;
icon?: IconName;
iconRight?: IconName;
title?: string;
extendWidth?: boolean;
minWidth?: number;
Expand All @@ -23,6 +24,7 @@ export const Button: FC<ButtonProps> = ({
size = "normal",
iconButton,
title,
iconRight,
extendWidth,
minWidth,
onClick,
Expand All @@ -38,6 +40,7 @@ export const Button: FC<ButtonProps> = ({
onClick={onClick}>
{icon && <Icon icon={icon} />}
{!iconButton && title}
{iconRight && <Icon icon={iconRight} />}
</StyledButton>
);
};
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions web/src/beta/lib/reearth-ui/components/Icon/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import CaretDoubleLeft from "./Icons/CaretDoubleLeft.svg?react";
import CaretDoubleRight from "./Icons/CaretDoubleRight.svg?react";
import CaretDown from "./Icons/CaretDown.svg?react";
import CaretLeft from "./Icons/CaretLeft.svg?react";
import CaretRight from "./Icons/CaretRight.svg?react";
import CaretUp from "./Icons/CaretUp.svg?react";
import Check from "./Icons/Check.svg?react";
import Checked from "./Icons/Checked.svg?react";
Expand Down Expand Up @@ -128,6 +129,7 @@ export default {
caretDoubleLeft: CaretDoubleLeft,
caretDoubleRight: CaretDoubleRight,
caretDown: CaretDown,
caretRight: CaretRight,
caretLeft: CaretLeft,
caretUp: CaretUp,
check: Check,
Expand Down
9 changes: 8 additions & 1 deletion web/src/beta/lib/reearth-ui/components/Popup/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
useClick,
useDismiss,
useInteractions,
useHover,
safePolygon,
} from "@floating-ui/react";
import { useCallback, useMemo, useState } from "react";

Expand All @@ -18,6 +20,7 @@ const usePopover = ({
offset: offsetProps,
shift: shiftProps,
onOpenChange,
triggerOnHover = false,
}: Omit<PopupProps, "children" | "trigger">) => {
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);

Expand Down Expand Up @@ -55,8 +58,12 @@ const usePopover = ({

const click = useClick(context);
const dismiss = useDismiss(context);
const hover = useHover(context, {
enabled: triggerOnHover,
handleClose: safePolygon(),
});

const interactions = useInteractions([click, dismiss]);
const interactions = useInteractions([hover, click, dismiss]);

return useMemo(
() => ({
Expand Down
15 changes: 12 additions & 3 deletions web/src/beta/lib/reearth-ui/components/Popup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,26 @@ export type PopupProps = {
children?: ReactNode;
trigger?: ReactNode;
disabled?: boolean;
triggerOnHover?: boolean;
placement?: Placement;
open?: boolean;
offset?: OffsetOptions;
shift?: ShiftOptions;
nested?: boolean;
onOpenChange?: (open: boolean) => void;
};

export const Popup = ({ children, trigger, disabled, ...restOptions }: PopupProps) => {
const popover = usePopover({ ...restOptions });
export const Popup = ({
children,
trigger,
disabled,
triggerOnHover,
...restOptions
}: PopupProps) => {
const popover = usePopover({ ...restOptions, triggerOnHover });

return (
<PopoverContext.Provider value={popover}>
<PopoverContext.Provider value={{ ...popover, setOpen: popover.setOpen }}>
<Trigger disabled={disabled}>{trigger}</Trigger>
<Content>{children}</Content>
</PopoverContext.Provider>
Expand All @@ -96,6 +104,7 @@ export const Popup = ({ children, trigger, disabled, ...restOptions }: PopupProp
const TriggerWrapper = styled("div")<{ disabled?: boolean }>(({ disabled }) => ({
width: "fit-content",
pointerEvents: disabled ? "none" : "auto",
flex: 1,
}));

const ContentWrapper = styled("div")(({ theme }) => ({
Expand Down
Loading

0 comments on commit 5b50d83

Please sign in to comment.