Skip to content

Commit

Permalink
Merge pull request #48 from code0-tech/menu
Browse files Browse the repository at this point in the history
Menu component
  • Loading branch information
nicosammito authored Feb 13, 2024
2 parents 491603f + 69f26f7 commit 9d81ee6
Show file tree
Hide file tree
Showing 23 changed files with 451 additions and 30 deletions.
2 changes: 1 addition & 1 deletion .storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ const preview: Preview = {
},
},
},
};
}

export default preview;
Binary file modified __snapshots__/badge--button-example-chromium.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __snapshots__/badge--button-example-firefox.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __snapshots__/badge--button-example-webkit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __snapshots__/badge--variants-chromium.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __snapshots__/badge--variants-firefox.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __snapshots__/badge--variants-webkit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added __snapshots__/menu--menu-account-chromium.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added __snapshots__/menu--menu-account-firefox.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added __snapshots__/menu--menu-account-list-firefox.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added __snapshots__/menu--menu-account-list-webkit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added __snapshots__/menu--menu-account-webkit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion src/components/badge/Badge.style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,5 @@
.badge--#{$name} {
@include opacityBox(false, $color);
font-size: $tertiaryFontSize;
border: none;
}
}
83 changes: 83 additions & 0 deletions src/components/menu/InternalMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {AriaMenuProps, useMenu, useMenuItem, useMenuSection} from "react-aria";
import {Node, useTreeState} from "react-stately";
import React from "react";
import {TreeState} from "@react-stately/tree";
import {IconCheck} from "@tabler/icons-react";
import "./Menu.style.scss"
import {MenuItemType, MenuSectionType} from "./Menu";

export function InternalMenu<T extends object>(props: AriaMenuProps<T>) {

const dummyState = useTreeState(props);
const disabledKeys = [...dummyState.collection.getKeys()].map(key => dummyState.collection.getItem(key)).filter(item => {
return item?.props.disabled || item?.props.unselectable
}).map(item => item?.key ?? "")

const state = useTreeState({
...props,
disabledKeys
});

// Get props for the menu element
const ref = React.useRef(null);
const {menuProps} = useMenu({
...props,
disabledKeys
}, state, ref);

return (
<ul {...menuProps} ref={ref} className={"menu"}>
{[...state.collection].map((item) => (
item.type === 'section' ? <MenuSection key={item.key} section={item} state={state}/>
: <InternalMenuItem key={item.key} item={item} state={state}/>
))}
</ul>
);
}

function InternalMenuItem<T>({item, state}: {item: Node<T>, state: TreeState<T>}) {

const {variant = "secondary", disabled = false, unselectable = false} = item.props as MenuItemType

// Get props for the menu item element
const ref = React.useRef(null);
const {menuItemProps, isSelected} = useMenuItem(
{key: item.key},
state,
ref
)

return (
<li {...(!disabled ? {...menuItemProps} : {})} ref={ref} className={`menu__item menu__item--${variant} ${disabled && "menu__item--disabled"} ${unselectable && "menu__item--unselectable"}`}>

<div>{item.rendered}</div>
{isSelected && !unselectable ? <IconCheck size={16} style={{marginLeft: ".5rem"}}/> : menuItemProps.role != "menuitem" ? <IconCheck size={16} style={{marginLeft: ".5rem", opacity: 0}}/> : null}
</li>
)
}

function MenuSection<T>({section, state}: {section: Node<T>, state: TreeState<T>}) {

const {title} = section.props as MenuSectionType
// Get props for the menu item element
const ref = React.useRef(null);
const { itemProps, headingProps, groupProps } = useMenuSection({
heading: section.rendered,
'aria-label': section['aria-label']
});

/**const children = [...state.collection.getKeys()].map((value) => {
return state.collection.getItem(value)
}).filter(item => item?.parentKey == section.key) as Node<any>[]**/

return <ul {...groupProps} className={"menu__section"}>
{title && <span className={"menu__section-title"}>{title}</span>}
{[...section.childNodes].map((node) => (
<InternalMenuItem
key={node.key}
item={node}
state={state}
/>
))}
</ul>
}
92 changes: 92 additions & 0 deletions src/components/menu/Menu.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {Meta, StoryObj} from "@storybook/react";
import React from "react";
import Button from "../button/Button";
import {Placement} from "react-aria";
import Menu from "./Menu";
import {IconLogout, IconUserCancel, IconUserEdit} from "@tabler/icons-react";
import Badge from "../badge/Badge";

