Skip to content

Commit 9a295c0

Browse files
committed
remove support for collapsed menu items
1 parent 0e08eaa commit 9a295c0

8 files changed

Lines changed: 130 additions & 149 deletions

File tree

svelte/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

svelte/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@invopop/popui",
33
"license": "MIT",
4-
"version": "0.1.94",
4+
"version": "0.1.95",
55
"repository": {
66
"url": "https://github.com/invopop/popui"
77
},

svelte/src/lib/MenuItem.svelte

Lines changed: 70 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,45 @@
11
<script lang="ts">
22
import MenuItem from './MenuItem.svelte'
3-
import { flip, shift, offset } from 'svelte-floating-ui/dom'
4-
import { createFloatingActions } from 'svelte-floating-ui'
53
import clsx from 'clsx'
6-
import type { MenuItemProps, DrawerOption, AnyProp } from './types.ts'
4+
import { cn } from './utils.js'
5+
import type { MenuItemProps } from './types.ts'
76
import { Icon, type IconSource } from '@steeze-ui/svelte-icon'
87
import { ChevronDown, ChevronRight } from '@invopop/ui-icons'
9-
import { FolderL } from '@invopop/ui-icons'
108
import { resolveIcon } from './helpers.js'
11-
import DrawerContext from './DrawerContext.svelte'
129
import TagBeta from './TagBeta.svelte'
1310
14-
const [floatingRef, floatingContent] = createFloatingActions({
15-
strategy: 'absolute',
16-
placement: 'bottom-start',
17-
middleware: [offset(-4), flip(), shift()]
18-
})
19-
2011
let {
2112
label = '',
2213
url = '',
2314
isFolderItem = false,
2415
collapsable = false,
2516
open = $bindable(false),
2617
active = false,
27-
collapsedSidebar = false,
2818
iconTheme = 'default',
2919
icon = undefined,
20+
imageUrl = undefined,
3021
beta = false,
3122
children = undefined,
23+
action,
24+
ref = $bindable(null),
25+
class: className,
3226
onclick
3327
}: MenuItemProps = $props()
3428
3529
let resolvedIcon: IconSource | undefined = $state()
36-
let hovered = $state(false)
37-
let highlight = $state(false)
38-
let leaveHoverTimeout: ReturnType<typeof setTimeout> | null = null
3930
let itemStyles = $derived(
40-
clsx(
41-
{ 'text-foreground-inverse font-medium': !isFolderItem },
42-
{ 'text-foreground-inverse-secondary': isFolderItem && !active },
43-
{
44-
'border border-transparent hover:bg-background-selected-inverse group p-[7px] size-8':
45-
collapsedSidebar
46-
},
47-
{ 'w-full px-2 py-1.5 h-8': !collapsedSidebar },
48-
{
49-
'bg-background-selected-inverse text-white': active
50-
},
51-
{ 'hover:bg-background-selected-inverse': !active }
52-
)
53-
)
54-
let iconStyles = $derived(
55-
clsx({ 'group-hover:text-white text-icon-inverse-bold!': collapsedSidebar })
31+
clsx('flex-1 min-w-0 px-2 py-1.5 h-8', {
32+
'text-foreground-inverse font-medium': !isFolderItem,
33+
'text-foreground-inverse-secondary': isFolderItem && !active,
34+
'bg-background-selected-inverse text-white': active,
35+
'hover:bg-background-selected-inverse': !active
36+
})
5637
)
5738
let wrapperStyles = $derived(
5839
clsx({
59-
'ml-4 border-l border-white-10 pl-2 pt-0.5 relative': isFolderItem
40+
'group/menu-item ml-4 border-l border-white-10 pl-2 pt-0.5 relative': isFolderItem
6041
})
6142
)
62-
let items = $derived([
63-
{ label, value: url, selected: active, icon: resolvedIcon },
64-
...(children || []).map((c) => ({
65-
label: c.label || '',
66-
value: c.url || '',
67-
selected: c.active,
68-
icon: FolderL
69-
}))
70-
] as DrawerOption[])
7143
7244
$effect(() => {
7345
resolveIcon(icon).then((res) => (resolvedIcon = res))
@@ -80,84 +52,71 @@
8052
8153
onclick?.(url)
8254
}
83-
84-
function handleClickChild(value: AnyProp) {
85-
hovered = false
86-
onclick?.(value as string)
87-
}
88-
89-
function handleHover() {
90-
highlight = true
91-
if (leaveHoverTimeout) {
92-
clearTimeout(leaveHoverTimeout)
93-
}
94-
hovered = true
95-
}
96-
97-
function handleBlur() {
98-
highlight = false
99-
leaveHoverTimeout = setTimeout(() => {
100-
hovered = false
101-
}, 200)
102-
}
10355
</script>
10456

105-
<div class={wrapperStyles}>
106-
{#if isFolderItem && (highlight || active)}
107-
<div class="border-l border-white h-3 w-px absolute top-3.5 left-0 -m-px"></div>
57+
<div bind:this={ref} class={cn(wrapperStyles, className)} data-menu-item-root>
58+
{#if isFolderItem}
59+
<div
60+
class={clsx('border-l border-white h-3 w-px absolute top-3.5 left-0 -m-px', {
61+
'opacity-0 group-hover/menu-item:opacity-100': !active
62+
})}
63+
data-menu-item-tree-indicator
64+
></div>
10865
{/if}
109-
<button
110-
use:floatingRef
111-
onmouseenter={handleHover}
112-
onmouseleave={handleBlur}
113-
onclick={handleClick}
114-
title={label}
115-
class="cursor-pointer {itemStyles} text-base border border-transparent flex items-center justify-between hover:text-white focus:text-white rounded-lg"
116-
>
117-
<span class="flex items-center space-x-1.5">
118-
{#if resolvedIcon}
119-
<Icon src={resolvedIcon} theme={iconTheme} class="{iconStyles} h-4 w-4 text-icon-inverse" />
120-
{/if}
121-
{#if !collapsedSidebar}
122-
<span class="whitespace-nowrap tracking-normal">{label}</span>
66+
<div class={clsx('flex items-center', { 'gap-1.5': action })} data-menu-item-row>
67+
<button
68+
onclick={handleClick}
69+
title={label}
70+
data-menu-item-button
71+
class="cursor-pointer {itemStyles} text-base border border-transparent flex items-center justify-between hover:text-white focus:text-white rounded-lg"
72+
>
73+
<span class="flex items-center space-x-1.5 min-w-0 flex-1" data-menu-item-content>
74+
{#if imageUrl}
75+
<img
76+
src={imageUrl}
77+
alt={label}
78+
class="size-4 shrink-0 rounded bg-white object-contain"
79+
data-menu-item-image
80+
/>
81+
{:else if resolvedIcon}
82+
<Icon
83+
src={resolvedIcon}
84+
theme={iconTheme}
85+
class="h-4 w-4 text-icon-inverse"
86+
data-menu-item-icon
87+
/>
88+
{/if}
89+
<span class="truncate tracking-normal" data-menu-item-label>{label}</span>
12390
{#if beta}
12491
<TagBeta />
12592
{/if}
126-
{/if}
127-
</span>
128-
{#if collapsable && !collapsedSidebar}
129-
<button
130-
class="cursor-pointer"
131-
onclick={(e) => {
132-
e.stopPropagation()
133-
open = !open
134-
}}
135-
>
136-
<Icon src={open ? ChevronDown : ChevronRight} class="h-4 w-4 text-white-40" />
137-
</button>
138-
{/if}
139-
</button>
140-
{#if children?.length}
141-
{#if collapsedSidebar}
142-
{#if hovered}
143-
<div
144-
use:floatingContent
145-
role="contentinfo"
146-
onmouseenter={handleHover}
147-
onmouseleave={handleBlur}
148-
class="pt-4 z-30"
93+
</span>
94+
{#if collapsable}
95+
<button
96+
class="cursor-pointer"
97+
data-menu-item-chevron
98+
onclick={(e) => {
99+
e.stopPropagation()
100+
open = !open
101+
}}
149102
>
150-
<DrawerContext autofocus onclick={handleClickChild} {items} />
151-
</div>
103+
<Icon src={open ? ChevronDown : ChevronRight} class="h-4 w-4 text-white-40" />
104+
</button>
152105
{/if}
153-
{:else if open || !collapsable}
154-
<ul>
155-
{#each children as child}
156-
<li>
157-
<MenuItem {...child} isFolderItem {onclick} />
158-
</li>
159-
{/each}
160-
</ul>
106+
</button>
107+
{#if action}
108+
<span class="shrink-0" data-menu-item-action>
109+
{@render action()}
110+
</span>
161111
{/if}
112+
</div>
113+
{#if children?.length && (open || !collapsable)}
114+
<ul data-menu-item-children>
115+
{#each children as child}
116+
<li>
117+
<MenuItem {...child} isFolderItem {onclick} />
118+
</li>
119+
{/each}
120+
</ul>
162121
{/if}
163122
</div>

svelte/src/lib/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -625,11 +625,14 @@ export interface BaseDropdownProps {
625625
collapsable?: boolean;
626626
open?: boolean;
627627
active?: boolean;
628-
collapsedSidebar?: boolean;
629628
iconTheme?: IconTheme;
630629
icon?: IconSource | string | undefined;
630+
imageUrl?: string;
631631
beta?: boolean;
632632
children?: MenuItemProps[] | undefined;
633+
action?: Snippet;
634+
ref?: HTMLDivElement | null;
635+
class?: string;
633636
onclick?: (url: string) => void;
634637
}
635638

svelte/src/stories/MenuItem.stories.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { Meta, StoryObj } from '@storybook/sveltekit'
22

33
import MenuItem from '../lib/MenuItem.svelte'
4+
import MenuItemWithAction from './helpers/MenuItemWithAction.svelte'
45
import MarginDecorator from './decorartors/MarginDecorator.svelte'
56
import MaxWidthSmDecorator from './decorartors/MaxWidthSmDecorator.svelte'
7+
import MaxWidthXsDecorator from './decorartors/MaxWidthXsDecorator.svelte'
68
import DarkBackgroundDecorator from './decorartors/DarkBackgroundDecorator.svelte'
79
import { Cog6Tooth } from '@steeze-ui/heroicons'
810

@@ -84,3 +86,40 @@ export const CollapsableOpen: Story = {
8486
open: true
8587
}
8688
}
89+
90+
export const WithImage: Story = {
91+
decorators: [() => ({ Component: DarkBackgroundDecorator as any })],
92+
args: {
93+
label: 'Acme Invoicing',
94+
imageUrl: 'https://www.google.com/s2/favicons?domain=invopop.com&sz=32'
95+
}
96+
}
97+
98+
export const WithAction: Story = {
99+
decorators: [() => ({ Component: DarkBackgroundDecorator as any })],
100+
render: (args) => ({ Component: MenuItemWithAction as any, props: args }),
101+
args: {
102+
label: 'Item with action',
103+
icon: Cog6Tooth
104+
}
105+
}
106+
107+
export const Truncating: Story = {
108+
decorators: [
109+
() => ({ Component: DarkBackgroundDecorator as any }),
110+
() => ({ Component: MaxWidthXsDecorator as any })
111+
],
112+
args: {
113+
label: 'A very long menu item label that should truncate when the container is narrow',
114+
icon: Cog6Tooth
115+
}
116+
}
117+
118+
export const CustomClass: Story = {
119+
decorators: [() => ({ Component: DarkBackgroundDecorator as any })],
120+
args: {
121+
label: 'Custom class targeting label',
122+
icon: Cog6Tooth,
123+
class: '[&_[data-menu-item-label]]:italic [&_[data-menu-item-label]]:tracking-wider'
124+
}
125+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script lang="ts">
2+
import MenuItem from '$lib/MenuItem.svelte'
3+
import BaseButton from '$lib/BaseButton.svelte'
4+
import type { MenuItemProps } from '$lib/types.js'
5+
import { Pin } from '@invopop/ui-icons'
6+
7+
let props: MenuItemProps = $props()
8+
</script>
9+
10+
<MenuItem {...props}>
11+
{#snippet action()}
12+
<BaseButton variant="dark-ghost" size="sm" icon={Pin} title="Pin" />
13+
{/snippet}
14+
</MenuItem>

svelte/src/stories/molecules/MenuItemWithChildren.stories.ts

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type { Meta, StoryObj } from '@storybook/sveltekit'
33
import MenuItemWithChildren from './MenuItemWithChildren.svelte'
44
import MarginDecorator from '../decorartors/MarginDecorator.svelte'
55
import DarkBackgroundDecorator from '../decorartors/DarkBackgroundDecorator.svelte'
6-
import { Invoice } from '@invopop/ui-icons'
76

87
// More on how to set up stories at: https://storybook.js.org/docs/svelte/writing-stories/introduction
98
const meta = {
@@ -59,33 +58,3 @@ export const ChildActive: Story = {
5958
children: [{ label: 'Child Item' }, { label: 'Another Child Item', active: true }]
6059
}
6160
}
62-
63-
export const CollapsedSidebarWithChild: Story = {
64-
args: {
65-
icon: Invoice,
66-
collapsedSidebar: true,
67-
collapsable: true,
68-
open: true,
69-
url: 'parent-item',
70-
children: [
71-
{ label: 'Child Item', url: 'child-item' },
72-
{ label: 'Another Child Item', url: 'another-child-item', active: true }
73-
]
74-
}
75-
}
76-
77-
export const CollapsedSidebarParentSelected: Story = {
78-
decorators: [() => ({ Component: DarkBackgroundDecorator as any })],
79-
args: {
80-
icon: Invoice,
81-
collapsedSidebar: true,
82-
collapsable: true,
83-
open: true,
84-
url: 'parent-item',
85-
active: true,
86-
children: [
87-
{ label: 'Child Item', url: 'child-item' },
88-
{ label: 'Another Child Item', url: 'another-child-item' }
89-
]
90-
}
91-
}

0 commit comments

Comments
 (0)