├── packages ├── @headlessui-vue │ ├── examples │ │ ├── src │ │ │ ├── .generated │ │ │ │ └── .gitignore │ │ │ ├── main.js │ │ │ ├── router.js │ │ │ ├── components │ │ │ │ ├── focus-trap │ │ │ │ │ └── focus-trap.vue │ │ │ │ ├── portal │ │ │ │ │ └── portal.vue │ │ │ │ ├── disclosure │ │ │ │ │ └── disclosure.vue │ │ │ │ ├── Home.vue │ │ │ │ ├── switch │ │ │ │ │ └── switch.vue │ │ │ │ ├── menu │ │ │ │ │ ├── menu.vue │ │ │ │ │ ├── menu-with-transition.vue │ │ │ │ │ ├── menu-with-popper.vue │ │ │ │ │ └── menu-with-transition-and-popper.vue │ │ │ │ ├── radio-group │ │ │ │ │ └── radio-group.vue │ │ │ │ └── listbox │ │ │ │ │ └── listbox.vue │ │ │ ├── playground-utils │ │ │ │ └── hooks │ │ │ │ │ └── use-popper.js │ │ │ ├── KeyCaster.vue │ │ │ └── routes.json │ │ ├── .gitignore │ │ ├── public │ │ │ └── favicon.ico │ │ └── index.html │ ├── tsconfig.tsdx.json │ ├── postcss.config.js │ ├── tailwind.config.js │ ├── vercel.json │ ├── jest.config.js │ ├── src │ │ ├── hooks │ │ │ ├── use-id.ts │ │ │ ├── __mocks__ │ │ │ │ └── use-id.ts │ │ │ ├── use-window-event.ts │ │ │ ├── use-tree-walker.ts │ │ │ ├── use-focus-trap.ts │ │ │ └── use-inert-others.ts │ │ ├── test-utils │ │ │ ├── html.ts │ │ │ ├── report-dom-node-changes.ts │ │ │ ├── suppress-console-logs.ts │ │ │ └── vue-testing-library.ts │ │ ├── internal │ │ │ ├── dom-containers.ts │ │ │ ├── open-closed.ts │ │ │ ├── portal-force-root.ts │ │ │ └── stack-context.ts │ │ ├── utils │ │ │ ├── once.ts │ │ │ ├── resolve-prop-value.ts │ │ │ ├── dom.ts │ │ │ ├── match.ts │ │ │ ├── disposables.ts │ │ │ ├── calculate-active-index.ts │ │ │ ├── render.test.ts │ │ │ └── render.ts │ │ ├── index.ts │ │ ├── keyboard.ts │ │ ├── index.test.ts │ │ └── components │ │ │ ├── focus-trap │ │ │ └── focus-trap.ts │ │ │ ├── description │ │ │ └── description.ts │ │ │ ├── label │ │ │ └── label.ts │ │ │ ├── transitions │ │ │ └── utils │ │ │ │ └── transition.ts │ │ │ ├── portal │ │ │ └── portal.ts │ │ │ └── switch │ │ │ └── switch.ts │ ├── .eslintrc.js │ ├── tsdx.config.js │ ├── tsconfig.json │ ├── README.md │ ├── package.json │ └── vite.config.js └── @headlessui-react │ ├── postcss.config.js │ ├── tailwind.config.js │ ├── next-env.d.ts │ ├── next.config.js │ ├── vercel.json │ ├── tsconfig.tsdx.json │ ├── jest.config.js │ ├── src │ ├── utils │ │ ├── class-names.ts │ │ ├── once.ts │ │ ├── match.ts │ │ ├── disposables.ts │ │ ├── bugs.ts │ │ └── calculate-active-index.ts │ ├── hooks │ │ ├── use-iso-morphic-effect.ts │ │ ├── use-is-initial-render.ts │ │ ├── __mocks__ │ │ │ └── use-id.ts │ │ ├── use-is-mounted.ts │ │ ├── use-disposables.ts │ │ ├── use-computed.ts │ │ ├── use-flags.ts │ │ ├── use-sync-refs.ts │ │ ├── use-server-handoff-complete.ts │ │ ├── use-window-event.ts │ │ ├── use-id.ts │ │ ├── use-tree-walker.ts │ │ └── use-inert-others.ts │ ├── test-utils │ │ ├── report-dom-node-changes.ts │ │ └── suppress-console-logs.ts │ ├── index.ts │ ├── components │ │ ├── keyboard.ts │ │ ├── focus-trap │ │ │ └── focus-trap.tsx │ │ ├── label │ │ │ ├── label.test.tsx │ │ │ └── label.tsx │ │ ├── description │ │ │ ├── description.test.tsx │ │ │ └── description.tsx │ │ ├── transitions │ │ │ └── utils │ │ │ │ └── transition.ts │ │ ├── portal │ │ │ └── portal.tsx │ │ └── switch │ │ │ └── switch.tsx │ ├── index.test.ts │ ├── internal │ │ ├── portal-force-root.tsx │ │ ├── open-closed.tsx │ │ └── stack-context.tsx │ └── types.ts │ ├── tsconfig.json │ ├── pages │ ├── disclosure │ │ └── disclosure.tsx │ ├── switch │ │ └── switch-with-pure-tailwind.tsx │ ├── transitions │ │ └── component-examples │ │ │ ├── peek-a-boo.tsx │ │ │ ├── nested │ │ │ ├── hidden.tsx │ │ │ └── unmount.tsx │ │ │ └── dropdown.tsx │ ├── _error.tsx │ ├── menu │ │ ├── menu.tsx │ │ ├── multiple-elements.tsx │ │ ├── menu-with-transition.tsx │ │ ├── menu-with-popper.tsx │ │ ├── menu-with-transition-and-popper.tsx │ │ └── menu-with-framer-motion.tsx │ ├── radio-group │ │ └── radio-group.tsx │ └── popover │ │ └── popover.tsx │ ├── playground-utils │ ├── hooks │ │ └── use-popper.ts │ └── resolve-all-examples.ts │ ├── README.md │ └── package.json ├── .eslintignore ├── jest.config.js ├── postcss.config.js ├── scripts ├── build.sh ├── watch.sh ├── test.sh └── lint.sh ├── jest ├── create-jest-config.js └── custom-matchers.ts ├── tailwind.config.js ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── 1.bug_report.yml └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── package.json └── README.md /packages/@headlessui-vue/examples/src/.generated/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /packages/**/dist 3 | /node_modules 4 | /packages/**/node_modules -------------------------------------------------------------------------------- /packages/@headlessui-vue/examples/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | *.local -------------------------------------------------------------------------------- /packages/@headlessui-vue/tsconfig.tsdx.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../postcss.config.js') 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | projects: ['/packages/*/jest.config.js'], 3 | } 4 | -------------------------------------------------------------------------------- /packages/@headlessui-react/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../postcss.config.js') 2 | -------------------------------------------------------------------------------- /packages/@headlessui-react/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../tailwind.config.js') 2 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../tailwind.config.js') 2 | -------------------------------------------------------------------------------- /packages/@headlessui-react/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] 3 | } 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/@headlessui-react/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devIndicators: { 3 | autoPrerender: false, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /packages/@headlessui-react/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [{ "handle": "filesystem" }, { "src": "/(.*)", "status": 404, "dest": "/404.html" }] 3 | } 4 | -------------------------------------------------------------------------------- /packages/@headlessui-react/tsconfig.tsdx.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | 4 | "compilerOptions": { 5 | "jsx": "react" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/examples/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasa8x/headlessui/main/packages/@headlessui-vue/examples/public/favicon.ico -------------------------------------------------------------------------------- /packages/@headlessui-vue/jest.config.js: -------------------------------------------------------------------------------- 1 | const create = require('../../jest/create-jest-config.js') 2 | 3 | module.exports = create(__dirname, { displayName: ' Vue ' }) 4 | -------------------------------------------------------------------------------- /packages/@headlessui-react/jest.config.js: -------------------------------------------------------------------------------- 1 | const create = require('../../jest/create-jest-config.js') 2 | 3 | module.exports = create(__dirname, { displayName: 'React' }) 4 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/hooks/use-id.ts: -------------------------------------------------------------------------------- 1 | let id = 0 2 | function generateId() { 3 | return ++id 4 | } 5 | 6 | export function useId() { 7 | return generateId() 8 | } 9 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'react-hooks/rules-of-hooks': 'off', 4 | 'react-hooks/exhaustive-deps': 'off', 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/utils/class-names.ts: -------------------------------------------------------------------------------- 1 | export function classNames(...classes: (false | null | undefined | string)[]): string { 2 | return classes.filter(Boolean).join(' ') 3 | } 4 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/hooks/use-iso-morphic-effect.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useEffect } from 'react' 2 | 3 | export const useIsoMorphicEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect 4 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/test-utils/html.ts: -------------------------------------------------------------------------------- 1 | export function jsx(templates: TemplateStringsArray) { 2 | return templates.join('') 3 | } 4 | 5 | export function html(templates: TemplateStringsArray) { 6 | return templates.join('') 7 | } 8 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/hooks/__mocks__/use-id.ts: -------------------------------------------------------------------------------- 1 | beforeEach(() => { 2 | id = 0 3 | }) 4 | 5 | let id = 0 6 | function generateId() { 7 | return ++id 8 | } 9 | 10 | export function useId() { 11 | return generateId() 12 | } 13 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/tsdx.config.js: -------------------------------------------------------------------------------- 1 | const globals = { 2 | vue: 'Vue', 3 | } 4 | 5 | module.exports = { 6 | rollup(config) { 7 | for (let key in globals) config.output.globals[key] = globals[key] 8 | return config 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/examples/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | 5 | import 'tailwindcss/tailwind.css' 6 | 7 | createApp(App) 8 | .use(router) 9 | .mount('#app') 10 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/internal/dom-containers.ts: -------------------------------------------------------------------------------- 1 | export function contains(containers: Set, element: HTMLElement) { 2 | for (let container of containers) { 3 | if (container.contains(element)) return true 4 | } 5 | 6 | return false 7 | } 8 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/utils/once.ts: -------------------------------------------------------------------------------- 1 | export function once(cb: (...args: T[]) => void) { 2 | let state = { called: false } 3 | 4 | return (...args: T[]) => { 5 | if (state.called) return 6 | state.called = true 7 | return cb(...args) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/utils/once.ts: -------------------------------------------------------------------------------- 1 | export function once(cb: (...args: T[]) => void) { 2 | let state = { called: false } 3 | 4 | return (...args: T[]) => { 5 | if (state.called) return 6 | state.called = true 7 | return cb(...args) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/utils/resolve-prop-value.ts: -------------------------------------------------------------------------------- 1 | export function resolvePropValue(property: TProperty, bag: TBag) { 2 | if (property === undefined) return undefined 3 | if (typeof property === 'function') return property(bag) 4 | return property 5 | } 6 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/hooks/use-is-initial-render.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'react' 2 | 3 | export function useIsInitialRender() { 4 | let initial = useRef(true) 5 | 6 | useEffect(() => { 7 | initial.current = false 8 | }, []) 9 | 10 | return initial.current 11 | } 12 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/utils/dom.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from 'vue' 2 | 3 | export function dom(ref?: Ref): T | null { 4 | if (ref == null) return null 5 | if (ref.value == null) return null 6 | return ((ref as Ref).value.$el ?? ref.value) as T | null 7 | } 8 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/hooks/__mocks__/use-id.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | beforeEach(() => { 4 | id = 0 5 | }) 6 | 7 | let id = 0 8 | function generateId() { 9 | return ++id 10 | } 11 | 12 | export function useId() { 13 | const [id] = useState(generateId) 14 | return id 15 | } 16 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | node="yarn node" 5 | tsdxArgs=() 6 | 7 | # Add script name 8 | tsdxArgs+=("build" "--name" "headlessui" "--format" "cjs,esm,umd" "--tsconfig" "./tsconfig.tsdx.json") 9 | 10 | # Passthrough arguments and flags 11 | tsdxArgs+=($@) 12 | 13 | # Execute 14 | $node "$(yarn bin tsdx)" "${tsdxArgs[@]}" 15 | -------------------------------------------------------------------------------- /scripts/watch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | node="yarn node" 5 | tsdxArgs=() 6 | 7 | # Add script name 8 | tsdxArgs+=("watch" "--name" "headlessui" "--format" "cjs,esm,umd" "--tsconfig" "./tsconfig.tsdx.json") 9 | 10 | # Passthrough arguments and flags 11 | tsdxArgs+=($@) 12 | 13 | # Execute 14 | $node "$(yarn bin tsdx)" "${tsdxArgs[@]}" 15 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/hooks/use-is-mounted.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'react' 2 | 3 | export function useIsMounted() { 4 | let mounted = useRef(false) 5 | 6 | useEffect(() => { 7 | mounted.current = true 8 | 9 | return () => { 10 | mounted.current = false 11 | } 12 | }, []) 13 | 14 | return mounted 15 | } 16 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/hooks/use-disposables.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | import { disposables } from '../utils/disposables' 4 | 5 | export function useDisposables() { 6 | // Using useState instead of useRef so that we can use the initializer function. 7 | let [d] = useState(disposables) 8 | useEffect(() => () => d.dispose(), [d]) 9 | return d 10 | } 11 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | node="yarn node" 5 | jestArgs=() 6 | 7 | # Add default arguments 8 | jestArgs+=("--passWithNoTests") 9 | 10 | # Add arguments based on environment variables 11 | if [ -n "$CI" ]; then 12 | jestArgs+=("--maxWorkers=4") 13 | jestArgs+=("--ci") 14 | fi 15 | 16 | # Passthrough arguments and flags 17 | jestArgs+=($@) 18 | 19 | # Execute 20 | $node "$(yarn bin jest)" "${jestArgs[@]}" 21 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/hooks/use-window-event.ts: -------------------------------------------------------------------------------- 1 | import { onUnmounted } from 'vue' 2 | 3 | export function useWindowEvent( 4 | type: TType, 5 | listener: (this: Window, ev: WindowEventMap[TType]) => any, 6 | options?: boolean | AddEventListenerOptions 7 | ) { 8 | window.addEventListener(type, listener, options) 9 | onUnmounted(() => window.removeEventListener(type, listener, options)) 10 | } 11 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/internal/open-closed.ts: -------------------------------------------------------------------------------- 1 | import { 2 | inject, 3 | provide, 4 | 5 | // Types 6 | InjectionKey, 7 | Ref, 8 | } from 'vue' 9 | 10 | let Context = Symbol('Context') as InjectionKey> 11 | 12 | export enum State { 13 | Open, 14 | Closed, 15 | } 16 | 17 | export function useOpenClosed() { 18 | return inject(Context, null) 19 | } 20 | 21 | export function useOpenClosedProvider(value: Ref) { 22 | provide(Context, value) 23 | } 24 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/hooks/use-computed.ts: -------------------------------------------------------------------------------- 1 | import { useState, useRef } from 'react' 2 | import { useIsoMorphicEffect } from './use-iso-morphic-effect' 3 | 4 | export function useComputed(cb: () => T, dependencies: React.DependencyList) { 5 | let [value, setValue] = useState(cb) 6 | let cbRef = useRef(cb) 7 | useIsoMorphicEffect(() => { 8 | cbRef.current = cb 9 | }, [cb]) 10 | useIsoMorphicEffect(() => setValue(cbRef.current), [cbRef, setValue, ...dependencies]) 11 | return value 12 | } 13 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/test-utils/report-dom-node-changes.ts: -------------------------------------------------------------------------------- 1 | import { disposables } from '../utils/disposables' 2 | 3 | export function reportChanges(key: () => TType, onChange: (value: TType) => void) { 4 | let d = disposables() 5 | 6 | let previous: TType 7 | 8 | function track() { 9 | let next = key() 10 | if (previous !== next) { 11 | previous = next 12 | onChange(next) 13 | } 14 | d.requestAnimationFrame(track) 15 | } 16 | 17 | track() 18 | 19 | return d.dispose 20 | } 21 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components/dialog/dialog' 2 | export * from './components/disclosure/disclosure' 3 | export * from './components/focus-trap/focus-trap' 4 | export * from './components/listbox/listbox' 5 | export * from './components/menu/menu' 6 | export * from './components/popover/popover' 7 | export * from './components/portal/portal' 8 | export * from './components/radio-group/radio-group' 9 | export * from './components/switch/switch' 10 | export * from './components/transitions/transition' 11 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/test-utils/report-dom-node-changes.ts: -------------------------------------------------------------------------------- 1 | import { disposables } from '../utils/disposables' 2 | 3 | export function reportChanges(key: () => TType, onChange: (value: TType) => void) { 4 | let d = disposables() 5 | 6 | let previous: TType 7 | 8 | function track() { 9 | let next = key() 10 | if (previous !== next) { 11 | previous = next 12 | onChange(next) 13 | } 14 | d.requestAnimationFrame(track) 15 | } 16 | 17 | track() 18 | 19 | return d.dispose 20 | } 21 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components/dialog/dialog' 2 | export * from './components/disclosure/disclosure' 3 | export * from './components/focus-trap/focus-trap' 4 | export * from './components/listbox/listbox' 5 | export * from './components/menu/menu' 6 | export * from './components/popover/popover' 7 | export * from './components/portal/portal' 8 | export * from './components/radio-group/radio-group' 9 | export * from './components/switch/switch' 10 | export * from './components/transitions/transition' 11 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/keyboard.ts: -------------------------------------------------------------------------------- 1 | // TODO: This must already exist somewhere, right? 🤔 2 | // Ref: https://www.w3.org/TR/uievents-key/#named-key-attribute-values 3 | export enum Keys { 4 | Space = ' ', 5 | Enter = 'Enter', 6 | Escape = 'Escape', 7 | Backspace = 'Backspace', 8 | 9 | ArrowLeft = 'ArrowLeft', 10 | ArrowUp = 'ArrowUp', 11 | ArrowRight = 'ArrowRight', 12 | ArrowDown = 'ArrowDown', 13 | 14 | Home = 'Home', 15 | End = 'End', 16 | 17 | PageUp = 'PageUp', 18 | PageDown = 'PageDown', 19 | 20 | Tab = 'Tab', 21 | } 22 | -------------------------------------------------------------------------------- /jest/create-jest-config.js: -------------------------------------------------------------------------------- 1 | const { createJestConfig: create } = require('tsdx/dist/createJestConfig') 2 | 3 | module.exports = function createJestConfig(root, options) { 4 | return Object.assign( 5 | {}, 6 | create(undefined, root), 7 | { 8 | rootDir: root, 9 | setupFilesAfterEnv: ['../../jest/custom-matchers.ts'], 10 | globals: { 11 | 'ts-jest': { 12 | isolatedModules: true, 13 | tsConfig: '/tsconfig.tsdx.json', 14 | }, 15 | }, 16 | }, 17 | options 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/components/keyboard.ts: -------------------------------------------------------------------------------- 1 | // TODO: This must already exist somewhere, right? 🤔 2 | // Ref: https://www.w3.org/TR/uievents-key/#named-key-attribute-values 3 | export enum Keys { 4 | Space = ' ', 5 | Enter = 'Enter', 6 | Escape = 'Escape', 7 | Backspace = 'Backspace', 8 | 9 | ArrowLeft = 'ArrowLeft', 10 | ArrowUp = 'ArrowUp', 11 | ArrowRight = 'ArrowRight', 12 | ArrowDown = 'ArrowDown', 13 | 14 | Home = 'Home', 15 | End = 'End', 16 | 17 | PageUp = 'PageUp', 18 | PageDown = 'PageDown', 19 | 20 | Tab = 'Tab', 21 | } 22 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as HeadlessUI from './index' 2 | 3 | /** 4 | * Looks a bit of a silly test, however this ensures that we don't accidentally expose something to 5 | * the outside world that we didn't want! 6 | */ 7 | it('should expose the correct components', () => { 8 | expect(Object.keys(HeadlessUI)).toEqual([ 9 | 'Dialog', 10 | 'Disclosure', 11 | 'FocusTrap', 12 | 'Listbox', 13 | 'Menu', 14 | 'Popover', 15 | 'Portal', 16 | 'RadioGroup', 17 | 'Switch', 18 | 'Transition', 19 | ]) 20 | }) 21 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Headless UI - Playground 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/hooks/use-flags.ts: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from 'react' 2 | 3 | export function useFlags(initialFlags = 0) { 4 | let [flags, setFlags] = useState(initialFlags) 5 | 6 | let addFlag = useCallback((flag: number) => setFlags(flags => flags | flag), [setFlags]) 7 | let hasFlag = useCallback((flag: number) => Boolean(flags & flag), [flags]) 8 | let removeFlag = useCallback((flag: number) => setFlags(flags => flags & ~flag), [setFlags]) 9 | let toggleFlag = useCallback((flag: number) => setFlags(flags => flags ^ flag), [setFlags]) 10 | 11 | return { addFlag, hasFlag, removeFlag, toggleFlag } 12 | } 13 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/hooks/use-sync-refs.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect, useCallback } from 'react' 2 | 3 | export function useSyncRefs( 4 | ...refs: (React.MutableRefObject | ((instance: TType) => void) | null)[] 5 | ) { 6 | let cache = useRef(refs) 7 | 8 | useEffect(() => { 9 | cache.current = refs 10 | }, [refs]) 11 | 12 | return useCallback( 13 | (value: TType) => { 14 | for (let ref of cache.current) { 15 | if (ref == null) continue 16 | if (typeof ref === 'function') ref(value) 17 | else ref.current = value 18 | } 19 | }, 20 | [cache] 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/hooks/use-server-handoff-complete.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | let state = { serverHandoffComplete: false } 4 | 5 | export function useServerHandoffComplete() { 6 | let [serverHandoffComplete, setServerHandoffComplete] = useState(state.serverHandoffComplete) 7 | 8 | useEffect(() => { 9 | if (serverHandoffComplete === true) return 10 | 11 | setServerHandoffComplete(true) 12 | }, [serverHandoffComplete]) 13 | 14 | useEffect(() => { 15 | if (state.serverHandoffComplete === false) state.serverHandoffComplete = true 16 | }, []) 17 | 18 | return serverHandoffComplete 19 | } 20 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/internal/portal-force-root.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | createContext, 3 | useContext, 4 | 5 | // Types 6 | ReactNode, 7 | } from 'react' 8 | 9 | let ForcePortalRootContext = createContext(false) 10 | 11 | export function usePortalRoot() { 12 | return useContext(ForcePortalRootContext) 13 | } 14 | 15 | interface ForcePortalRootProps { 16 | force: boolean 17 | children: ReactNode 18 | } 19 | 20 | export function ForcePortalRoot(props: ForcePortalRootProps) { 21 | return ( 22 | 23 | {props.children} 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/test-utils/suppress-console-logs.ts: -------------------------------------------------------------------------------- 1 | type FunctionPropertyNames = { 2 | [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never 3 | }[keyof T] & 4 | string 5 | 6 | export function suppressConsoleLogs( 7 | cb: (...args: T) => void, 8 | type: FunctionPropertyNames = 'warn' 9 | ) { 10 | return (...args: T) => { 11 | let spy = jest.spyOn(global.console, type).mockImplementation(jest.fn()) 12 | 13 | return new Promise((resolve, reject) => { 14 | Promise.resolve(cb(...args)).then(resolve, reject) 15 | }).finally(() => spy.mockRestore()) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme') 2 | 3 | module.exports = { 4 | purge: [], 5 | theme: { 6 | container: { 7 | center: true, 8 | }, 9 | extend: { 10 | fontFamily: { 11 | sans: ['Inter var', ...defaultTheme.fontFamily.sans], 12 | }, 13 | colors: { 14 | code: { 15 | green: '#b5f4a5', 16 | yellow: '#ffe484', 17 | purple: '#d9a9ff', 18 | red: '#ff8383', 19 | blue: '#93ddfd', 20 | white: '#fff', 21 | }, 22 | }, 23 | }, 24 | }, 25 | variants: {}, 26 | plugins: [require('@tailwindcss/ui')], 27 | } 28 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/test-utils/suppress-console-logs.ts: -------------------------------------------------------------------------------- 1 | type FunctionPropertyNames = { 2 | [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never 3 | }[keyof T] & 4 | string 5 | 6 | export function suppressConsoleLogs( 7 | cb: (...args: T) => unknown, 8 | type: FunctionPropertyNames = 'error' 9 | ) { 10 | return (...args: T) => { 11 | let spy = jest.spyOn(global.console, type).mockImplementation(jest.fn()) 12 | 13 | return new Promise((resolve, reject) => { 14 | Promise.resolve(cb(...args)).then(resolve, reject) 15 | }).finally(() => spy.mockRestore()) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Feature Request 4 | url: https://github.com/tailwindlabs/headlessui/discussions/new?category=ideas 5 | about: 'Suggest any ideas you have using our discussion forums.' 6 | - name: Help 7 | url: https://github.com/tailwindlabs/headlessui/discussions/new?category=help 8 | about: 'If you have a question or need help, ask a question on the discussion forums.' 9 | - name: Kind Words 10 | url: https://github.com/tailwindlabs/headlessui/discussions/new?category=kind-words 11 | about: "Have something nice to say about Headless UI or Tailwind in general? We'd love to hear it!" 12 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/internal/open-closed.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | createContext, 3 | useContext, 4 | 5 | // Types 6 | ReactNode, 7 | ReactElement, 8 | } from 'react' 9 | 10 | let Context = createContext(null) 11 | Context.displayName = 'OpenClosedContext' 12 | 13 | export enum State { 14 | Open, 15 | Closed, 16 | } 17 | 18 | export function useOpenClosed() { 19 | return useContext(Context) 20 | } 21 | 22 | interface Props { 23 | value: State 24 | children: ReactNode 25 | } 26 | 27 | export function OpenClosedProvider({ value, children }: Props): ReactElement { 28 | return {children} 29 | } 30 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/examples/src/router.js: -------------------------------------------------------------------------------- 1 | import { createWebHistory, createRouter, RouterView } from 'vue-router' 2 | import lookup from './.generated/preload.js' 3 | import routes from './routes.json' 4 | 5 | function buildRoutes(routes) { 6 | return routes.map(route => { 7 | let definition = { 8 | path: route.path, 9 | component: route.component ? lookup[route.component] : RouterView, 10 | } 11 | 12 | if (route.children) { 13 | definition.children = buildRoutes(route.children) 14 | } 15 | 16 | return definition 17 | }) 18 | } 19 | 20 | export default createRouter({ 21 | history: createWebHistory(), 22 | routes: buildRoutes(routes), 23 | }) 24 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/hooks/use-window-event.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | export function useWindowEvent( 4 | type: TType, 5 | listener: (this: Window, ev: WindowEventMap[TType]) => any, 6 | options?: boolean | AddEventListenerOptions 7 | ) { 8 | let listenerRef = useRef(listener) 9 | listenerRef.current = listener 10 | 11 | useEffect(() => { 12 | function handler(event: WindowEventMap[TType]) { 13 | listenerRef.current.call(window, event) 14 | } 15 | 16 | window.addEventListener(type, handler, options) 17 | return () => window.removeEventListener(type, handler, options) 18 | }, [type, options]) 19 | } 20 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | ROOT_DIR="$(git rev-parse --show-toplevel)/" 5 | TARGET_DIR="$(pwd)" 6 | RELATIVE_TARGET_DIR="${TARGET_DIR/$ROOT_DIR/}" 7 | 8 | # INFO: This script is always run from the root of the repository. If we execute this script from a 9 | # package then the filters (in this case a path to $RELATIVE_TARGET_DIR) will be applied. 10 | 11 | pushd $ROOT_DIR > /dev/null 12 | 13 | node="yarn node" 14 | tsdxArgs=() 15 | 16 | # Add script name 17 | tsdxArgs+=("lint") 18 | 19 | # Add default arguments 20 | tsdxArgs+=($RELATIVE_TARGET_DIR) 21 | 22 | # Passthrough arguments and flags 23 | tsdxArgs+=($@) 24 | 25 | # Execute 26 | $node "$(yarn bin tsdx)" "${tsdxArgs[@]}" 27 | 28 | popd > /dev/null 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /packages/**/node_modules 6 | 7 | # testing 8 | /coverage 9 | /packages/**/coverage 10 | 11 | # logs 12 | *.log 13 | /packages/**/*.log 14 | 15 | # next.js 16 | /.next/ 17 | /packages/**/.next/ 18 | /out/ 19 | /packages/**/out/ 20 | 21 | # production 22 | /build 23 | /packages/**/dist 24 | /dist 25 | /packages/**/dist 26 | 27 | # misc 28 | .DS_Store 29 | *.pem 30 | .cache 31 | 32 | # debug 33 | npm-debug.log* 34 | yarn-debug.log* 35 | yarn-error.log* 36 | 37 | # local env files 38 | .env.local 39 | .env.development.local 40 | .env.test.local 41 | .env.production.local 42 | 43 | # vercel 44 | .vercel 45 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/utils/match.ts: -------------------------------------------------------------------------------- 1 | export function match( 2 | value: TValue, 3 | lookup: Record TReturnValue)>, 4 | ...args: any[] 5 | ): TReturnValue { 6 | if (value in lookup) { 7 | let returnValue = lookup[value] 8 | return typeof returnValue === 'function' ? returnValue(...args) : returnValue 9 | } 10 | 11 | let error = new Error( 12 | `Tried to handle "${value}" but there is no handler defined. Only defined handlers are: ${Object.keys( 13 | lookup 14 | ) 15 | .map(key => `"${key}"`) 16 | .join(', ')}.` 17 | ) 18 | if (Error.captureStackTrace) Error.captureStackTrace(error, match) 19 | throw error 20 | } 21 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/utils/match.ts: -------------------------------------------------------------------------------- 1 | export function match( 2 | value: TValue, 3 | lookup: Record TReturnValue)>, 4 | ...args: any[] 5 | ): TReturnValue { 6 | if (value in lookup) { 7 | let returnValue = lookup[value] 8 | return typeof returnValue === 'function' ? returnValue(...args) : returnValue 9 | } 10 | 11 | let error = new Error( 12 | `Tried to handle "${value}" but there is no handler defined. Only defined handlers are: ${Object.keys( 13 | lookup 14 | ) 15 | .map(key => `"${key}"`) 16 | .join(', ')}.` 17 | ) 18 | if (Error.captureStackTrace) Error.captureStackTrace(error, match) 19 | throw error 20 | } 21 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/examples/src/components/focus-trap/focus-trap.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 28 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/examples/src/playground-utils/hooks/use-popper.js: -------------------------------------------------------------------------------- 1 | import { ref, onMounted, watchEffect } from 'vue' 2 | import { createPopper } from '@popperjs/core' 3 | 4 | export function usePopper(options) { 5 | let reference = ref(null) 6 | let popper = ref(null) 7 | 8 | onMounted(() => { 9 | watchEffect(onInvalidate => { 10 | if (!popper.value) return 11 | if (!reference.value) return 12 | 13 | let popperEl = popper.value.el || popper.value 14 | let referenceEl = reference.value.el || reference.value 15 | 16 | if (!(referenceEl instanceof HTMLElement)) return 17 | if (!(popperEl instanceof HTMLElement)) return 18 | 19 | let { destroy } = createPopper(referenceEl, popperEl, options) 20 | 21 | onInvalidate(destroy) 22 | }) 23 | }) 24 | 25 | return [reference, popper] 26 | } 27 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/hooks/use-id.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { useIsoMorphicEffect } from './use-iso-morphic-effect' 3 | import { useServerHandoffComplete } from './use-server-handoff-complete' 4 | 5 | // We used a "simple" approach first which worked for SSR and rehydration on the client. However we 6 | // didn't take care of the Suspense case. To fix this we used the approach the @reach-ui/auto-id 7 | // uses. 8 | // 9 | // Credits: https://github.com/reach/reach-ui/blob/develop/packages/auto-id/src/index.tsx 10 | 11 | let id = 0 12 | function generateId() { 13 | return ++id 14 | } 15 | 16 | export function useId() { 17 | let ready = useServerHandoffComplete() 18 | let [id, setId] = useState(ready ? generateId : null) 19 | 20 | useIsoMorphicEffect(() => { 21 | if (id === null) setId(generateId()) 22 | }, [id]) 23 | 24 | return id != null ? '' + id : undefined 25 | } 26 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/utils/disposables.ts: -------------------------------------------------------------------------------- 1 | export function disposables() { 2 | let disposables: Function[] = [] 3 | 4 | let api = { 5 | requestAnimationFrame(...args: Parameters) { 6 | let raf = requestAnimationFrame(...args) 7 | api.add(() => cancelAnimationFrame(raf)) 8 | }, 9 | 10 | nextFrame(...args: Parameters) { 11 | api.requestAnimationFrame(() => { 12 | api.requestAnimationFrame(...args) 13 | }) 14 | }, 15 | 16 | setTimeout(...args: Parameters) { 17 | let timer = setTimeout(...args) 18 | api.add(() => clearTimeout(timer)) 19 | }, 20 | 21 | add(cb: () => void) { 22 | disposables.push(cb) 23 | }, 24 | 25 | dispose() { 26 | for (let dispose of disposables.splice(0)) { 27 | dispose() 28 | } 29 | }, 30 | } 31 | 32 | return api 33 | } 34 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/utils/disposables.ts: -------------------------------------------------------------------------------- 1 | export function disposables() { 2 | let disposables: Function[] = [] 3 | 4 | let api = { 5 | requestAnimationFrame(...args: Parameters) { 6 | let raf = requestAnimationFrame(...args) 7 | api.add(() => cancelAnimationFrame(raf)) 8 | }, 9 | 10 | nextFrame(...args: Parameters) { 11 | api.requestAnimationFrame(() => { 12 | api.requestAnimationFrame(...args) 13 | }) 14 | }, 15 | 16 | setTimeout(...args: Parameters) { 17 | let timer = setTimeout(...args) 18 | api.add(() => clearTimeout(timer)) 19 | }, 20 | 21 | add(cb: () => void) { 22 | disposables.push(cb) 23 | }, 24 | 25 | dispose() { 26 | for (let dispose of disposables.splice(0)) { 27 | dispose() 28 | } 29 | }, 30 | } 31 | 32 | return api 33 | } 34 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/internal/portal-force-root.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineComponent, 3 | inject, 4 | provide, 5 | 6 | // Types 7 | InjectionKey, 8 | } from 'vue' 9 | import { render } from '../utils/render' 10 | 11 | let ForcePortalRootContext = Symbol('ForcePortalRootContext') as InjectionKey 12 | 13 | export function usePortalRoot() { 14 | return inject(ForcePortalRootContext, false) 15 | } 16 | 17 | export let ForcePortalRoot = defineComponent({ 18 | name: 'ForcePortalRoot', 19 | props: { 20 | as: { type: [Object, String], default: 'template' }, 21 | force: { type: Boolean, default: false }, 22 | }, 23 | setup(props, { slots, attrs }) { 24 | provide(ForcePortalRootContext, props.force) 25 | 26 | return () => { 27 | let { force, ...passThroughProps } = props 28 | return render({ props: passThroughProps, slot: {}, slots, attrs, name: 'ForcePortalRoot' }) 29 | } 30 | }, 31 | }) 32 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "types"], 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "importHelpers": true, 7 | "declaration": true, 8 | "sourceMap": true, 9 | "rootDir": "./src", 10 | "strict": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "downlevelIteration": true, 16 | "moduleResolution": "node", 17 | "baseUrl": "./", 18 | "paths": { 19 | "@headlessui/vue": ["src"], 20 | "*": ["src/*", "node_modules/*"] 21 | }, 22 | "esModuleInterop": true, 23 | "target": "es5", 24 | "allowJs": true, 25 | "skipLibCheck": true, 26 | "forceConsistentCasingInFileNames": true, 27 | "noEmit": true, 28 | "resolveJsonModule": true, 29 | "isolatedModules": true 30 | }, 31 | "exclude": ["node_modules", "dist"] 32 | } 33 | -------------------------------------------------------------------------------- /jest/custom-matchers.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect' 2 | 3 | // Assuming requestAnimationFrame is roughly 60 frames per second 4 | let frame = 1000 / 60 5 | let amountOfFrames = 2 6 | 7 | let formatter = new Intl.NumberFormat('en') 8 | 9 | expect.extend({ 10 | toBeWithinRenderFrame(actual, expected) { 11 | let min = expected - frame * amountOfFrames 12 | let max = expected + frame * amountOfFrames 13 | 14 | let pass = actual >= min && actual <= max 15 | 16 | return { 17 | message: pass 18 | ? () => { 19 | return `expected ${actual} not to be within range of a frame ${formatter.format( 20 | min 21 | )} - ${formatter.format(max)}` 22 | } 23 | : () => { 24 | return `expected ${actual} not to be within range of a frame ${formatter.format( 25 | min 26 | )} - ${formatter.format(max)}` 27 | }, 28 | pass, 29 | } 30 | }, 31 | }) 32 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/hooks/use-tree-walker.ts: -------------------------------------------------------------------------------- 1 | import { watchEffect, ComputedRef } from 'vue' 2 | 3 | type AcceptNode = ( 4 | node: HTMLElement 5 | ) => 6 | | typeof NodeFilter.FILTER_ACCEPT 7 | | typeof NodeFilter.FILTER_SKIP 8 | | typeof NodeFilter.FILTER_REJECT 9 | 10 | export function useTreeWalker({ 11 | container, 12 | accept, 13 | walk, 14 | enabled, 15 | }: { 16 | container: ComputedRef 17 | accept: AcceptNode 18 | walk(node: HTMLElement): void 19 | enabled?: ComputedRef 20 | }) { 21 | watchEffect(() => { 22 | let root = container.value 23 | if (!root) return 24 | if (enabled !== undefined && !enabled.value) return 25 | 26 | let acceptNode = Object.assign((node: HTMLElement) => accept(node), { acceptNode: accept }) 27 | let walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, acceptNode, false) 28 | 29 | while (walker.nextNode()) walk(walker.currentNode as HTMLElement) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /packages/@headlessui-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "types"], 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "importHelpers": true, 7 | "declaration": true, 8 | "sourceMap": true, 9 | "rootDir": "./src", 10 | "strict": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "downlevelIteration": true, 16 | "moduleResolution": "node", 17 | "baseUrl": "./", 18 | "paths": { 19 | "@headlessui/react": ["src"], 20 | "*": ["src/*", "node_modules/*"] 21 | }, 22 | "jsx": "preserve", 23 | "esModuleInterop": true, 24 | "target": "es5", 25 | "allowJs": true, 26 | "skipLibCheck": true, 27 | "forceConsistentCasingInFileNames": true, 28 | "noEmit": true, 29 | "resolveJsonModule": true, 30 | "isolatedModules": true 31 | }, 32 | "exclude": ["node_modules"] 33 | } 34 | -------------------------------------------------------------------------------- /packages/@headlessui-react/pages/disclosure/disclosure.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Disclosure, Transition } from '@headlessui/react' 3 | 4 | export default function Home() { 5 | return ( 6 |
7 |
8 | 9 | Trigger 10 | 11 | 19 | Content 20 | 21 | 22 |
23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/examples/src/components/portal/portal.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 38 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/utils/bugs.ts: -------------------------------------------------------------------------------- 1 | // See: https://github.com/facebook/react/issues/7711 2 | // See: https://github.com/facebook/react/pull/20612 3 | // See: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-fe-disabled (2.) 4 | export function isDisabledReactIssue7711(element: Element): boolean { 5 | let parent = element.parentElement 6 | let legend = null 7 | 8 | while (parent && !(parent instanceof HTMLFieldSetElement)) { 9 | if (parent instanceof HTMLLegendElement) legend = parent 10 | parent = parent.parentElement 11 | } 12 | 13 | let isParentDisabled = parent?.getAttribute('disabled') === '' ?? false 14 | if (isParentDisabled && isFirstLegend(legend)) return false 15 | 16 | return isParentDisabled 17 | } 18 | 19 | function isFirstLegend(element: HTMLLegendElement | null): boolean { 20 | if (!element) return false 21 | 22 | let previous = element.previousElementSibling 23 | 24 | while (previous !== null) { 25 | if (previous instanceof HTMLLegendElement) return false 26 | previous = previous.previousElementSibling 27 | } 28 | 29 | return true 30 | } 31 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/examples/src/components/disclosure/disclosure.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tailwind Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/@headlessui-vue/examples/src/components/Home.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 45 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/components/focus-trap/focus-trap.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useRef, 3 | 4 | // Types 5 | ElementType, 6 | MutableRefObject, 7 | } from 'react' 8 | 9 | import { Props } from '../../types' 10 | import { render } from '../../utils/render' 11 | import { useFocusTrap, Features as FocusTrapFeatures } from '../../hooks/use-focus-trap' 12 | import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete' 13 | 14 | let DEFAULT_FOCUS_TRAP_TAG = 'div' as const 15 | 16 | export function FocusTrap( 17 | props: Props & { initialFocus?: MutableRefObject } 18 | ) { 19 | let container = useRef(null) 20 | let { initialFocus, ...passthroughProps } = props 21 | 22 | let ready = useServerHandoffComplete() 23 | useFocusTrap(container, ready ? FocusTrapFeatures.All : FocusTrapFeatures.None, { initialFocus }) 24 | 25 | let propsWeControl = { 26 | ref: container, 27 | } 28 | 29 | return render({ 30 | props: { ...passthroughProps, ...propsWeControl }, 31 | defaultTag: DEFAULT_FOCUS_TRAP_TAG, 32 | name: 'FocusTrap', 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1.bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a bug report for Headless UI. 3 | title: "[Bug]: " 4 | labels: [] 5 | body: 6 | - type: input 7 | attributes: 8 | label: What package within Headless UI are you using? 9 | description: 'For example: @headlessui/react' 10 | validations: 11 | required: true 12 | - type: input 13 | attributes: 14 | label: What version of that package are you using? 15 | description: 'For example: v0.3.1' 16 | validations: 17 | required: true 18 | - type: input 19 | attributes: 20 | label: What browser are you using? 21 | description: 'For example: Chrome, Safari, or N/A' 22 | validations: 23 | required: true 24 | - type: input 25 | attributes: 26 | label: Reproduction repository 27 | description: A public GitHub repo that demonstrates the bug. If it's really unnecessary, link us to a YouTube video you think is awesome instead. 28 | validations: 29 | required: true 30 | - type: textarea 31 | attributes: 32 | label: Describe your issue 33 | description: Describe the problem you're seeing, any important steps to reproduce and what behavior you expect instead 34 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/internal/stack-context.ts: -------------------------------------------------------------------------------- 1 | import { 2 | inject, 3 | provide, 4 | watchEffect, 5 | 6 | // Types 7 | InjectionKey, 8 | Ref, 9 | } from 'vue' 10 | 11 | type OnUpdate = (message: StackMessage, element: HTMLElement) => void 12 | 13 | let StackContext = Symbol('StackContext') as InjectionKey 14 | 15 | export enum StackMessage { 16 | AddElement, 17 | RemoveElement, 18 | } 19 | 20 | export function useStackContext() { 21 | return inject(StackContext, () => {}) 22 | } 23 | 24 | export function useElemenStack(element: Ref | null) { 25 | let notify = useStackContext() 26 | 27 | watchEffect(onInvalidate => { 28 | let domElement = element?.value 29 | if (!domElement) return 30 | 31 | notify(StackMessage.AddElement, domElement) 32 | onInvalidate(() => notify(StackMessage.RemoveElement, domElement!)) 33 | }) 34 | } 35 | 36 | export function useStackProvider(onUpdate?: OnUpdate) { 37 | let parentUpdate = useStackContext() 38 | 39 | function notify(...args: Parameters) { 40 | // Notify our layer 41 | onUpdate?.(...args) 42 | 43 | // Notify the parent 44 | parentUpdate(...args) 45 | } 46 | 47 | provide(StackContext, notify) 48 | } 49 | -------------------------------------------------------------------------------- /packages/@headlessui-react/playground-utils/hooks/use-popper.ts: -------------------------------------------------------------------------------- 1 | import { RefCallback, useRef, useCallback, useMemo } from 'react' 2 | import { createPopper, Options } from '@popperjs/core' 3 | 4 | /** 5 | * Example implementation to use Popper: https://popper.js.org/ 6 | */ 7 | export function usePopper( 8 | options?: Partial 9 | ): [RefCallback, RefCallback] { 10 | let reference = useRef(null) 11 | let popper = useRef(null) 12 | 13 | let cleanupCallback = useRef(() => {}) 14 | 15 | let instantiatePopper = useCallback(() => { 16 | if (!reference.current) return 17 | if (!popper.current) return 18 | 19 | if (cleanupCallback.current) cleanupCallback.current() 20 | 21 | cleanupCallback.current = createPopper(reference.current, popper.current, options).destroy 22 | }, [reference, popper, cleanupCallback, options]) 23 | 24 | return useMemo( 25 | () => [ 26 | referenceDomNode => { 27 | reference.current = referenceDomNode 28 | instantiatePopper() 29 | }, 30 | popperDomNode => { 31 | popper.current = popperDomNode 32 | instantiatePopper() 33 | }, 34 | ], 35 | [reference, popper, instantiatePopper] 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /packages/@headlessui-react/playground-utils/resolve-all-examples.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | export type ExamplesType = { 4 | name: string 5 | path: string 6 | children?: ExamplesType[] 7 | } 8 | 9 | export async function resolveAllExamples(...paths: string[]) { 10 | let base = path.resolve(process.cwd(), ...paths) 11 | 12 | if (!fs.existsSync(base)) { 13 | return false 14 | } 15 | 16 | let files = await fs.promises.readdir(base, { withFileTypes: true }) 17 | let items: ExamplesType[] = [] 18 | 19 | for (let file of files) { 20 | // Skip reserved filenames from Next. E.g.: _app.tsx, _error.tsx 21 | if (file.name.startsWith('_')) { 22 | continue 23 | } 24 | 25 | let bucket: ExamplesType = { 26 | name: file.name.replace(/-/g, ' ').replace(/\.tsx?/g, ''), 27 | path: [...paths, file.name] 28 | .join('/') 29 | .replace(/^pages/, '') 30 | .replace(/\.tsx?/g, '') 31 | .replace(/\/+/g, '/'), 32 | } 33 | 34 | if (file.isDirectory()) { 35 | let children = await resolveAllExamples(...paths, file.name) 36 | 37 | if (children) { 38 | bucket.children = children 39 | } 40 | } 41 | 42 | items.push(bucket) 43 | } 44 | 45 | return items 46 | } 47 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/hooks/use-tree-walker.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'react' 2 | import { useIsoMorphicEffect } from './use-iso-morphic-effect' 3 | 4 | type AcceptNode = ( 5 | node: HTMLElement 6 | ) => 7 | | typeof NodeFilter.FILTER_ACCEPT 8 | | typeof NodeFilter.FILTER_SKIP 9 | | typeof NodeFilter.FILTER_REJECT 10 | 11 | export function useTreeWalker({ 12 | container, 13 | accept, 14 | walk, 15 | enabled = true, 16 | }: { 17 | container: HTMLElement | null 18 | accept: AcceptNode 19 | walk(node: HTMLElement): void 20 | enabled?: boolean 21 | }) { 22 | let acceptRef = useRef(accept) 23 | let walkRef = useRef(walk) 24 | 25 | useEffect(() => { 26 | acceptRef.current = accept 27 | walkRef.current = walk 28 | }, [accept, walk]) 29 | 30 | useIsoMorphicEffect(() => { 31 | if (!container) return 32 | if (!enabled) return 33 | 34 | let accept = acceptRef.current 35 | let walk = walkRef.current 36 | 37 | let acceptNode = Object.assign((node: HTMLElement) => accept(node), { acceptNode: accept }) 38 | let walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, acceptNode, false) 39 | 40 | while (walker.nextNode()) walk(walker.currentNode as HTMLElement) 41 | }, [container, enabled, acceptRef, walkRef]) 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "headlessui", 3 | "version": "0.0.2", 4 | "description": "Headless UI components for various libraries like React and Vue", 5 | "main": "index.js", 6 | "repository": "https://github.com/tailwindlabs/headlessui", 7 | "license": "MIT", 8 | "private": true, 9 | "workspaces": [ 10 | "packages/*" 11 | ], 12 | "scripts": { 13 | "react": "yarn workspace @headlessui/react", 14 | "vue": "yarn workspace @headlessui/vue", 15 | "shared": "yarn workspace @headlessui/shared", 16 | "build": "yarn workspaces run build", 17 | "test": "./scripts/test.sh", 18 | "lint": "./scripts/lint.sh" 19 | }, 20 | "husky": { 21 | "hooks": { 22 | "pre-commit": "lint-staged" 23 | } 24 | }, 25 | "lint-staged": { 26 | "*.{js,jsx,ts,tsx}": "tsdx lint" 27 | }, 28 | "prettier": { 29 | "printWidth": 100, 30 | "semi": false, 31 | "singleQuote": true, 32 | "trailingComma": "es5" 33 | }, 34 | "devDependencies": { 35 | "@tailwindcss/ui": "^0.6.2", 36 | "@testing-library/jest-dom": "^5.11.9", 37 | "@types/node": "^14.14.22", 38 | "husky": "^4.3.8", 39 | "lint-staged": "^10.5.3", 40 | "prismjs": "^1.23.0", 41 | "tailwindcss": "^1.9.6", 42 | "tsdx": "^0.14.1", 43 | "tslib": "^2.1.0", 44 | "typescript": "^3.9.7" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/@headlessui-react/README.md: -------------------------------------------------------------------------------- 1 |

2 | @headlessui/react 3 |

4 | 5 |

6 | A set of completely unstyled, fully accessible UI components for React, designed to integrate 7 | beautifully with Tailwind CSS. 8 |

9 | 10 |

11 | Total Downloads 12 | Latest Release 13 | License 14 |

15 | 16 | ## Installation 17 | 18 | ```sh 19 | # npm 20 | npm install @headlessui/react 21 | 22 | # Yarn 23 | yarn add @headlessui/react 24 | ``` 25 | 26 | ## Documentation 27 | 28 | For full documentation, visit [headlessui.dev](https://headlessui.dev/react/menu). 29 | 30 | ## Community 31 | 32 | For help, discussion about best practices, or any other conversation that would benefit from being searchable: 33 | 34 | [Discuss Headless UI on GitHub](https://github.com/tailwindlabs/headlessui/discussions) 35 | 36 | For casual chit-chat with others using the library: 37 | 38 | [Join the Tailwind CSS Discord Server](https://discord.gg/7NF8GNe) 39 | 40 | -------------------------------------------------------------------------------- /packages/@headlessui-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@headlessui/react", 3 | "version": "1.2.0", 4 | "description": "A set of completely unstyled, fully accessible UI components for React, designed to integrate beautifully with Tailwind CSS.", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "module": "dist/headlessui.esm.js", 8 | "license": "MIT", 9 | "files": [ 10 | "README.md", 11 | "dist" 12 | ], 13 | "engines": { 14 | "node": ">=10" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/tailwindlabs/headlessui.git", 19 | "directory": "packages/@headlessui-react" 20 | }, 21 | "publishConfig": { 22 | "access": "public" 23 | }, 24 | "scripts": { 25 | "playground": "next dev", 26 | "playground:build": "next build", 27 | "prepublishOnly": "npm run build", 28 | "test": "../../scripts/test.sh", 29 | "build": "../../scripts/build.sh", 30 | "lint": "../../scripts/lint.sh" 31 | }, 32 | "peerDependencies": { 33 | "react": ">=16" 34 | }, 35 | "devDependencies": { 36 | "@types/react": "^16.14.2", 37 | "@types/react-dom": "^16.9.10", 38 | "@popperjs/core": "^2.6.0", 39 | "@testing-library/react": "^11.2.3", 40 | "framer-motion": "^2.9.5", 41 | "next": "10.0.5", 42 | "react": "^16.14.0", 43 | "react-dom": "^16.14.0", 44 | "snapshot-diff": "^0.8.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as HeadlessUI from './index' 2 | 3 | /** 4 | * Looks a bit of a silly test, however this ensures that we don't accidentally expose something to 5 | * the outside world that we didn't want! 6 | */ 7 | it('should expose the correct components', () => { 8 | expect(Object.keys(HeadlessUI)).toEqual([ 9 | // Dialog 10 | 'Dialog', 11 | 'DialogOverlay', 12 | 'DialogTitle', 13 | 'DialogDescription', 14 | 15 | // Disclosure 16 | 'Disclosure', 17 | 'DisclosureButton', 18 | 'DisclosurePanel', 19 | 20 | // FocusTrap 21 | 'FocusTrap', 22 | 23 | // Listbox 24 | 'Listbox', 25 | 'ListboxLabel', 26 | 'ListboxButton', 27 | 'ListboxOptions', 28 | 'ListboxOption', 29 | 30 | // Menu 31 | 'Menu', 32 | 'MenuButton', 33 | 'MenuItems', 34 | 'MenuItem', 35 | 36 | // Popover 37 | 'Popover', 38 | 'PopoverButton', 39 | 'PopoverOverlay', 40 | 'PopoverPanel', 41 | 'PopoverGroup', 42 | 43 | // Portal 44 | 'Portal', 45 | 'PortalGroup', 46 | 47 | // RadioGroup 48 | 'RadioGroup', 49 | 'RadioGroupOption', 50 | 'RadioGroupLabel', 51 | 'RadioGroupDescription', 52 | 53 | // Switch 54 | 'SwitchGroup', 55 | 'Switch', 56 | 'SwitchLabel', 57 | 'SwitchDescription', 58 | 59 | // Transition 60 | 'TransitionChild', 61 | 'TransitionRoot', 62 | ]) 63 | }) 64 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/README.md: -------------------------------------------------------------------------------- 1 |

2 | @headlessui/vue 3 |

4 | 5 |

6 | A set of completely unstyled, fully accessible UI components for Vue 3, designed to integrate 7 | beautifully with Tailwind CSS. 8 |

9 | 10 |

11 | Total Downloads 12 | Latest Release 13 | License 14 |

15 | 16 | ## Installation 17 | 18 | Please note that **this library only supports Vue 3**. 19 | 20 | ```sh 21 | # npm 22 | npm install @headlessui/vue 23 | 24 | # Yarn 25 | yarn add @headlessui/vue 26 | ``` 27 | 28 | ## Documentation 29 | 30 | For full documentation, visit [headlessui.dev](https://headlessui.dev/vue/menu). 31 | 32 | ## Community 33 | 34 | For help, discussion about best practices, or any other conversation that would benefit from being searchable: 35 | 36 | [Discuss Headless UI on GitHub](https://github.com/tailwindlabs/headlessui/discussions) 37 | 38 | For casual chit-chat with others using the library: 39 | 40 | [Join the Tailwind CSS Discord Server](https://discord.gg/7NF8GNe) 41 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@headlessui/vue", 3 | "version": "1.2.0", 4 | "description": "A set of completely unstyled, fully accessible UI components for Vue 3, designed to integrate beautifully with Tailwind CSS.", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "module": "dist/headlessui.esm.js", 8 | "license": "MIT", 9 | "files": [ 10 | "README.md", 11 | "dist" 12 | ], 13 | "engines": { 14 | "node": ">=10" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/tailwindlabs/headlessui.git", 19 | "directory": "packages/@headlessui-vue" 20 | }, 21 | "publishConfig": { 22 | "access": "public" 23 | }, 24 | "scripts": { 25 | "playground": "vite --config ./vite.config.js serve examples", 26 | "playground:build": "NODE_ENV=production vite build examples", 27 | "prepublishOnly": "npm run build", 28 | "build": "../../scripts/build.sh", 29 | "watch": "../../scripts/watch.sh", 30 | "test": "../../scripts/test.sh", 31 | "lint": "../../scripts/lint.sh" 32 | }, 33 | "peerDependencies": { 34 | "vue": "^3.0.0" 35 | }, 36 | "devDependencies": { 37 | "@popperjs/core": "^2.5.3", 38 | "@testing-library/vue": "^5.1.0", 39 | "@types/debounce": "^1.2.0", 40 | "@vue/compiler-sfc": "3.0.1", 41 | "@vue/test-utils": "^2.0.0-beta.7", 42 | "vite": "^1.0.0-rc.4", 43 | "vue": "^3.0.0", 44 | "vue-router": "^4.0.0-beta.13" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/@headlessui-react/pages/switch/switch-with-pure-tailwind.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Switch } from '@headlessui/react' 3 | 4 | import { classNames } from '../../src/utils/class-names' 5 | 6 | export default function Home() { 7 | let [state, setState] = useState(false) 8 | 9 | return ( 10 |
11 | 12 | Enable notifications 13 | 14 | 19 | classNames( 20 | 'relative inline-flex flex-shrink-0 h-6 border-2 border-transparent rounded-full cursor-pointer w-11 focus:outline-none focus:shadow-outline transition-colors ease-in-out duration-200', 21 | checked ? 'bg-indigo-600' : 'bg-gray-200' 22 | ) 23 | } 24 | > 25 | {({ checked }) => ( 26 | <> 27 | 33 | 34 | )} 35 | 36 | 37 |
38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/components/focus-trap/focus-trap.ts: -------------------------------------------------------------------------------- 1 | import { 2 | computed, 3 | defineComponent, 4 | onMounted, 5 | onUnmounted, 6 | ref, 7 | 8 | // Types 9 | PropType, 10 | } from 'vue' 11 | import { render } from '../../utils/render' 12 | import { useFocusTrap } from '../../hooks/use-focus-trap' 13 | 14 | export let FocusTrap = defineComponent({ 15 | name: 'FocusTrap', 16 | props: { 17 | as: { type: [Object, String], default: 'div' }, 18 | initialFocus: { type: Object as PropType, default: null }, 19 | }, 20 | render() { 21 | let slot = {} 22 | let propsWeControl = { ref: 'el' } 23 | let { initialFocus, ...passThroughProps } = this.$props 24 | 25 | return render({ 26 | props: { ...passThroughProps, ...propsWeControl }, 27 | slot, 28 | attrs: this.$attrs, 29 | slots: this.$slots, 30 | name: 'FocusTrap', 31 | }) 32 | }, 33 | setup(props) { 34 | let containers = ref(new Set()) 35 | let container = ref(null) 36 | let enabled = ref(true) 37 | let focusTrapOptions = computed(() => ({ initialFocus: props.initialFocus })) 38 | 39 | onMounted(() => { 40 | if (!container.value) return 41 | containers.value.add(container.value) 42 | 43 | useFocusTrap(containers, enabled, focusTrapOptions) 44 | }) 45 | 46 | onUnmounted(() => { 47 | enabled.value = false 48 | }) 49 | 50 | return { el: container } 51 | }, 52 | }) 53 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/examples/src/components/switch/switch.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 41 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/internal/stack-context.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | createContext, 3 | useCallback, 4 | useContext, 5 | 6 | // Types 7 | MutableRefObject, 8 | ReactNode, 9 | } from 'react' 10 | import { useIsoMorphicEffect } from '../hooks/use-iso-morphic-effect' 11 | 12 | type OnUpdate = ( 13 | message: StackMessage, 14 | type: string, 15 | element: MutableRefObject 16 | ) => void 17 | 18 | let StackContext = createContext(() => {}) 19 | StackContext.displayName = 'StackContext' 20 | 21 | export enum StackMessage { 22 | Add, 23 | Remove, 24 | } 25 | 26 | export function useStackContext() { 27 | return useContext(StackContext) 28 | } 29 | 30 | export function StackProvider({ 31 | children, 32 | onUpdate, 33 | type, 34 | element, 35 | }: { 36 | children: ReactNode 37 | onUpdate?: OnUpdate 38 | type: string 39 | element: MutableRefObject 40 | }) { 41 | let parentUpdate = useStackContext() 42 | 43 | let notify = useCallback( 44 | (...args: Parameters) => { 45 | // Notify our layer 46 | onUpdate?.(...args) 47 | 48 | // Notify the parent 49 | parentUpdate(...args) 50 | }, 51 | [parentUpdate, onUpdate] 52 | ) 53 | 54 | useIsoMorphicEffect(() => { 55 | notify(StackMessage.Add, type, element) 56 | return () => notify(StackMessage.Remove, type, element) 57 | }, [notify, type, element]) 58 | 59 | return {children} 60 | } 61 | -------------------------------------------------------------------------------- /packages/@headlessui-react/pages/transitions/component-examples/peek-a-boo.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Transition } from '@headlessui/react' 3 | 4 | export default function Home() { 5 | let [isOpen, setIsOpen] = useState(true) 6 | 7 | return ( 8 | <> 9 |
10 |
11 | 12 | 19 | 20 | 21 | 32 | Contents to show and hide 33 | 34 |
35 |
36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /packages/@headlessui-react/pages/_error.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ErrorPage from 'next/error' 3 | import Head from 'next/head' 4 | import Link from 'next/link' 5 | 6 | import { ExamplesType, resolveAllExamples } from '../playground-utils/resolve-all-examples' 7 | import { PropsOf } from '../src/types' 8 | 9 | function NextLink(props: PropsOf<'a'>) { 10 | let { href, children, ...rest } = props 11 | return ( 12 | 13 | {children} 14 | 15 | ) 16 | } 17 | 18 | export async function getStaticProps() { 19 | return { 20 | props: { 21 | examples: await resolveAllExamples('pages'), 22 | }, 23 | } 24 | } 25 | 26 | export default function Page(props: { examples: false | ExamplesType[] }) { 27 | if (props.examples === false) { 28 | return 29 | } 30 | 31 | return ( 32 | <> 33 | 34 | Examples 35 | 36 | 37 |
38 |
39 |

Examples

40 | 41 |
42 |
43 | 44 | ) 45 | } 46 | 47 | export function Examples(props: { examples: ExamplesType[] }) { 48 | return ( 49 |
    50 | {props.examples.map(example => ( 51 |
  • 52 | {example.children ? ( 53 |

    {example.name}

    54 | ) : ( 55 | 56 | {example.name} 57 | 58 | )} 59 | {example.children && } 60 |
  • 61 | ))} 62 |
63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/test-utils/vue-testing-library.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { logDOM, fireEvent, screen } from '@testing-library/dom' 3 | 4 | let mountedWrappers = new Set() 5 | 6 | function resolveContainer(): HTMLElement { 7 | let div = document.createElement('div') 8 | let baseElement = document.body 9 | let container = baseElement.appendChild(div) 10 | 11 | let attachTo = document.createElement('div') 12 | container.appendChild(attachTo) 13 | return attachTo 14 | } 15 | 16 | export function render(TestComponent: any, options?: Parameters[1] | undefined) { 17 | let wrapper = mount(TestComponent, { 18 | ...options, 19 | attachTo: options?.attachTo ?? resolveContainer(), 20 | }) 21 | 22 | mountedWrappers.add(wrapper) 23 | 24 | return { 25 | get container() { 26 | return wrapper.element.parentElement! 27 | }, 28 | debug(element = wrapper.element.parentElement!) { 29 | logDOM(element) 30 | }, 31 | asFragment() { 32 | let template = document.createElement('template') 33 | template.innerHTML = wrapper.element.parentElement!.innerHTML 34 | return template.content 35 | }, 36 | } 37 | } 38 | 39 | function cleanup() { 40 | mountedWrappers.forEach(cleanupAtWrapper) 41 | document.body.innerHTML = '' 42 | } 43 | 44 | function cleanupAtWrapper(wrapper: any) { 45 | if (wrapper.element.parentNode && wrapper.element.parentNode.parentNode === document.body) { 46 | document.body.removeChild(wrapper.element.parentNode) 47 | } 48 | 49 | try { 50 | wrapper.unmount() 51 | } catch { 52 | } finally { 53 | mountedWrappers.delete(wrapper) 54 | } 55 | } 56 | 57 | if (typeof afterEach === 'function') { 58 | afterEach(() => cleanup()) 59 | } 60 | 61 | export { fireEvent, screen } 62 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/examples/src/KeyCaster.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 75 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | env: 6 | NODE_VERSION: 12.x 7 | 8 | jobs: 9 | install: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Begin CI... 14 | uses: actions/checkout@v2 15 | - name: Use Node ${{ env.NODE_VERSION }} 16 | uses: actions/setup-node@v2 17 | with: 18 | node-version: ${{ env.NODE_VERSION }} 19 | - uses: actions/cache@v2 20 | with: 21 | path: '**/node_modules' 22 | key: ${{ runner.os }}-${{ env.NODE_VERSION }}-modules-${{ hashFiles('**/yarn.lock') }} 23 | - name: Install dependencies 24 | run: yarn install --frozen-lockfile 25 | env: 26 | CI: true 27 | 28 | lint: 29 | runs-on: ubuntu-latest 30 | needs: [install] 31 | 32 | steps: 33 | - name: Begin CI... 34 | uses: actions/checkout@v2 35 | - uses: actions/cache@v2 36 | with: 37 | path: '**/node_modules' 38 | key: ${{ runner.os }}-${{ env.NODE_VERSION }}-modules-${{ hashFiles('**/yarn.lock') }} 39 | - name: Lint 40 | run: yarn lint 41 | env: 42 | CI: true 43 | 44 | test: 45 | runs-on: ubuntu-latest 46 | needs: [install] 47 | 48 | steps: 49 | - name: Begin CI... 50 | uses: actions/checkout@v2 51 | - uses: actions/cache@v2 52 | with: 53 | path: '**/node_modules' 54 | key: ${{ runner.os }}-${{ env.NODE_VERSION }}-modules-${{ hashFiles('**/yarn.lock') }} 55 | - name: Test 56 | run: yarn test 57 | env: 58 | CI: true 59 | 60 | build: 61 | runs-on: ubuntu-latest 62 | needs: [install] 63 | 64 | steps: 65 | - name: Begin CI... 66 | uses: actions/checkout@v2 67 | - uses: actions/cache@v2 68 | with: 69 | path: '**/node_modules' 70 | key: ${{ runner.os }}-${{ env.NODE_VERSION }}-modules-${{ hashFiles('**/yarn.lock') }} 71 | - name: Build 72 | run: yarn build 73 | env: 74 | CI: true 75 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode, ReactElement } from 'react' 2 | 3 | // A unique placeholder we can use as a default. This is nice because we can use this instead of 4 | // defaulting to null / never / ... and possibly collide with actual data. 5 | // Ideally we use a unique symbol here. 6 | let __ = '1D45E01E-AF44-47C4-988A-19A94EBAF55C' as const 7 | export type __ = typeof __ 8 | 9 | export type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never 10 | 11 | export type PropsOf = TTag extends React.ElementType 12 | ? React.ComponentProps 13 | : never 14 | 15 | type PropsWeControl = 'as' | 'children' | 'refName' | 'className' 16 | 17 | // Resolve the props of the component, but ensure to omit certain props that we control 18 | type CleanProps = TOmitableProps extends __ 19 | ? Omit, PropsWeControl> 20 | : Omit, TOmitableProps | PropsWeControl> 21 | 22 | // Add certain props that we control 23 | type OurProps = { 24 | as?: TTag 25 | children?: ReactNode | ((bag: TSlot) => ReactElement) 26 | refName?: string 27 | } 28 | 29 | // Conditionally override the `className`, to also allow for a function 30 | // if and only if the PropsOf already define `className`. 31 | // This will allow us to have a TS error on as={Fragment} 32 | type ClassNameOverride = PropsOf extends { className?: any } 33 | ? { className?: string | ((bag: TSlot) => string) } 34 | : {} 35 | 36 | // Provide clean TypeScript props, which exposes some of our custom API's. 37 | export type Props = CleanProps< 38 | TTag, 39 | TOmitableProps 40 | > & 41 | OurProps & 42 | ClassNameOverride 43 | 44 | type Without = { [P in Exclude]?: never } 45 | export type XOR = T | U extends __ 46 | ? never 47 | : T extends __ 48 | ? U 49 | : U extends __ 50 | ? T 51 | : T | U extends object 52 | ? (Without & U) | (Without & T) 53 | : T | U 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Headless UI 3 |

4 | 5 |

6 | A set of completely unstyled, fully accessible UI components, designed to integrate 7 | beautifully with Tailwind CSS. 8 |

9 | 10 | --- 11 | 12 | ## Documentation 13 | 14 | For full documentation, visit [headlessui.dev](https://headlessui.dev). 15 | 16 | ## Packages 17 | 18 | | Name | Version | Downloads | 19 | | :------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------: | 20 | | [`@headlessui/react`](https://github.com/tailwindlabs/headlessui/tree/main/packages/%40headlessui-react) | [![npm version](https://img.shields.io/npm/v/@headlessui/react.svg)](https://www.npmjs.com/package/@headlessui/react) | [![npm downloads](https://img.shields.io/npm/dt/@headlessui/react.svg)](https://www.npmjs.com/package/@headlessui/react) | 21 | | [`@headlessui/vue`](https://github.com/tailwindlabs/headlessui/tree/main/packages/%40headlessui-vue) | [![npm version](https://img.shields.io/npm/v/@headlessui/vue.svg)](https://www.npmjs.com/package/@headlessui/vue) | [![npm downloads](https://img.shields.io/npm/dt/@headlessui/vue.svg)](https://www.npmjs.com/package/@headlessui/vue) | 22 | 23 | ## Community 24 | 25 | For help, discussion about best practices, or any other conversation that would benefit from being searchable: 26 | 27 | [Discuss Headless UI on GitHub](https://github.com/tailwindlabs/headlessui/discussions) 28 | 29 | For casual chit-chat with others using the library: 30 | 31 | [Join the Tailwind CSS Discord Server](https://discord.gg/7NF8GNe) 32 | -------------------------------------------------------------------------------- /packages/@headlessui-react/pages/transitions/component-examples/nested/hidden.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, ReactNode } from 'react' 2 | import { Transition } from '@headlessui/react' 3 | 4 | export default function Home() { 5 | let [isOpen, setIsOpen] = useState(true) 6 | 7 | return ( 8 | <> 9 |
10 |
11 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 | 40 | ) 41 | } 42 | 43 | function Box({ children }: { children?: ReactNode }) { 44 | return ( 45 | 54 |
55 | This is a box 56 | {children} 57 |
58 |
59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /packages/@headlessui-react/pages/transitions/component-examples/nested/unmount.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, ReactNode } from 'react' 2 | import { Transition } from '@headlessui/react' 3 | 4 | export default function Home() { 5 | let [isOpen, setIsOpen] = useState(true) 6 | 7 | return ( 8 | <> 9 |
10 |
11 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 | 40 | ) 41 | } 42 | 43 | function Box({ children }: { children?: ReactNode }) { 44 | return ( 45 | 54 |
55 | This is a box 56 | {children} 57 |
58 |
59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /packages/@headlessui-react/src/utils/calculate-active-index.ts: -------------------------------------------------------------------------------- 1 | function assertNever(x: never): never { 2 | throw new Error('Unexpected object: ' + x) 3 | } 4 | 5 | export enum Focus { 6 | /** Focus the first non-disabled item. */ 7 | First, 8 | 9 | /** Focus the previous non-disabled item. */ 10 | Previous, 11 | 12 | /** Focus the next non-disabled item. */ 13 | Next, 14 | 15 | /** Focus the last non-disabled item. */ 16 | Last, 17 | 18 | /** Focus a specific item based on the `id` of the item. */ 19 | Specific, 20 | 21 | /** Focus no items at all. */ 22 | Nothing, 23 | } 24 | 25 | export function calculateActiveIndex( 26 | action: { focus: Focus.Specific; id: string } | { focus: Exclude }, 27 | resolvers: { 28 | resolveItems(): TItem[] 29 | resolveActiveIndex(): number | null 30 | resolveId(item: TItem): string 31 | resolveDisabled(item: TItem): boolean 32 | } 33 | ) { 34 | let items = resolvers.resolveItems() 35 | if (items.length <= 0) return null 36 | 37 | let currentActiveIndex = resolvers.resolveActiveIndex() 38 | let activeIndex = currentActiveIndex ?? -1 39 | 40 | let nextActiveIndex = (() => { 41 | switch (action.focus) { 42 | case Focus.First: 43 | return items.findIndex(item => !resolvers.resolveDisabled(item)) 44 | 45 | case Focus.Previous: { 46 | let idx = items 47 | .slice() 48 | .reverse() 49 | .findIndex((item, idx, all) => { 50 | if (activeIndex !== -1 && all.length - idx - 1 >= activeIndex) return false 51 | return !resolvers.resolveDisabled(item) 52 | }) 53 | if (idx === -1) return idx 54 | return items.length - 1 - idx 55 | } 56 | 57 | case Focus.Next: 58 | return items.findIndex((item, idx) => { 59 | if (idx <= activeIndex) return false 60 | return !resolvers.resolveDisabled(item) 61 | }) 62 | 63 | case Focus.Last: { 64 | let idx = items 65 | .slice() 66 | .reverse() 67 | .findIndex(item => !resolvers.resolveDisabled(item)) 68 | if (idx === -1) return idx 69 | return items.length - 1 - idx 70 | } 71 | 72 | case Focus.Specific: 73 | return items.findIndex(item => resolvers.resolveId(item) === action.id) 74 | 75 | case Focus.Nothing: 76 | return null 77 | 78 | default: 79 | assertNever(action) 80 | } 81 | })() 82 | 83 | return nextActiveIndex === -1 ? currentActiveIndex : nextActiveIndex 84 | } 85 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/utils/calculate-active-index.ts: -------------------------------------------------------------------------------- 1 | function assertNever(x: never): never { 2 | throw new Error('Unexpected object: ' + x) 3 | } 4 | 5 | export enum Focus { 6 | /** Focus the first non-disabled item. */ 7 | First, 8 | 9 | /** Focus the previous non-disabled item. */ 10 | Previous, 11 | 12 | /** Focus the next non-disabled item. */ 13 | Next, 14 | 15 | /** Focus the last non-disabled item. */ 16 | Last, 17 | 18 | /** Focus a specific item based on the `id` of the item. */ 19 | Specific, 20 | 21 | /** Focus no items at all. */ 22 | Nothing, 23 | } 24 | 25 | export function calculateActiveIndex( 26 | action: { focus: Focus.Specific; id: string } | { focus: Exclude }, 27 | resolvers: { 28 | resolveItems(): TItem[] 29 | resolveActiveIndex(): number | null 30 | resolveId(item: TItem): string 31 | resolveDisabled(item: TItem): boolean 32 | } 33 | ) { 34 | let items = resolvers.resolveItems() 35 | if (items.length <= 0) return null 36 | 37 | let currentActiveIndex = resolvers.resolveActiveIndex() 38 | let activeIndex = currentActiveIndex ?? -1 39 | 40 | let nextActiveIndex = (() => { 41 | switch (action.focus) { 42 | case Focus.First: 43 | return items.findIndex(item => !resolvers.resolveDisabled(item)) 44 | 45 | case Focus.Previous: { 46 | let idx = items 47 | .slice() 48 | .reverse() 49 | .findIndex((item, idx, all) => { 50 | if (activeIndex !== -1 && all.length - idx - 1 >= activeIndex) return false 51 | return !resolvers.resolveDisabled(item) 52 | }) 53 | if (idx === -1) return idx 54 | return items.length - 1 - idx 55 | } 56 | 57 | case Focus.Next: 58 | return items.findIndex((item, idx) => { 59 | if (idx <= activeIndex) return false 60 | return !resolvers.resolveDisabled(item) 61 | }) 62 | 63 | case Focus.Last: { 64 | let idx = items 65 | .slice() 66 | .reverse() 67 | .findIndex(item => !resolvers.resolveDisabled(item)) 68 | if (idx === -1) return idx 69 | return items.length - 1 - idx 70 | } 71 | 72 | case Focus.Specific: 73 | return items.findIndex(item => resolvers.resolveId(item) === action.id) 74 | 75 | case Focus.Nothing: 76 | return null 77 | 78 | default: 79 | assertNever(action) 80 | } 81 | })() 82 | 83 | return nextActiveIndex === -1 ? currentActiveIndex : nextActiveIndex 84 | } 85 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/components/description/description.ts: -------------------------------------------------------------------------------- 1 | import { 2 | computed, 3 | defineComponent, 4 | inject, 5 | onMounted, 6 | onUnmounted, 7 | provide, 8 | ref, 9 | unref, 10 | 11 | // Types 12 | ComputedRef, 13 | InjectionKey, 14 | Ref, 15 | } from 'vue' 16 | 17 | import { useId } from '../../hooks/use-id' 18 | import { render } from '../../utils/render' 19 | 20 | // --- 21 | 22 | let DescriptionContext = Symbol('DescriptionContext') as InjectionKey<{ 23 | register(value: string): () => void 24 | slot: Ref> 25 | name: string 26 | props: Record 27 | }> 28 | 29 | function useDescriptionContext() { 30 | let context = inject(DescriptionContext, null) 31 | if (context === null) { 32 | throw new Error('Missing parent') 33 | } 34 | return context 35 | } 36 | 37 | export function useDescriptions({ 38 | slot = ref({}), 39 | name = 'Description', 40 | props = {}, 41 | }: { 42 | slot?: Ref> 43 | name?: string 44 | props?: Record 45 | } = {}): ComputedRef { 46 | let descriptionIds = ref([]) 47 | 48 | function register(value: string) { 49 | descriptionIds.value.push(value) 50 | 51 | return () => { 52 | let idx = descriptionIds.value.indexOf(value) 53 | if (idx === -1) return 54 | descriptionIds.value.splice(idx, 1) 55 | } 56 | } 57 | 58 | provide(DescriptionContext, { register, slot, name, props }) 59 | 60 | // The actual id's as string or undefined. 61 | return computed(() => 62 | descriptionIds.value.length > 0 ? descriptionIds.value.join(' ') : undefined 63 | ) 64 | } 65 | 66 | // --- 67 | 68 | export let Description = defineComponent({ 69 | name: 'Description', 70 | props: { 71 | as: { type: [Object, String], default: 'p' }, 72 | }, 73 | render() { 74 | let { name = 'Description', slot = ref({}), props = {} } = this.context 75 | let passThroughProps = this.$props 76 | let propsWeControl = { 77 | ...Object.entries(props).reduce( 78 | (acc, [key, value]) => Object.assign(acc, { [key]: unref(value) }), 79 | {} 80 | ), 81 | id: this.id, 82 | } 83 | 84 | return render({ 85 | props: { ...passThroughProps, ...propsWeControl }, 86 | slot: slot.value, 87 | attrs: this.$attrs, 88 | slots: this.$slots, 89 | name, 90 | }) 91 | }, 92 | setup() { 93 | let context = useDescriptionContext() 94 | let id = `headlessui-description-${useId()}` 95 | 96 | onMounted(() => onUnmounted(context.register(id))) 97 | 98 | return { id, context } 99 | }, 100 | }) 101 | -------------------------------------------------------------------------------- /packages/@headlessui-vue/src/components/label/label.ts: -------------------------------------------------------------------------------- 1 | import { 2 | computed, 3 | defineComponent, 4 | inject, 5 | onMounted, 6 | onUnmounted, 7 | provide, 8 | ref, 9 | unref, 10 | 11 | // Types 12 | ComputedRef, 13 | InjectionKey, 14 | } from 'vue' 15 | 16 | import { useId } from '../../hooks/use-id' 17 | import { render } from '../../utils/render' 18 | 19 | // --- 20 | 21 | let LabelContext = Symbol('LabelContext') as InjectionKey<{ 22 | register(value: string): () => void 23 | slot: Record 24 | name: string 25 | props: Record 26 | }> 27 | 28 | function useLabelContext() { 29 | let context = inject(LabelContext, null) 30 | if (context === null) { 31 | let err = new Error('You used a