const meta: Meta = {
title: "Menu",
component: Menu,
argTypes: {
placement: {
options: ['left start', 'left end', 'bottom start', 'bottom end', 'top start', 'top end', 'right start', 'right end'],
control: {type: 'radio'},
}
}
}

export default meta;

type MenuStory = StoryObj<{ placement: Placement }>

export const MenuAccount: MenuStory = {
render: (args) => {

const {placement} = args

return <>
<Menu placement={placement} defaultOpen>
<Menu.Trigger>
<Button>Click me</Button>
</Menu.Trigger>
<Menu.Content>
<Menu.Section>
<Menu.Item variant={"info"} unselectable key={"ssd"}>
Storage almost full. You can <br/>
manage your storage in Settings →
</Menu.Item>
</Menu.Section>
<Menu.Section title={"Account Settings"}>
<Menu.Item key={"update-account"}><Menu.Icon><IconUserEdit/></Menu.Icon> Update
Account</Menu.Item>
<Menu.Item variant={"error"}
key={"delete-account"}><Menu.Icon><IconUserCancel/></Menu.Icon> Delete
Account</Menu.Item>
</Menu.Section>
<Menu.Item variant={"warning"}
key="logout"><Menu.Icon><IconLogout/></Menu.Icon> Logout <Menu.Shortcut>⌘Q</Menu.Shortcut></Menu.Item>
</Menu.Content>
</Menu>

</>
}
}

export const MenuAccountList: MenuStory = {
render: (args) => {

const {placement} = args

return <>
<Menu placement={placement} defaultOpen selectionMode={"multiple"}
defaultSelectedKeys={["[email protected]"]}>
<Menu.Trigger>
<Button>Click me</Button>
</Menu.Trigger>
<Menu.Content>
{
[{
mail: "[email protected]",
name: "Nico Sammito"
}, {
mail: "[email protected]",
name: "Niklas van Schrick"
}, {
mail: "[email protected]",
name: "Raphael Götz"
}, {
mail: "[email protected]",
name: "Maximillian Städler"
}].map(item => (
<Menu.Item key={item.mail}>{item.name} <Badge
style={{marginLeft: ".5rem"}}>{item.mail}</Badge></Menu.Item>
))
}
</Menu.Content>
</Menu>

</>
}
}
89 changes: 89 additions & 0 deletions src/components/menu/Menu.style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
@import "src/styles/helpers";

.menu {

list-style: none;
margin: -.25rem 0;
padding: 0;
outline: none;

> *:first-child.menu__section {
border-top: none;
margin-top: 0;
padding-top: 0;
}

> *:last-child.menu__section {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}

&__item {
@include disabled();
border: none !important;
margin: 0 -.25rem;
border-radius: .5rem;
padding: .5rem;
cursor: pointer;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;

> div {
position: relative;
display: flex;
width: 100%;
align-items: center;
}

}

&__section {
border-top: 1px solid borderColor();
border-bottom: 1px solid borderColor();
list-style: none;
margin: .25rem -.5rem;
padding: .25rem .5rem;
outline: none;

+ .menu__section {
border-top: none;
margin-top: -.25rem;
}
}

&__section-title {
font-size: $tertiaryFontSize;
color: rgba($white, .25);
display: block;
margin: .25rem 0 .25rem .25rem;
}

&__icon {
margin-right: .5rem;
}

&__shortcut {
margin-left: auto;
padding-left: .5rem;
}

}

@each $name, $color in $variants {
.menu__item--#{$name} {
@include hoverAndActiveContent {
background: rgba($color, .2);
}

.menu__icon {
color: rgba($color, .5);
}
}
.menu__item--unselectable {
background: transparent !important;
pointer-events: none;
}
}
Loading

0 comments on commit 9d81ee6

Please sign in to comment.