Skip to content

Commit 9e9c08c

Browse files
authored
Fix Tab incorrectly activating on focus event (#1887)
* rework Tabs so that they don't change on focus The "change on focus" was an incorrect implementation detail that made it a bit easier but this causes a problem as seen in #1858. If you want to conditionally check if you want to change the tab or note (e.g. by using `window.confirm`) then the focus is lost while the popup is shown. Regardless of your choice, the browser will re-focus the Tab therefore asking you *again* what you want to do. This fixes that by only activating the tab if needed while using arrow keys or when you click the tab (not when it is focused). * update changelog
1 parent e7cfb05 commit 9e9c08c

File tree

4 files changed

+44
-25
lines changed

4 files changed

+44
-25
lines changed

packages/@headlessui-react/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Improve `Portal` detection for `Popover` components ([#1842](https://github.com/tailwindlabs/headlessui/pull/1842))
1313
- Fix `useOutsideClick` swallowing events inside ShadowDOM ([#1876](https://github.com/tailwindlabs/headlessui/pull/1876))
14+
- Fix `Tab` incorrectly activating on `focus` event ([#1887](https://github.com/tailwindlabs/headlessui/pull/1887))
1415

1516
## [1.7.2] - 2022-09-15
1617

packages/@headlessui-react/src/components/tabs/tabs.tsx

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { render, Features, PropsForFeatures, forwardRefWithAs } from '../../util
1919
import { useId } from '../../hooks/use-id'
2020
import { match } from '../../utils/match'
2121
import { Keys } from '../../components/keyboard'
22-
import { focusIn, Focus, sortByDomNode } from '../../utils/focus-management'
22+
import { focusIn, Focus, sortByDomNode, FocusResult } from '../../utils/focus-management'
2323
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
2424
import { useSyncRefs } from '../../hooks/use-sync-refs'
2525
import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
@@ -28,6 +28,7 @@ import { FocusSentinel } from '../../internal/focus-sentinel'
2828
import { useEvent } from '../../hooks/use-event'
2929
import { microTask } from '../../utils/micro-task'
3030
import { Hidden } from '../../internal/hidden'
31+
import { getOwnerDocument } from '../../utils/owner'
3132

3233
interface StateDefinition {
3334
selectedIndex: number
@@ -322,6 +323,7 @@ let TabRoot = forwardRefWithAs(function Tab<TTag extends ElementType = typeof DE
322323

323324
let { orientation, activation, selectedIndex, tabs, panels } = useData('Tab')
324325
let actions = useActions('Tab')
326+
let data = useData('Tab')
325327
let SSRContext = useSSRTabsCounter('Tab')
326328

327329
let internalTabRef = useRef<HTMLElement | null>(null)
@@ -336,6 +338,16 @@ let TabRoot = forwardRefWithAs(function Tab<TTag extends ElementType = typeof DE
336338
if (myIndex === -1) myIndex = mySSRIndex
337339
let selected = myIndex === selectedIndex
338340

341+
let activateUsing = useEvent((cb: () => FocusResult) => {
342+
let result = cb()
343+
if (result === FocusResult.Success && activation === 'auto') {
344+
let newTab = getOwnerDocument(internalTabRef)?.activeElement
345+
let idx = data.tabs.findIndex((tab) => tab.current === newTab)
346+
if (idx !== -1) actions.change(idx)
347+
}
348+
return result
349+
})
350+
339351
let handleKeyDown = useEvent((event: ReactKeyboardEvent<HTMLElement>) => {
340352
let list = tabs.map((tab) => tab.current).filter(Boolean) as HTMLElement[]
341353

@@ -353,38 +365,36 @@ let TabRoot = forwardRefWithAs(function Tab<TTag extends ElementType = typeof DE
353365
event.preventDefault()
354366
event.stopPropagation()
355367

356-
return focusIn(list, Focus.First)
368+
return activateUsing(() => focusIn(list, Focus.First))
357369

358370
case Keys.End:
359371
case Keys.PageDown:
360372
event.preventDefault()
361373
event.stopPropagation()
362374

363-
return focusIn(list, Focus.Last)
375+
return activateUsing(() => focusIn(list, Focus.Last))
364376
}
365377

366-
if (
367-
match(orientation, {
378+
let result = activateUsing(() => {
379+
return match(orientation, {
368380
vertical() {
369381
if (event.key === Keys.ArrowUp) return focusIn(list, Focus.Previous | Focus.WrapAround)
370382
if (event.key === Keys.ArrowDown) return focusIn(list, Focus.Next | Focus.WrapAround)
371-
return
383+
return FocusResult.Error
372384
},
373385
horizontal() {
374386
if (event.key === Keys.ArrowLeft) return focusIn(list, Focus.Previous | Focus.WrapAround)
375387
if (event.key === Keys.ArrowRight) return focusIn(list, Focus.Next | Focus.WrapAround)
376-
return
388+
return FocusResult.Error
377389
},
378390
})
379-
) {
391+
})
392+
393+
if (result === FocusResult.Success) {
380394
return event.preventDefault()
381395
}
382396
})
383397

384-
let handleFocus = useEvent(() => {
385-
internalTabRef.current?.focus()
386-
})
387-
388398
let ready = useRef(false)
389399
let handleSelection = useEvent(() => {
390400
if (ready.current) return
@@ -411,7 +421,6 @@ let TabRoot = forwardRefWithAs(function Tab<TTag extends ElementType = typeof DE
411421
let ourProps = {
412422
ref: tabRef,
413423
onKeyDown: handleKeyDown,
414-
onFocus: activation === 'manual' ? handleFocus : handleSelection,
415424
onMouseDown: handleMouseDown,
416425
onClick: handleSelection,
417426
id,

packages/@headlessui-vue/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Improve `Portal` detection for `Popover` components ([#1842](https://github.com/tailwindlabs/headlessui/pull/1842))
1414
- Fix crash when `children` are `undefined` ([#1885](https://github.com/tailwindlabs/headlessui/pull/1885))
1515
- Fix `useOutsideClick` swallowing events inside ShadowDOM ([#1876](https://github.com/tailwindlabs/headlessui/pull/1876))
16+
- Fix `Tab` incorrectly activating on `focus` event ([#1887](https://github.com/tailwindlabs/headlessui/pull/1887))
1617

1718
## [1.7.2] - 2022-09-15
1819

packages/@headlessui-vue/src/components/tabs/tabs.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ import { useId } from '../../hooks/use-id'
2020
import { Keys } from '../../keyboard'
2121
import { dom } from '../../utils/dom'
2222
import { match } from '../../utils/match'
23-
import { focusIn, Focus } from '../../utils/focus-management'
23+
import { focusIn, Focus, FocusResult } from '../../utils/focus-management'
2424
import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
2525
import { FocusSentinel } from '../../internal/focus-sentinel'
2626
import { microTask } from '../../utils/micro-task'
2727
import { Hidden } from '../../internal/hidden'
28+
import { getOwnerDocument } from '../../utils/owner'
2829

2930
type StateDefinition = {
3031
// State
@@ -233,6 +234,16 @@ export let Tab = defineComponent({
233234
let myIndex = computed(() => api.tabs.value.indexOf(internalTabRef))
234235
let selected = computed(() => myIndex.value === api.selectedIndex.value)
235236

237+
function activateUsing(cb: () => FocusResult) {
238+
let result = cb()
239+
if (result === FocusResult.Success && api.activation.value === 'auto') {
240+
let newTab = getOwnerDocument(internalTabRef)?.activeElement
241+
let idx = api.tabs.value.findIndex((tab) => dom(tab) === newTab)
242+
if (idx !== -1) api.setSelectedIndex(idx)
243+
}
244+
return result
245+
}
246+
236247
function handleKeyDown(event: KeyboardEvent) {
237248
let list = api.tabs.value.map((tab) => dom(tab)).filter(Boolean) as HTMLElement[]
238249

@@ -250,39 +261,37 @@ export let Tab = defineComponent({
250261
event.preventDefault()
251262
event.stopPropagation()
252263

253-
return focusIn(list, Focus.First)
264+
return activateUsing(() => focusIn(list, Focus.First))
254265

255266
case Keys.End:
256267
case Keys.PageDown:
257268
event.preventDefault()
258269
event.stopPropagation()
259270

260-
return focusIn(list, Focus.Last)
271+
return activateUsing(() => focusIn(list, Focus.Last))
261272
}
262273

263-
if (
274+
let result = activateUsing(() =>
264275
match(api.orientation.value, {
265276
vertical() {
266277
if (event.key === Keys.ArrowUp) return focusIn(list, Focus.Previous | Focus.WrapAround)
267278
if (event.key === Keys.ArrowDown) return focusIn(list, Focus.Next | Focus.WrapAround)
268-
return
279+
return FocusResult.Error
269280
},
270281
horizontal() {
271282
if (event.key === Keys.ArrowLeft)
272283
return focusIn(list, Focus.Previous | Focus.WrapAround)
273284
if (event.key === Keys.ArrowRight) return focusIn(list, Focus.Next | Focus.WrapAround)
274-
return
285+
return FocusResult.Error
275286
},
276287
})
277-
) {
288+
)
289+
290+
if (result === FocusResult.Success) {
278291
return event.preventDefault()
279292
}
280293
}
281294

282-
function handleFocus() {
283-
dom(internalTabRef)?.focus()
284-
}
285-
286295
let ready = ref(false)
287296
function handleSelection() {
288297
if (ready.value) return
@@ -315,7 +324,6 @@ export let Tab = defineComponent({
315324
let ourProps = {
316325
ref: internalTabRef,
317326
onKeydown: handleKeyDown,
318-
onFocus: api.activation.value === 'manual' ? handleFocus : handleSelection,
319327
onMousedown: handleMouseDown,
320328
onClick: handleSelection,
321329
id,

0 commit comments

Comments
 (0)