Skip to content

Commit 0c0601f

Browse files
authored
Fix focus styles showing up when using the mouse (#2347)
* update playground examples to use a shared `Button` * expose a `ui-focus-visible` variant * keep track of a `data-headlessui-focus-visible` attribute * do not set the `tabindex` The focus was always set, but the ring wasn't showing up. This was also focusing a ring when the browser decided not the add one. Let's make the browser decide when to show this or not. * update changelog
1 parent 9a7dcfc commit 0c0601f

File tree

26 files changed

+258
-216
lines changed

26 files changed

+258
-216
lines changed

packages/@headlessui-react/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
- Nothing yet!
10+
### Fixed
11+
12+
- Fix focus styles showing up when using the mouse ([#2347](https://github.com/tailwindlabs/headlessui/pull/2347))
1113

1214
## [1.7.13] - 2023-03-03
1315

packages/@headlessui-react/src/utils/focus-management.ts

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,47 @@ export function restoreFocusIfNecessary(element: HTMLElement | null) {
116116
})
117117
}
118118

119+
// The method of triggering an action, this is used to determine how we should
120+
// restore focus after an action has been performed.
121+
enum ActivationMethod {
122+
/* If the action was triggered by a keyboard event. */
123+
Keyboard = 0,
124+
125+
/* If the action was triggered by a mouse / pointer / ... event.*/
126+
Mouse = 1,
127+
}
128+
129+
// We want to be able to set and remove the `data-headlessui-mouse` attribute on the `html` element.
130+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
131+
document.addEventListener(
132+
'keydown',
133+
(event) => {
134+
if (event.metaKey || event.altKey || event.ctrlKey) {
135+
return
136+
}
137+
138+
document.documentElement.dataset.headlessuiFocusVisible = ''
139+
},
140+
true
141+
)
142+
143+
document.addEventListener(
144+
'click',
145+
(event) => {
146+
// Event originated from an actual mouse click
147+
if (event.detail === ActivationMethod.Mouse) {
148+
delete document.documentElement.dataset.headlessuiFocusVisible
149+
}
150+
151+
// Event originated from a keyboard event that triggered the `click` event
152+
else if (event.detail === ActivationMethod.Keyboard) {
153+
document.documentElement.dataset.headlessuiFocusVisible = ''
154+
}
155+
},
156+
true
157+
)
158+
}
159+
119160
export function focusElement(element: HTMLElement | null) {
120161
element?.focus({ preventScroll: true })
121162
}
@@ -232,14 +273,5 @@ export function focusIn(
232273
next.select()
233274
}
234275

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

packages/@headlessui-tailwindcss/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,6 @@
3737
},
3838
"devDependencies": {
3939
"esbuild": "^0.11.18",
40-
"tailwindcss": "^3.2.4"
40+
"tailwindcss": "^3.2.7"
4141
}
4242
}

