Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
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={["raphael@goetz.de"]}>
<Menu.Trigger>
<Button>Click me</Button>
</Menu.Trigger>
<Menu.Content>
{
[{
mail: "nsammito@dummy.de",
name: "Nico Sammito"
}, {
mail: "nvschrick@dummy.de",
name: "Niklas van Schrick"
}, {
mail: "rgoetz@dummy.de",
name: "Raphael Götz"
}, {
mail: "mstaedler@dummy.de",
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