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
This commit is contained in:
@@ -28,5 +28,8 @@
|
||||
"react-dom": "^18.0.0",
|
||||
"react-flatpickr": "^3.10.9",
|
||||
"tailwindcss": "^3.2.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@floating-ui/react": "^0.24.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import React, { ReactNode, useState, useEffect } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Menu } from '@headlessui/react'
|
||||
import { useFloating, offset } from '@floating-ui/react'
|
||||
|
||||
import { classNames } from '../../utils/class-names'
|
||||
import { Button } from '../../components/button'
|
||||
|
||||
export default function Home() {
|
||||
let { refs, floatingStyles } = useFloating({
|
||||
placement: 'bottom-end',
|
||||
strategy: 'fixed',
|
||||
middleware: [offset(10)],
|
||||
})
|
||||
|
||||
function resolveClass({ active, disabled }) {
|
||||
return classNames(
|
||||
'block w-full text-left px-4 py-2 text-sm leading-5 text-gray-700',
|
||||
active && 'bg-gray-100 text-gray-900',
|
||||
disabled && 'cursor-not-allowed opacity-50'
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-screen justify-center bg-gray-50 p-12">
|
||||
<div className="mt-64 inline-block text-left">
|
||||
<Menu>
|
||||
<span className="inline-flex rounded-md shadow-sm">
|
||||
<Menu.Button ref={refs.setReference} as={Button}>
|
||||
<span>Options</span>
|
||||
<svg className="ml-2 -mr-1 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
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"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</Menu.Button>
|
||||
</span>
|
||||
|
||||
<Portal>
|
||||
<Menu.Items
|
||||
className="w-56 divide-y divide-gray-100 rounded-md border border-gray-200 bg-white shadow-lg outline-none"
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
>
|
||||
<div className="px-4 py-3">
|
||||
<p className="text-sm leading-5">Signed in as</p>
|
||||
<p className="truncate text-sm font-medium leading-5 text-gray-900">
|
||||
tom@example.com
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="py-1">
|
||||
<Menu.Item as="a" href="#account-settings" className={resolveClass}>
|
||||
Account settings
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{(data) => (
|
||||
<a href="#support" className={resolveClass(data)}>
|
||||
Support
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item as="a" disabled href="#new-feature" className={resolveClass}>
|
||||
New feature (soon)
|
||||
</Menu.Item>
|
||||
<Menu.Item as="a" href="#license" className={resolveClass}>
|
||||
License
|
||||
</Menu.Item>
|
||||
</div>
|
||||
|
||||
<div className="py-1">
|
||||
<Menu.Item as="a" href="#sign-out" className={resolveClass}>
|
||||
Sign out
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Portal>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Portal(props: { children: ReactNode }) {
|
||||
let { children } = props
|
||||
let [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => setMounted(true), [])
|
||||
|
||||
if (!mounted) return null
|
||||
return createPortal(children, document.body)
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
"vue-router": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@floating-ui/vue": "^1.0.2",
|
||||
"@vitejs/plugin-vue": "^3.0.3",
|
||||
"vite": "^3.0.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="flex h-full w-screen justify-center bg-gray-50 p-12">
|
||||
<div class="mt-64 inline-block text-left">
|
||||
<Menu>
|
||||
<span class="inline-flex rounded-md shadow-sm">
|
||||
<MenuButton
|
||||
ref="reference"
|
||||
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"
|
||||
>
|
||||
<span>Options</span>
|
||||
<svg class="ml-2 -mr-1 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
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"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</MenuButton>
|
||||
</span>
|
||||
|
||||
<teleport to="body">
|
||||
<MenuItems
|
||||
ref="floating"
|
||||
:style="floatingStyles"
|
||||
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"
|
||||
>
|
||||
<div class="px-4 py-3">
|
||||
<p class="text-sm leading-5">Signed in as</p>
|
||||
<p class="truncate text-sm font-medium leading-5 text-gray-900">tom@example.com</p>
|
||||
</div>
|
||||
|
||||
<div class="py-1">
|
||||
<MenuItem as="a" href="#account-settings" :className="resolveClass">
|
||||
Account settings
|
||||
</MenuItem>
|
||||
<MenuItem v-slot="data">
|
||||
<a href="#support" :class="resolveClass(data)">Support</a>
|
||||
</MenuItem>
|
||||
<MenuItem as="a" disabled href="#new-feature" :className="resolveClass">
|
||||
New feature (soon)
|
||||
</MenuItem>
|
||||
<MenuItem as="a" href="#license" :className="resolveClass">License</MenuItem>
|
||||
</div>
|
||||
<div class="py-1">
|
||||
<MenuItem as="a" href="#sign-out" :className="resolveClass">Sign out</MenuItem>
|
||||
</div>
|
||||
</MenuItems>
|
||||
</teleport>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, h, ref, onMounted, watchEffect, watch, computed } from 'vue'
|
||||
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
|
||||
import { useFloating, offset } from '@floating-ui/vue'
|
||||
|
||||
function classNames(...classes) {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { Menu, MenuButton, MenuItems, MenuItem },
|
||||
setup(props, context) {
|
||||
let reference = ref(null)
|
||||
let floating = ref(null)
|
||||
|
||||
let { floatingStyles } = useFloating(
|
||||
computed(() => reference.value?.el),
|
||||
computed(() => floating.value?.el),
|
||||
{
|
||||
placement: 'bottom-end',
|
||||
strategy: 'fixed',
|
||||
middleware: [offset(10)],
|
||||
}
|
||||
)
|
||||
|
||||
function resolveClass({ active, disabled }) {
|
||||
return classNames(
|
||||
'flex justify-between w-full px-4 py-2 text-sm leading-5 text-left',
|
||||
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
|
||||
disabled && 'cursor-not-allowed opacity-50'
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
reference,
|
||||
floating,
|
||||
floatingStyles,
|
||||
resolveClass,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user