packages/@headlessui-tailwindcss/src/index.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ let css = String.raw
99
function run(input: string, config: any, plugin = tailwind) {
1010
let { currentTestName } = expect.getState()
1111

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

56+
it('should generate the ui-focus-visible variant', async () => {
57+
let config = {
58+
content: [{ raw: html`<div class="ui-focus-visible:underline"></div>` }],
59+
plugins: [hui],
60+
}
61+
62+
return run('@tailwind utilities', config).then((result) => {
63+
expect(result.css).toMatchFormattedCss(css`
64+
:where([data-headlessui-focus-visible]) .ui-focus-visible\:underline:focus {
65+
text-decoration-line: underline;
66+
}
67+
`)
68+
})
69+
})
70+
5571
describe('custom prefix', () => {
5672
it('should generate css for an exposed state', async () => {
5773
let config = {

packages/@headlessui-tailwindcss/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export default plugin.withOptions<Options>(({ prefix = 'ui' } = {}) => {
3131
`&[data-headlessui-state]:not([data-headlessui-state~="${state}"])`,
3232
`:where([data-headlessui-state]:not([data-headlessui-state~="${state}"])) &:not([data-headlessui-state])`,
3333
])
34+
35+
addVariant(`${prefix}-focus-visible`, ':where([data-headlessui-focus-visible]) &:focus')
3436
}
3537
}
3638
})

packages/@headlessui-vue/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
- Nothing yet!
10+
### Fixed
11+
12+
- Fix focus styles showing up when using the mouse ([#2347](https://github.com/tailwindlabs/headlessui/pull/2347))
1113

1214
## [1.7.12] - 2023-03-03
1315

packages/@headlessui-vue/src/utils/focus-management.ts

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,47 @@ export function restoreFocusIfNecessary(element: HTMLElement | null) {
109109
})
110110
}
111111

112+
// The method of triggering an action, this is used to determine how we should
113+
// restore focus after an action has been performed.
114+
enum ActivationMethod {
115+
/* If the action was triggered by a keyboard event. */
116+
Keyboard = 0,
117+
118+
/* If the action was triggered by a mouse / pointer / ... event.*/
119+
Mouse = 1,
120+
}
121+
122+
// We want to be able to set and remove the `data-headlessui-mouse` attribute on the `html` element.
123+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
124+
document.addEventListener(
125+
'keydown',
126+
(event) => {
127+
if (event.metaKey || event.altKey || event.ctrlKey) {
128+
return
129+
}
130+
131+
document.documentElement.dataset.headlessuiFocusVisible = ''
132+
},
133+
true
134+
)
135+
136+
document.addEventListener(
137+
'click',
138+
(event) => {
139+
// Event originated from an actual mouse click
140+
if (event.detail === ActivationMethod.Mouse) {
141+
delete document.documentElement.dataset.headlessuiFocusVisible
142+
}
143+
144+
// Event originated from a keyboard event that triggered the `click` event
145+
else if (event.detail === ActivationMethod.Keyboard) {
146+
document.documentElement.dataset.headlessuiFocusVisible = ''
147+
}
148+
},
149+
true
150+
)
151+
}
152+
112153
export function focusElement(element: HTMLElement | null) {
113154
element?.focus({ preventScroll: true })
114155
}
@@ -226,14 +267,5 @@ export function focusIn(
226267
next.select()
227268
}
228269

229-
// This is a little weird, but let me try and explain: There are a few scenario's
230-
// in chrome for example where a focused `<a>` tag does not get the default focus
231-
// styles and sometimes they do. This highly depends on whether you started by
232-
// clicking or by using your keyboard. When you programmatically add focus `anchor.focus()`
233-
// then the active element (document.activeElement) is this anchor, which is expected.
234-
// However in that case the default focus styles are not applied *unless* you
235-
// also add this tabindex.
236-
if (!next.hasAttribute('tabindex')) next.setAttribute('tabindex', '0')
237-
238270
return FocusResult.Success
239271
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ComponentProps, forwardRef, ReactNode } from 'react'
2+
3+
function classNames(...classes: (string | false | undefined | null)[]) {
4+
return classes.filter(Boolean).join(' ')
5+
}
6+
7+
export let Button = forwardRef<
8+
HTMLButtonElement,
9+
ComponentProps<'button'> & { children?: ReactNode }
10+
>(({ className, ...props }, ref) => (
11+
<button
12+
ref={ref}
13+
type="button"
14+
className={classNames(
15+
'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',
16+
className
17+
)}
18+
{...props}
19+
/>
20+
))

packages/playground-react/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@
2727
"react": "^18.0.0",
2828
"react-dom": "^18.0.0",
2929
"react-flatpickr": "^3.10.9",
30-
"tailwindcss": "^0.0.0-insiders.83b4811"
30+
"tailwindcss": "^3.2.7"
3131
}
3232
}

packages/playground-react/pages/combinations/form.tsx

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState } from 'react'
22
import { Switch, RadioGroup, Listbox, Combobox } from '@headlessui/react'
33
import { classNames } from '../../utils/class-names'
4+
import { Button } from '../../components/button'
45

56
function Section({ title, children }) {
67
return (
@@ -170,26 +171,24 @@ export default function App() {
170171
{({ value }) => (
171172
<>
172173
<div className="relative">
173-
<span className="inline-block w-full rounded-md shadow-sm">
174-
<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">
175-
<span className="block truncate">{value?.name?.first}</span>
176-
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
177-
<svg
178-
className="h-5 w-5 text-gray-400"
179-
viewBox="0 0 20 20"
180-
fill="none"
181-
stroke="currentColor"
182-
>
183-
<path
184-
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
185-
strokeWidth="1.5"
186-
strokeLinecap="round"
187-
strokeLinejoin="round"
188-
/>
189-
</svg>
190-
</span>
191-
</Listbox.Button>
192-
</span>
174+
<Listbox.Button as={Button} className="w-full">
175+
<span className="block truncate">{value?.name?.first}</span>
176+
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
177+
<svg
178+
className="h-5 w-5 text-gray-400"
179+
viewBox="0 0 20 20"
180+
fill="none"
181+
stroke="currentColor"
182+
>
183+
<path
184+
d="M7 7l3-3 3 3m0 6l-3 3-3-3"
185+
strokeWidth="1.5"
186+
strokeLinecap="round"
187+
strokeLinejoin="round"
188+
/>
189+
</svg>
190+
</span>
191+
</Listbox.Button>
193192

194193
<div className="absolute z-10 mt-1 w-full rounded-md bg-white shadow-lg">
195194
<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">

0 commit comments

Comments
 (0)