Skip to content

Commit ac920b3

Browse files
committed
extract DropdownMenu and convert TopBarPicker
1 parent 0ab5627 commit ac920b3

File tree

3 files changed

+139
-68
lines changed

3 files changed

+139
-68
lines changed

app/components/TopBar.tsx

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,15 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
98
import cn from 'classnames'
109
import React from 'react'
11-
import { Link } from 'react-router-dom'
1210

1311
import { navToLogin, useApiMutation } from '@oxide/api'
1412
import { DirectionDownIcon, Profile16Icon } from '@oxide/design-system/icons/react'
1513

1614
import { useCurrentUser } from '~/layouts/AuthenticatedLayout'
1715
import { buttonStyle } from '~/ui/lib/Button'
16+
import * as DropdownMenu from '~/ui/lib/DropdownMenu2'
1817
import { pb } from '~/util/path-builder'
1918

2019
export function TopBar({ children }: { children: React.ReactNode }) {
@@ -42,11 +41,11 @@ export function TopBar({ children }: { children: React.ReactNode }) {
4241
<div className="mx-3 flex h-[60px] shrink-0 items-center justify-between">
4342
<div className="flex items-center">{otherPickers}</div>
4443
<div className="flex items-center gap-2">
45-
<Menu>
46-
<MenuButton
44+
<DropdownMenu.Root>
45+
<DropdownMenu.Trigger
4746
className={cn(
48-
'flex items-center gap-2',
49-
buttonStyle({ size: 'sm', variant: 'secondary' })
47+
buttonStyle({ size: 'sm', variant: 'secondary' }),
48+
'flex items-center gap-2'
5049
)}
5150
aria-label="User menu"
5251
>
@@ -55,29 +54,15 @@ export function TopBar({ children }: { children: React.ReactNode }) {
5554
{me.displayName || 'User'}
5655
</span>
5756
<DirectionDownIcon className="!w-2.5" />
58-
</MenuButton>
57+
</DropdownMenu.Trigger>
5958
{/* TODO: fix hover style + should be able to click anywhere in the menu item */}
60-
<MenuItems
61-
anchor="bottom end"
62-
className="DropdownMenuContent [--anchor-gap:8px]"
63-
>
64-
{/* TODO: extract Item and LinkItem components*/}
65-
<MenuItem>
66-
<Link className="DropdownMenuItem ox-menu-item" to={pb.profile()}>
67-
Settings
68-
</Link>
69-
</MenuItem>
70-
<MenuItem>
71-
<button
72-
type="button"
73-
onClick={() => logout.mutate({})}
74-
className="DropdownMenuItem ox-menu-item"
75-
>
76-
Sign out
77-
</button>
78-
</MenuItem>
79-
</MenuItems>
80-
</Menu>
59+
<DropdownMenu.Content gap={8}>
60+
<DropdownMenu.LinkItem to={pb.profile()}>Settings</DropdownMenu.LinkItem>
61+
<DropdownMenu.Item onSelect={() => logout.mutate({})}>
62+
Sign out
63+
</DropdownMenu.Item>
64+
</DropdownMenu.Content>
65+
</DropdownMenu.Root>
8166
</div>
8267
</div>
8368
</div>

app/components/TopBarPicker.tsx

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import {
2424
} from '~/hooks/use-params'
2525
import { useCurrentUser } from '~/layouts/AuthenticatedLayout'
2626
import { PAGE_SIZE } from '~/table/QueryTable'
27-
import { Button } from '~/ui/lib/Button'
28-
import { DropdownMenu } from '~/ui/lib/DropdownMenu'
27+
import { buttonStyle } from '~/ui/lib/Button'
28+
import * as DropdownMenu from '~/ui/lib/DropdownMenu2'
2929
import { Identicon } from '~/ui/lib/Identicon'
3030
import { Wrap } from '~/ui/util/wrap'
3131
import { pb } from '~/util/path-builder'
@@ -95,53 +95,51 @@ const TopBarPicker = (props: TopBarPickerProps) => {
9595
{props.items && (
9696
<div className="ml-2 shrink-0">
9797
<DropdownMenu.Trigger
98-
className="group"
98+
className={cn(
99+
'group h-[2rem] w-[1.125rem]',
100+
buttonStyle({ size: 'icon', variant: 'ghost' })
101+
)}
99102
aria-label={props['aria-label']}
100-
asChild
101103
>
102-
<Button size="icon" variant="ghost" className="h-[2rem] w-[1.125rem]">
103-
{/* aria-hidden is a tip from the Reach docs */}
104-
<SelectArrows6Icon className="text-secondary" aria-hidden />
105-
</Button>
104+
{/* aria-hidden is a tip from the Reach docs */}
105+
<SelectArrows6Icon className="text-secondary" aria-hidden />
106106
</DropdownMenu.Trigger>
107107
</div>
108108
)}
109109
</div>
110110
{/* TODO: item size and focus highlight */}
111111
{/* TODO: popover position should be further right */}
112112
{props.items && (
113-
// portal is necessary to avoid the menu popover getting its own after:
114-
// separator thing
115-
<DropdownMenu.Portal>
116-
<DropdownMenu.Content
117-
className="mt-2 max-h-80 min-w-[12.8125rem] overflow-y-auto"
118-
align="start"
119-
>
120-
{props.items.length > 0 ? (
121-
props.items.map(({ label, to }) => {
122-
const isSelected = props.current === label
123-
return (
124-
<DropdownMenu.Item asChild key={label}>
125-
<Link to={to} className={cn({ 'is-selected': isSelected })}>
126-
<span className="flex w-full items-center gap-2">
127-
{label}
128-
{isSelected && <Success12Icon className="-mr-3 block" />}
129-
</span>
130-
</Link>
131-
</DropdownMenu.Item>
132-
)
133-
})
134-
) : (
135-
<DropdownMenu.Item
136-
className="!pr-3 !text-center !text-secondary hover:cursor-default"
137-
onSelect={() => {}}
138-
disabled
139-
>
140-
{props.noItemsText || 'No items found'}
141-
</DropdownMenu.Item>
142-
)}
143-
</DropdownMenu.Content>
144-
</DropdownMenu.Portal>
113+
<DropdownMenu.Content
114+
className="mt-2 max-h-80 min-w-[12.8125rem] overflow-y-auto"
115+
anchor="bottom start"
116+
>
117+
{props.items.length > 0 ? (
118+
props.items.map(({ label, to }) => {
119+
const isSelected = props.current === label
120+
return (
121+
<DropdownMenu.LinkItem
122+
key={label}
123+
to={to}
124+
className={cn({ 'is-selected': isSelected })}
125+
>
126+
<span className="flex w-full items-center gap-2">
127+
{label}
128+
{isSelected && <Success12Icon className="-mr-3 block" />}
129+
</span>
130+
</DropdownMenu.LinkItem>
131+
)
132+
})
133+
) : (
134+
<DropdownMenu.Item
135+
className="!pr-3 !text-center !text-secondary hover:cursor-default"
136+
onSelect={() => {}}
137+
disabled
138+
>
139+
{props.noItemsText || 'No items found'}
140+
</DropdownMenu.Item>
141+
)}
142+
</DropdownMenu.Content>
145143
)}
146144
</DropdownMenu.Root>
147145
)

app/ui/lib/DropdownMenu2.tsx

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
5+
*
6+
* Copyright Oxide Computer Company
7+
*/
8+
9+
import {
10+
Menu,
11+
MenuButton,
12+
MenuItem,
13+
MenuItems,
14+
type MenuItemsProps,
15+
} from '@headlessui/react'
16+
import cn from 'classnames'
17+
import { forwardRef, type ForwardedRef, type ReactNode } from 'react'
18+
import { Link } from 'react-router-dom'
19+
20+
export const Root = Menu
21+
22+
export const Trigger = MenuButton
23+
24+
type ContentProps = {
25+
className?: string
26+
children: ReactNode
27+
anchor?: MenuItemsProps['anchor']
28+
portal?: boolean
29+
/** Spacing in px, passed as --anchor-gap */
30+
gap?: number
31+
}
32+
33+
export function Content({
34+
className,
35+
children,
36+
portal,
37+
anchor = 'bottom end',
38+
gap,
39+
}: ContentProps) {
40+
return (
41+
<MenuItems
42+
anchor={anchor}
43+
className={cn('DropdownMenuContent', gap && `[--anchor-gap:${gap}px]`, className)}
44+
// necessary to turn off scroll locking so the scrollbar doesn't pop in
45+
// and out as menu closes and opens
46+
modal={false}
47+
portal={portal}
48+
>
49+
{children}
50+
</MenuItems>
51+
)
52+
}
53+
54+
type LinkItemProps = { className?: string; to: string; children: ReactNode }
55+
56+
export function LinkItem({ className, to, children }: LinkItemProps) {
57+
return (
58+
<MenuItem>
59+
<Link className={cn('DropdownMenuItem ox-menu-item', className)} to={to}>
60+
{children}
61+
</Link>
62+
</MenuItem>
63+
)
64+
}
65+
66+
type ButtonRef = ForwardedRef<HTMLButtonElement>
67+
type ItemProps = {
68+
className?: string
69+
onSelect?: () => void
70+
children: ReactNode
71+
disabled?: boolean
72+
}
73+
74+
// need to forward ref because of tooltips on disabled menu buttons
75+
export const Item = forwardRef(
76+
({ className, onSelect, children, disabled }: ItemProps, ref: ButtonRef) => (
77+
<MenuItem disabled={disabled}>
78+
<button
79+
type="button"
80+
className={cn('DropdownMenuItem ox-menu-item', className)}
81+
ref={ref}
82+
onClick={onSelect}
83+
>
84+
{children}
85+
</button>
86+
</MenuItem>
87+
)
88+
)

0 commit comments

Comments
 (0)