Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix focus styles showing up when using the mouse #2347

Merged
merged 5 commits into from
Mar 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/@headlessui-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Nothing yet!
### Fixed

- Fix focus styles showing up when using the mouse ([#2347](https://github.com/tailwindlabs/headlessui/pull/2347))

## [1.7.13] - 2023-03-03

Expand Down
50 changes: 41 additions & 9 deletions packages/@headlessui-react/src/utils/focus-management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,47 @@ export function restoreFocusIfNecessary(element: HTMLElement | null) {
})
}

// The method of triggering an action, this is used to determine how we should
// restore focus after an action has been performed.
enum ActivationMethod {
/* If the action was triggered by a keyboard event. */
Keyboard = 0,

/* If the action was triggered by a mouse / pointer / ... event.*/
Mouse = 1,
}

// We want to be able to set and remove the `data-headlessui-mouse` attribute on the `html` element.
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
document.addEventListener(
'keydown',
(event) => {
if (event.metaKey || event.altKey || event.ctrlKey) {
return
}

document.documentElement.dataset.headlessuiFocusVisible = ''
},
true
)

document.addEventListener(
'click',
(event) => {
// Event originated from an actual mouse click
if (event.detail === ActivationMethod.Mouse) {
delete document.documentElement.dataset.headlessuiFocusVisible
}

// Event originated from a keyboard event that triggered the `click` event
else if (event.detail === ActivationMethod.Keyboard) {
document.documentElement.dataset.headlessuiFocusVisible = ''
}
},
true
)
}

export function focusElement(element: HTMLElement | null) {
element?.focus({ preventScroll: true })
}
Expand Down Expand Up @@ -232,14 +273,5 @@ export function focusIn(
next.select()
}

// This is a little weird, but let me try and explain: There are a few scenario's
// in chrome for example where a focused `<a>` tag does not get the default focus
// styles and sometimes they do. This highly depends on whether you started by
// clicking or by using your keyboard. When you programmatically add focus `anchor.focus()`
// then the active element (document.activeElement) is this anchor, which is expected.
// However in that case the default focus styles are not applied *unless* you
// also add this tabindex.
if (!next.hasAttribute('tabindex')) next.setAttribute('tabindex', '0')

return FocusResult.Success
}
2 changes: 1 addition & 1 deletion packages/@headlessui-tailwindcss/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,6 @@
},
"devDependencies": {
"esbuild": "^0.11.18",
"tailwindcss": "^3.2.4"
"tailwindcss": "^3.2.7"
}
}
16 changes: 16 additions & 0 deletions packages/@headlessui-tailwindcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ let css = String.raw
function run(input: string, config: any, plugin = tailwind) {
let { currentTestName } = expect.getState()

// @ts-ignore
return postcss(plugin(config)).process(input, {
from: `${path.resolve(__filename)}?test=${currentTestName}`,
})
Expand Down Expand Up @@ -52,6 +53,21 @@ it('should generate the inverse "not" css for an exposed state', async () => {
})
})

