Skip to content

Commit 2eabdd4

Browse files
authored
Add Floating UI examples in combination with the Menu component (#2612)
* add Floating UI example for React with Menu * add Floating UI example for Vue with Menu
1 parent 1739edb commit 2eabdd4

File tree

5 files changed

+988
-737
lines changed

5 files changed

+988
-737
lines changed

packages/playground-react/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,8 @@
2828
"react-dom": "^18.0.0",
2929
"react-flatpickr": "^3.10.9",
3030
"tailwindcss": "^3.2.7"
31+
},
32+
"devDependencies": {
33+
"@floating-ui/react": "^0.24.8"
3134
}
3235
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import React, { ReactNode, useState, useEffect } from 'react'
2+
import { createPortal } from 'react-dom'
3+
import { Menu } from '@headlessui/react'
4+
import { useFloating, offset } from '@floating-ui/react'
5+
6+
import { classNames } from '../../utils/class-names'
7+
import { Button } from '../../components/button'
8+
9+
export default function Home() {
10+
let { refs, floatingStyles } = useFloating({
11+
placement: 'bottom-end',
12+
strategy: 'fixed',
13+
middleware: [offset(10)],
14+
})
15+
16+
function resolveClass({ active, disabled }) {
17+
return classNames(
18+
'block w-full text-left px-4 py-2 text-sm leading-5 text-gray-700',
19+
active && 'bg-gray-100 text-gray-900',
20+
disabled && 'cursor-not-allowed opacity-50'
21+
)
22+
}
23+
24+
return (
25+
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
26+
<div className="mt-64 inline-block text-left">
27+
<Menu>
28+
<span className="inline-flex rounded-md shadow-sm">
29+
<Menu.Button ref={refs.setReference} as={Button}>
30+
<span>Options</span>
31+
<svg className="ml-2 -mr-1 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
32+
<path
33+
fillRule="evenodd"
34+
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"
35+
clipRule="evenodd"
36+
/>
37+
</svg>
38+
</Menu.Button>
39+
</span>
40+
41+
<Portal>
42+
<Menu.Items
43+
className="w-56 divide-y divide-gray-100 rounded-md border border-gray-200 bg-white shadow-lg outline-none"
44+
ref={refs.setFloating}
45+
style={floatingStyles}
46+
>
47+
<div className="px-4 py-3">
48+
<p className="text-sm leading-5">Signed in as</p>
49+
<p className="truncate text-sm font-medium leading-5 text-gray-900">
50+
51+
</p>
52+
</div>
53+
54+
<div className="py-1">
55+
<Menu.Item as="a" href="#account-settings" className={resolveClass}>
56+
Account settings
57+
</Menu.Item>
58+
<Menu.Item>
59+
{(data) => (
60+
<a href="#support" className={resolveClass(data)}>
61+
Support
62+
</a>
63+
)}
64+
</Menu.Item>
65+
<Menu.Item as="a" disabled href="#new-feature" className={resolveClass}>
66+
New feature (soon)
67+
</Menu.Item>
68+
<Menu.Item as="a" href="#license" className={resolveClass}>
69+
License
70+
</Menu.Item>
71+
</div>
72+
73+
<div className="py-1">
74+
<Menu.Item as="a" href="#sign-out" className={resolveClass}>
75+
Sign out
76+
</Menu.Item>
77+
</div>
78+
</Menu.Items>
79+
</Portal>
80+
</Menu>
81+
</div>
82+
</div>
83+
)
84+
}
85+
86+
function Portal(props: { children: ReactNode }) {
87+
let { children } = props
88+
let [mounted, setMounted] = useState(false)
89+
90+
useEffect(() => setMounted(true), [])
91+
92+
if (!mounted) return null
93+
return createPortal(children, document.body)
94+
}

packages/playground-vue/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"vue-router": "^4.0.0"
2929
},
3030
"devDependencies": {
31+
"@floating-ui/vue": "^1.0.2",
3132
"@vitejs/plugin-vue": "^3.0.3",
3233
"vite": "^3.0.0"
3334
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<template>
2+
<div class="flex h-full w-screen justify-center bg-gray-50 p-12">
3+
<div class="mt-64 inline-block text-left">
4+
<Menu>
5+
<span class="inline-flex rounded-md shadow-sm">
6+
<MenuButton
7+
ref="reference"
8+
class="focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none active:bg-gray-50 active:text-gray-800"
9+
>
10+
<span>Options</span>
11+
<svg class="ml-2 -mr-1 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
12+
<path
13+
fillRule="evenodd"
14+
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"
15+
clipRule="evenodd"
16+
/>
17+
</svg>
18+
</MenuButton>
19+
</span>
20+
21+
<teleport to="body">
22+
<MenuItems
23+
ref="floating"
24+
:style="floatingStyles"
25+
class="absolute right-0 w-56 origin-top-right divide-y divide-gray-100 rounded-md border border-gray-200 bg-white shadow-lg outline-none"
26+
>
27+
<div class="px-4 py-3">
28+
<p class="text-sm leading-5">Signed in as</p>
29+
<p class="truncate text-sm font-medium leading-5 text-gray-900">[email protected]</p>
30+
</div>
31+
32+
<div class="py-1">
33+
<MenuItem as="a" href="#account-settings" :className="resolveClass">
34+
Account settings
35+
</MenuItem>
36+
<MenuItem v-slot="data">
37+
<a href="#support" :class="resolveClass(data)">Support</a>
38+
</MenuItem>
39+
<MenuItem as="a" disabled href="#new-feature" :className="resolveClass">
40+
New feature (soon)
41+
</MenuItem>
42+
<MenuItem as="a" href="#license" :className="resolveClass">License</MenuItem>
43+
</div>
44+
<div class="py-1">
45+
<MenuItem as="a" href="#sign-out" :className="resolveClass">Sign out</MenuItem>
46+
</div>
47+
</MenuItems>
48+
</teleport>
49+
</Menu>
50+
</div>
51+
</div>
52+
</template>
53+
54+
<script>
55+
import { defineComponent, h, ref, onMounted, watchEffect, watch, computed } from 'vue'
56+
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
57+
import { useFloating, offset } from '@floating-ui/vue'
58+
59+
function classNames(...classes) {
60+
return classes.filter(Boolean).join(' ')
61+
}
62+
63+
export default {
64+
components: { Menu, MenuButton, MenuItems, MenuItem },
65+
setup(props, context) {
66+
let reference = ref(null)
67+
let floating = ref(null)
68+
69+
let { floatingStyles } = useFloating(
70+
computed(() => reference.value?.el),
71+
computed(() => floating.value?.el),
72+
{
73+
placement: 'bottom-end',
74+
strategy: 'fixed',
75+
middleware: [offset(10)],
76+
}
77+
)
78+
79+
function resolveClass({ active, disabled }) {
80+
return classNames(
81+
'flex justify-between w-full px-4 py-2 text-sm leading-5 text-left',
82+
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
83+
disabled && 'cursor-not-allowed opacity-50'
84+
)
85+
}
86+
87+
return {
88+
reference,
89+
floating,
90+
floatingStyles,
91+
resolveClass,
92+
}
93+
},
94+
}
95+
</script>

0 commit comments

Comments
 (0)