Skip to content

Commit 1f29594

Browse files
committed
add Menu examples in React
1 parent 8cc424c commit 1f29594

File tree

5 files changed

+347
-70
lines changed

5 files changed

+347
-70
lines changed

packages/@headlessui-react/pages/menu/menu-with-popper.tsx

Lines changed: 20 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import * as React from 'react'
22
import * as ReactDOM from 'react-dom'
33
import Head from 'next/head'
44
import Link from 'next/link'
5-
import { createPopper, Options } from '@popperjs/core'
65
import { Menu } from '@headlessui/react'
76

87
import { classNames } from '../../src/utils/class-names'
98
import { PropsOf } from '../../src/types'
9+
import { usePopper } from '../../playground-utils/hooks/use-popper'
1010

1111
export default function Home() {
1212
return (
@@ -22,41 +22,6 @@ export default function Home() {
2222
)
2323
}
2424

25-
/**
26-
* Example implementation to use Popper: https://popper.js.org/
27-
*/
28-
function usePopper(
29-
options?: Partial<Options>
30-
): [React.RefCallback<Element | null>, React.RefCallback<HTMLElement | null>] {
31-
const reference = React.useRef<Element>(null)
32-
const popper = React.useRef<HTMLElement>(null)
33-
34-
const cleanupCallback = React.useRef(() => {})
35-
36-
const instantiatePopper = React.useCallback(() => {
37-
if (!reference.current) return
38-
if (!popper.current) return
39-
40-
if (cleanupCallback.current) cleanupCallback.current()
41-
42-
cleanupCallback.current = createPopper(reference.current, popper.current, options).destroy
43-
}, [reference, popper, cleanupCallback, options])
44-
45-
return React.useMemo(
46-
() => [
47-
referenceDomNode => {
48-
reference.current = referenceDomNode
49-
instantiatePopper()
50-
},
51-
popperDomNode => {
52-
popper.current = popperDomNode
53-
instantiatePopper()
54-
},
55-
],
56-
[reference, popper, instantiatePopper]
57-
)
58-
}
59-
6025
function Portal(props: { children: React.ReactNode }) {
6126
const { children } = props
6227
const [mounted, setMounted] = React.useState(false)
@@ -74,6 +39,14 @@ function Dropdown() {
7439
modifiers: [{ name: 'offset', options: { offset: [0, 10] } }],
7540
})
7641

42+
function resolveClass({ active, disabled }) {
43+
return classNames(
44+
'block w-full text-left px-4 py-2 text-sm leading-5 text-gray-700',
45+
active && 'bg-gray-100 text-gray-900',
46+
disabled && 'cursor-not-allowed opacity-50'
47+
)
48+
}
49+
7750
return (
7851
<div className="inline-block mt-64 text-left">
7952
<Menu>
@@ -95,12 +68,6 @@ function Dropdown() {
9568

9669
<Portal>
9770
<Menu.Items
98-
enter="transition-opacity ease-out duration-100"
99-
enterFrom="transform opacity-0 scale-95"
100-
enterTo="transform opacity-100 scale-100"
101-
leave="transition-opacity ease-in duration-75"
102-
leaveFrom="transform opacity-100 scale-100"
103-
leaveTo="transform opacity-0 scale-95"
10471
className="w-56 bg-white border border-gray-200 divide-y divide-gray-100 rounded-md shadow-lg outline-none"
10572
ref={container}
10673
>
@@ -112,18 +79,22 @@ function Dropdown() {
11279
</div>
11380

11481
<div className="py-1">
115-
<Item href="#account-settings">Account settings</Item>
116-
<Item as={NextLink} href="#support">
82+
<Menu.Item as="a" href="#account-settings" className={resolveClass}>
83+
Account settings
84+
</Menu.Item>
85+
<Menu.Item as={NextLink} href="#support" className={resolveClass}>
11786
Support
118-
</Item>
119-
<Item href="#new-feature" disabled>
87+
</Menu.Item>
88+
<Menu.Item as="a" href="#new-feature" disabled className={resolveClass}>
12089
New feature (soon)
121-
</Item>
122-
<Item href="#license">License</Item>
90+
</Menu.Item>
91+
<Menu.Item as="a" href="#license" className={resolveClass}>
92+
License
93+
</Menu.Item>
12394
</div>
12495

12596
<div className="py-1">
126-
<Item as={SignOutButton} />
97+
<Menu.Item as={SignOutButton} className={resolveClass} />
12798
</div>
12899
</Menu.Items>
129100
</Portal>
@@ -158,19 +129,3 @@ function SignOutButton(props) {
158129
</form>
159130
)
160131
}
161-
162-
function Item(props: PropsOf<typeof Menu.Item>) {
163-
return (
164-
<Menu.Item
165-
as="a"
166-
className={({ active, disabled }) =>
167-
classNames(
168-
'block w-full text-left px-4 py-2 text-sm leading-5 text-gray-700',
169-
active && 'bg-gray-100 text-gray-900',
170-
disabled && 'cursor-not-allowed opacity-50'
171-
)
172-
}
173-
{...props}
174-
/>
175-
)
176-
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import * as React from 'react'
2+
import Head from 'next/head'
3+
import Link from 'next/link'
4+
import { Menu } from '@headlessui/react'
5+
6+
import { classNames } from '../../src/utils/class-names'
7+
import { PropsOf } from '../../src/types'
8+
import { usePopper } from '../../playground-utils/hooks/use-popper'
9+
10+
export default function Home() {
11+
return (
12+
<>
13+
<Head>
14+
<title>Menu with pure Tailwind- Playground</title>
15+
</Head>
16+
17+
<div className="flex justify-center w-screen h-full p-12 bg-gray-50">
18+
<Dropdown />
19+
</div>
20+
</>
21+
)
22+
}
23+
24+
function Dropdown() {
25+
const [trigger, container] = usePopper({
26+
placement: 'bottom-end',
27+
strategy: 'fixed',
28+
modifiers: [{ name: 'offset', options: { offset: [0, 10] } }],
29+
})
30+
31+
return (
32+
<div className="relative inline-block mt-64 text-left">
33+
<Menu>
34+
<span className="rounded-md shadow-sm">
35+
<Menu.Button
36+
ref={trigger}
37+
className="inline-flex justify-center w-full px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition duration-150 ease-in-out bg-white border border-gray-300 rounded-md hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800"
38+
>
39+
{({ open }) => (
40+
<>
41+
<span>Options</span>
42+
<svg
43+
className={classNames(
44+
'w-5 h-5 ml-2 -mr-1 transition-transform duration-150',
45+
!open && 'transform -rotate-90'
46+
)}
47+
viewBox="0 0 20 20"
48+
fill="currentColor"
49+
>
50+
<path
51+
fillRule="evenodd"
52+
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
53+
clipRule="evenodd"
54+
/>
55+
</svg>
56+
</>
57+
)}
58+
</Menu.Button>
59+
</span>
60+
61+
<div ref={container} className="relative w-56">
62+
<Menu.Items
63+
enter="transition ease-out duration-100"
64+
enterFrom="transform opacity-0 scale-95"
65+
enterTo="transform opacity-100 scale-100"
66+
leave="transition ease-in duration-75"
67+
leaveFrom="transform opacity-100 scale-100"
68+
leaveTo="transform opacity-0 scale-95"
69+
className="bg-white border border-gray-200 divide-y divide-gray-100 rounded-md shadow-lg outline-none"
70+
>
71+
<div className="px-4 py-3">
72+
<p className="text-sm leading-5">Signed in as</p>
73+
<p className="text-sm font-medium leading-5 text-gray-900 truncate">
74+
75+
</p>
76+
</div>
77+
78+
<div className="py-1">
79+
<Item href="#account-settings">Account settings</Item>
80+
<Item as={NextLink} href="#support">
81+
Support
82+
</Item>
83+
<Item href="#new-feature" disabled>
84+
New feature (soon)
85+
</Item>
86+
<Item href="#license">License</Item>
87+
</div>
88+
89+
<div className="py-1">
90+
<SignOutButton />
91+
</div>
92+
</Menu.Items>
93+
</div>
94+
</Menu>
95+
</div>
96+
)
97+
}
98+
99+
function NextLink(props: PropsOf<'a'>) {
100+
const { href, children, ...rest } = props
101+
return (
102+
<Link href={href}>
103+
<a {...rest}>{children}</a>
104+
</Link>
105+
)
106+
}
107+
108+
function SignOutButton() {
109+
return (
110+
<Menu.Item>
111+
{props => {
112+
const { active, disabled } = props
113+
return (
114+
<form
115+
method="POST"
116+
action="#"
117+
onSubmit={e => {
118+
e.preventDefault()
119+
alert('SIGNED OUT')
120+
}}
121+
>
122+
<button
123+
type="submit"
124+
className={classNames(
125+
'w-full',
126+
'flex justify-between w-full text-left px-4 py-2 text-sm leading-5',
127+
active ? 'bg-indigo-500 text-white' : 'text-gray-700',
128+
disabled && 'cursor-not-allowed opacity-50'
129+
)}
130+
>
131+
<span className={classNames(active && 'font-bold')}>Sign out</span>
132+
<kbd className={classNames('font-sans', active && 'text-indigo-50')}>⌘K</kbd>
133+
</button>
134+
</form>
135+
)
136+
}}
137+
</Menu.Item>
138+
)
139+
}
140+
141+
function Item({ children, as = 'a', ...props }: PropsOf<typeof Menu.Item>) {
142+
return (
143+
<Menu.Item
144+
className={({ active, disabled }) => {
145+
return classNames(
146+
'flex justify-between w-full text-left px-4 py-2 text-sm leading-5',
147+
active ? 'bg-indigo-500 text-white' : 'text-gray-700',
148+
disabled && 'cursor-not-allowed opacity-50'
149+
)
150+
}}
151+
as={as}
152+
{...props}
153+
>
154+
{({ active }) => (
155+
<>
156+
<span className={classNames(active && 'font-bold')}>{children}</span>
157+
<kbd className={classNames('font-sans', active && 'text-indigo-50')}>⌘K</kbd>
158+
</>
159+
)}
160+
</Menu.Item>
161+
)
162+
}

packages/@headlessui-react/pages/menu/menu-with-pure-tailwind.tsx renamed to packages/@headlessui-react/pages/menu/menu-with-transition.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ function Dropdown() {
2727
<span className="rounded-md shadow-sm">
2828
<Menu.Button className="inline-flex justify-center w-full px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition duration-150 ease-in-out bg-white border border-gray-300 rounded-md hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800">
2929
{({ open }) => (
30-
<button>
30+
<>
3131
<span>Options</span>
3232
<svg
3333
className={classNames(
@@ -43,7 +43,7 @@ function Dropdown() {
4343
clipRule="evenodd"
4444
/>
4545
</svg>
46-
</button>
46+
</>
4747
)}
4848
</Menu.Button>
4949
</span>
@@ -124,7 +124,7 @@ function SignOutButton() {
124124
)
125125
}
126126

127-
function Item({ children, href, ...props }: PropsOf<typeof Menu.Item>) {
127+
function Item({ children, as = 'a', ...props }: PropsOf<typeof Menu.Item>) {
128128
return (
129129
<Menu.Item
130130
className={({ active, disabled }) => {
@@ -134,13 +134,14 @@ function Item({ children, href, ...props }: PropsOf<typeof Menu.Item>) {
134134
disabled && 'cursor-not-allowed opacity-50'
135135
)
136136
}}
137+
as={as}
137138
{...props}
138139
>
139140
{({ active }) => (
140-
<a href={href}>
141+
<>
141142
<span className={classNames(active && 'font-bold')}>{children}</span>
142143
<kbd className={classNames('font-sans', active && 'text-indigo-50')}>⌘K</kbd>
143-
</a>
144+
</>
144145
)}
145146
</Menu.Item>
146147
)

0 commit comments

Comments
 (0)