it('should generate the ui-focus-visible variant', async () => {
let config = {
content: [{ raw: html`<div class="ui-focus-visible:underline"></div>` }],
plugins: [hui],
}

return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
:where([data-headlessui-focus-visible]) .ui-focus-visible\:underline:focus {
text-decoration-line: underline;
}
`)
})
})

describe('custom prefix', () => {
it('should generate css for an exposed state', async () => {
let config = {
Expand Down
2 changes: 2 additions & 0 deletions packages/@headlessui-tailwindcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export default plugin.withOptions<Options>(({ prefix = 'ui' } = {}) => {
`&[data-headlessui-state]:not([data-headlessui-state~="${state}"])`,
`:where([data-headlessui-state]:not([data-headlessui-state~="${state}"])) &:not([data-headlessui-state])`,
])

addVariant(`${prefix}-focus-visible`, ':where([data-headlessui-focus-visible]) &:focus')
}
}
})
4 changes: 3 additions & 1 deletion packages/@headlessui-vue/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Nothing yet!
### Fixed

- Fix focus styles showing up when using the mouse ([#2347](https://github.com/tailwindlabs/headlessui/pull/2347))

## [1.7.12] - 2023-03-03

Expand Down
50 changes: 41 additions & 9 deletions packages/@headlessui-vue/src/utils/focus-management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,47 @@ export function restoreFocusIfNecessary(element: HTMLElement | null) {
})
}

// The method of triggering an action, this is used to determine how we should
// restore focus after an action has been performed.
enum ActivationMethod {
/* If the action was triggered by a keyboard event. */
Keyboard = 0,

/* If the action was triggered by a mouse / pointer / ... event.*/
Mouse = 1,
}

// We want to be able to set and remove the `data-headlessui-mouse` attribute on the `html` element.
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
document.addEventListener(
'keydown',
(event) => {
if (event.metaKey || event.altKey || event.ctrlKey) {
return
}

document.documentElement.dataset.headlessuiFocusVisible = ''
},
true
)

document.addEventListener(
'click',
(event) => {
// Event originated from an actual mouse click
if (event.detail === ActivationMethod.Mouse) {
delete document.documentElement.dataset.headlessuiFocusVisible
}

// Event originated from a keyboard event that triggered the `click` event
else if (event.detail === ActivationMethod.Keyboard) {
document.documentElement.dataset.headlessuiFocusVisible = ''
}
},
true
)
}

export function focusElement(element: HTMLElement | null) {
element?.focus({ preventScroll: true })
}
Expand Down Expand Up @@ -226,14 +267,5 @@ export function focusIn(
next.select()
}

// This is a little weird, but let me try and explain: There are a few scenario's
// in chrome for example where a focused `<a>` tag does not get the default focus
// styles and sometimes they do. This highly depends on whether you started by
// clicking or by using your keyboard. When you programmatically add focus `anchor.focus()`
// then the active element (document.activeElement) is this anchor, which is expected.
// However in that case the default focus styles are not applied *unless* you
// also add this tabindex.
if (!next.hasAttribute('tabindex')) next.setAttribute('tabindex', '0')

return FocusResult.Success
}
20 changes: 20 additions & 0 deletions packages/playground-react/components/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ComponentProps, forwardRef, ReactNode } from 'react'

function classNames(...classes: (string | false | undefined | null)[]) {
return classes.filter(Boolean).join(' ')
}

export let Button = forwardRef<
HTMLButtonElement,
ComponentProps<'button'> & { children?: ReactNode }
>(({ className, ...props }, ref) => (
<button
ref={ref}
type="button"
className={classNames(
'ui-focus-visible:ring-2 ui-focus-visible:ring-offset-2 flex items-center rounded-md border border-gray-300 bg-white px-2 py-1 ring-gray-500 ring-offset-gray-100 focus:outline-none',
className
)}
{...props}
/>
))
2 changes: 1 addition & 1 deletion packages/playground-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-flatpickr": "^3.10.9",
"tailwindcss": "^0.0.0-insiders.83b4811"
"tailwindcss": "^3.2.7"
}
}
39 changes: 19 additions & 20 deletions packages/playground-react/pages/combinations/form.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { Switch, RadioGroup, Listbox, Combobox } from '@headlessui/react'
import { classNames } from '../../utils/class-names'
import { Button } from '../../components/button'

function Section({ title, children }) {
return (
Expand Down Expand Up @@ -170,26 +171,24 @@ export default function App() {
{({ value }) => (
<>
<div className="relative">
<span className="inline-block w-full rounded-md shadow-sm">
<Listbox.Button className="relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 sm:text-sm sm:leading-5">
<span className="block truncate">{value?.name?.first}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg
className="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</Listbox.Button>
</span>
<Listbox.Button as={Button} className="w-full">
<span className="block truncate">{value?.name?.first}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg
className="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
>
<path
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</Listbox.Button>

<div className="absolute z-10 mt-1 w-full rounded-md bg-white shadow-lg">
<Listbox.Options className="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5">
Expand Down
15 changes: 8 additions & 7 deletions packages/playground-react/pages/combinations/tabs-in-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { useState } from 'react'
import { Dialog, Tab } from '@headlessui/react'
import { Button } from '../../components/button'

export default function App() {
let [open, setOpen] = useState(false)

return (
<>
<button onClick={() => setOpen(true)}>Open dialog</button>
<div className="p-12">
<Button onClick={() => setOpen(true)}>Open dialog</Button>
<Dialog open={open} onClose={setOpen} className="fixed inset-0 grid place-content-center">
<div className="fixed inset-0 bg-gray-500/70" />
<Dialog.Panel className="inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<Tab.Group>
<Tab.List>
<Tab className="px-3 py-2">Tab 1</Tab>
<Tab className="px-3 py-2">Tab 2</Tab>
<Tab className="px-3 py-2">Tab 3</Tab>
<Tab.List className="flex gap-4 py-4">
<Tab as={Button}>Tab 1</Tab>
<Tab as={Button}>Tab 2</Tab>
<Tab as={Button}>Tab 3</Tab>
</Tab.List>
<Tab.Panels>
<Tab.Panel className="px-3 py-2">Panel 1</Tab.Panel>
Expand All @@ -26,6 +27,6 @@ export default function App() {
</div>
</Dialog.Panel>
</Dialog>
</>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'
import { Combobox } from '@headlessui/react'

import { classNames } from '../../utils/class-names'
import { Button } from '../../components/button'

let everybody = [
'Wade Cooper',
Expand Down Expand Up @@ -64,7 +65,7 @@ export default function Home() {
onChange={(e) => setQuery(e.target.value)}
className="border-none px-3 py-1 outline-none"
/>
<Combobox.Button className="cursor-default border-l bg-gray-100 px-1 text-indigo-600 focus:outline-none">
<Combobox.Button as={Button}>
<span className="pointer-events-none flex items-center px-2">
<svg
className="h-5 w-5 text-gray-400"
Expand Down
14 changes: 5 additions & 9 deletions packages/playground-react/pages/dialog/dialog-focus-issue.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { useState } from 'react'
import { Dialog } from '@headlessui/react'
import { Button } from '../../components/button'

function Modal(props) {
return (
<Dialog className="relative z-50" {...props}>
<div className="fixed inset-0 bg-green-500 bg-opacity-90 backdrop-blur backdrop-filter" />
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4">
<Dialog.Panel className="relative m-5 w-full max-w-3xl rounded-lg bg-white p-10 shadow">
<button className="m-5 rounded-lg bg-blue-600 py-2 px-5 text-white">One</button>
<button className="m-5 rounded-lg bg-blue-600 py-2 px-5 text-white">Two</button>
<Dialog.Panel className="relative m-5 flex w-full max-w-3xl gap-4 rounded-lg bg-white p-10 shadow">
<Button>One</Button>
<Button>Two</Button>
</Dialog.Panel>
</div>
</div>
Expand All @@ -23,12 +24,7 @@ export default function DialogFocusIssue() {
return (
<div className="p-10">
<h1 className="py-2 text-3xl font-semibold">Headless UI Focus Jump</h1>
<button
className="my-5 rounded-lg bg-blue-600 py-2 px-5 text-white"
onClick={() => setIsOpen(true)}
>
Open Dialog
</button>
<Button onClick={() => setIsOpen(true)}>Open Dialog</Button>
<div className="bg-white p-20"></div>
<div className="bg-gray-100 p-20"></div>
<div className="bg-gray-200 p-20"></div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useRef } from 'react'
import { useState } from 'react'
import { Dialog } from '@headlessui/react'
import { Button } from '../../components/button'

if (typeof document !== 'undefined') {
class MyCustomElement extends HTMLElement {
Expand Down Expand Up @@ -49,12 +50,7 @@ export default function App() {

return (
<div>
<button
className="m-4 rounded border-0 bg-gray-500 px-3 py-1 font-medium text-white hover:bg-gray-600"
onClick={() => setOpen(true)}
>
open
</button>
<Button onClick={() => setOpen(true)}>open</Button>
<Dialog open={open} onClose={() => setOpen(false)}>
<div className="fixed inset-0 z-50 bg-gray-900/75 backdrop-blur-lg">
<div>
Expand Down
Loading