From 78ebd3eb23e4f220559eebeb0275813fceb35446 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 30 Nov 2020 13:12:41 +0100 Subject: [PATCH] prevent scrolling when refocusing items Fixes #161 --- .../src/components/listbox/listbox.tsx | 23 ++++++++++--------- .../src/components/menu/menu.tsx | 16 ++++++------- .../src/components/switch/switch.tsx | 2 +- .../src/components/listbox/listbox.ts | 18 +++++++-------- .../src/components/menu/menu.ts | 16 ++++++------- .../src/components/switch/switch.ts | 2 +- 6 files changed, 39 insertions(+), 38 deletions(-) diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx index 75e535d..78cd9fd 100644 --- a/packages/@headlessui-react/src/components/listbox/listbox.tsx +++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx @@ -181,7 +181,7 @@ export function Listbox< if (!optionsRef.current?.contains(target)) dispatch({ type: ActionTypes.CloseListbox }) if (active !== document.body && active?.contains(target)) return // Keep focus on newly clicked/focused element - if (!event.defaultPrevented) buttonRef.current?.focus() + if (!event.defaultPrevented) buttonRef.current?.focus({ preventScroll: true }) } window.addEventListener('click', handler) @@ -237,7 +237,7 @@ const Button = forwardRefWithAs(function Button< event.preventDefault() dispatch({ type: ActionTypes.OpenListbox }) d.nextFrame(() => { - state.optionsRef.current?.focus() + state.optionsRef.current?.focus({ preventScroll: true }) if (!state.propsRef.current.value) dispatch({ type: ActionTypes.GoToOption, focus: Focus.First }) }) @@ -247,7 +247,7 @@ const Button = forwardRefWithAs(function Button< event.preventDefault() dispatch({ type: ActionTypes.OpenListbox }) d.nextFrame(() => { - state.optionsRef.current?.focus() + state.optionsRef.current?.focus({ preventScroll: true }) if (!state.propsRef.current.value) dispatch({ type: ActionTypes.GoToOption, focus: Focus.Last }) }) @@ -262,11 +262,11 @@ const Button = forwardRefWithAs(function Button< if (props.disabled) return if (state.listboxState === ListboxStates.Open) { dispatch({ type: ActionTypes.CloseListbox }) - d.nextFrame(() => state.buttonRef.current?.focus()) + d.nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true })) } else { event.preventDefault() dispatch({ type: ActionTypes.OpenListbox }) - d.nextFrame(() => state.optionsRef.current?.focus()) + d.nextFrame(() => state.optionsRef.current?.focus({ preventScroll: true })) } }, [dispatch, d, state, props.disabled] @@ -309,9 +309,10 @@ function Label( const [state] = useListboxContext([Listbox.name, Label.name].join('.')) const id = `headlessui-listbox-label-${useId()}` - const handlePointerUp = React.useCallback(() => state.buttonRef.current?.focus(), [ - state.buttonRef, - ]) + const handlePointerUp = React.useCallback( + () => state.buttonRef.current?.focus({ preventScroll: true }), + [state.buttonRef] + ) const propsBag = React.useMemo( () => ({ open: state.listboxState === ListboxStates.Open }), @@ -370,7 +371,7 @@ const Options = forwardRefWithAs(function Options< const { dataRef } = state.options[state.activeOptionIndex] state.propsRef.current.onChange(dataRef.current.value) } - disposables().nextFrame(() => state.buttonRef.current?.focus()) + disposables().nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true })) break case Keys.ArrowDown: @@ -394,7 +395,7 @@ const Options = forwardRefWithAs(function Options< case Keys.Escape: event.preventDefault() dispatch({ type: ActionTypes.CloseListbox }) - return d.nextFrame(() => state.buttonRef.current?.focus()) + return d.nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true })) case Keys.Tab: return event.preventDefault() @@ -516,7 +517,7 @@ function Option< if (disabled) return event.preventDefault() select() dispatch({ type: ActionTypes.CloseListbox }) - disposables().nextFrame(() => state.buttonRef.current?.focus()) + disposables().nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true })) }, [dispatch, state.buttonRef, disabled, select] ) diff --git a/packages/@headlessui-react/src/components/menu/menu.tsx b/packages/@headlessui-react/src/components/menu/menu.tsx index f597a28..f263c6c 100644 --- a/packages/@headlessui-react/src/components/menu/menu.tsx +++ b/packages/@headlessui-react/src/components/menu/menu.tsx @@ -157,7 +157,7 @@ export function Menu( if (!itemsRef.current?.contains(target)) dispatch({ type: ActionTypes.CloseMenu }) if (active !== document.body && active?.contains(target)) return // Keep focus on newly clicked/focused element - if (!event.defaultPrevented) buttonRef.current?.focus() + if (!event.defaultPrevented) buttonRef.current?.focus({ preventScroll: true }) } window.addEventListener('click', handler) @@ -209,7 +209,7 @@ const Button = forwardRefWithAs(function Button< event.preventDefault() dispatch({ type: ActionTypes.OpenMenu }) d.nextFrame(() => { - state.itemsRef.current?.focus() + state.itemsRef.current?.focus({ preventScroll: true }) dispatch({ type: ActionTypes.GoToItem, focus: Focus.First }) }) break @@ -218,7 +218,7 @@ const Button = forwardRefWithAs(function Button< event.preventDefault() dispatch({ type: ActionTypes.OpenMenu }) d.nextFrame(() => { - state.itemsRef.current?.focus() + state.itemsRef.current?.focus({ preventScroll: true }) dispatch({ type: ActionTypes.GoToItem, focus: Focus.Last }) }) break @@ -232,11 +232,11 @@ const Button = forwardRefWithAs(function Button< if (props.disabled) return if (state.menuState === MenuStates.Open) { dispatch({ type: ActionTypes.CloseMenu }) - d.nextFrame(() => state.buttonRef.current?.focus()) + d.nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true })) } else { event.preventDefault() dispatch({ type: ActionTypes.OpenMenu }) - d.nextFrame(() => state.itemsRef.current?.focus()) + d.nextFrame(() => state.itemsRef.current?.focus({ preventScroll: true })) } }, [dispatch, d, state, props.disabled] @@ -306,7 +306,7 @@ const Items = forwardRefWithAs(function Items< const { id } = state.items[state.activeItemIndex] document.getElementById(id)?.click() } - disposables().nextFrame(() => state.buttonRef.current?.focus()) + disposables().nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true })) break case Keys.ArrowDown: @@ -330,7 +330,7 @@ const Items = forwardRefWithAs(function Items< case Keys.Escape: event.preventDefault() dispatch({ type: ActionTypes.CloseMenu }) - disposables().nextFrame(() => state.buttonRef.current?.focus()) + disposables().nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true })) break case Keys.Tab: @@ -415,7 +415,7 @@ function Item( (event: { preventDefault: Function }) => { if (disabled) return event.preventDefault() dispatch({ type: ActionTypes.CloseMenu }) - disposables().nextFrame(() => state.buttonRef.current?.focus()) + disposables().nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true })) if (onClick) return onClick(event) }, [dispatch, state.buttonRef, disabled, onClick] diff --git a/packages/@headlessui-react/src/components/switch/switch.tsx b/packages/@headlessui-react/src/components/switch/switch.tsx index 59ac142..b254de1 100644 --- a/packages/@headlessui-react/src/components/switch/switch.tsx +++ b/packages/@headlessui-react/src/components/switch/switch.tsx @@ -134,7 +134,7 @@ function Label( const handlePointerUp = React.useCallback(() => { if (!state.switch) return state.switch.click() - state.switch.focus() + state.switch.focus({ preventScroll: true }) }, [state.switch]) const propsWeControl = { ref: state.setLabel, id, onPointerUp: handlePointerUp } diff --git a/packages/@headlessui-vue/src/components/listbox/listbox.ts b/packages/@headlessui-vue/src/components/listbox/listbox.ts index 7028422..6e79ab2 100644 --- a/packages/@headlessui-vue/src/components/listbox/listbox.ts +++ b/packages/@headlessui-vue/src/components/listbox/listbox.ts @@ -167,7 +167,7 @@ export const Listbox = defineComponent({ if (!optionsRef.value?.contains(target)) api.closeListbox() if (active !== document.body && active?.contains(target)) return // Keep focus on newly clicked/focused element - if (!event.defaultPrevented) buttonRef.value?.focus() + if (!event.defaultPrevented) buttonRef.value?.focus({ preventScroll: true }) } window.addEventListener('click', handler) @@ -210,7 +210,7 @@ export const ListboxLabel = defineComponent({ id, el: api.labelRef, handlePointerUp() { - api.buttonRef.value?.focus() + api.buttonRef.value?.focus({ preventScroll: true }) }, } }, @@ -263,7 +263,7 @@ export const ListboxButton = defineComponent({ event.preventDefault() api.openListbox() nextTick(() => { - api.optionsRef.value?.focus() + api.optionsRef.value?.focus({ preventScroll: true }) if (!api.value.value) api.goToOption(Focus.First) }) break @@ -272,7 +272,7 @@ export const ListboxButton = defineComponent({ event.preventDefault() api.openListbox() nextTick(() => { - api.optionsRef.value?.focus() + api.optionsRef.value?.focus({ preventScroll: true }) if (!api.value.value) api.goToOption(Focus.Last) }) break @@ -283,11 +283,11 @@ export const ListboxButton = defineComponent({ if (props.disabled) return if (api.listboxState.value === ListboxStates.Open) { api.closeListbox() - nextTick(() => api.buttonRef.value?.focus()) + nextTick(() => api.buttonRef.value?.focus({ preventScroll: true })) } else { event.preventDefault() api.openListbox() - nextFrame(() => api.optionsRef.value?.focus()) + nextFrame(() => api.optionsRef.value?.focus({ preventScroll: true })) } } @@ -356,7 +356,7 @@ export const ListboxOptions = defineComponent({ api.select(dataRef.value) } api.closeListbox() - nextTick(() => api.buttonRef.value?.focus()) + nextTick(() => api.buttonRef.value?.focus({ preventScroll: true })) break case Keys.ArrowDown: @@ -380,7 +380,7 @@ export const ListboxOptions = defineComponent({ case Keys.Escape: event.preventDefault() api.closeListbox() - nextTick(() => api.buttonRef.value?.focus()) + nextTick(() => api.buttonRef.value?.focus({ preventScroll: true })) break case Keys.Tab: @@ -456,7 +456,7 @@ export const ListboxOption = defineComponent({ if (disabled) return event.preventDefault() api.select(value) api.closeListbox() - nextTick(() => api.buttonRef.value?.focus()) + nextTick(() => api.buttonRef.value?.focus({ preventScroll: true })) } function handleFocus() { diff --git a/packages/@headlessui-vue/src/components/menu/menu.ts b/packages/@headlessui-vue/src/components/menu/menu.ts index 2cf3d5d..8d6aca2 100644 --- a/packages/@headlessui-vue/src/components/menu/menu.ts +++ b/packages/@headlessui-vue/src/components/menu/menu.ts @@ -144,7 +144,7 @@ export const Menu = defineComponent({ if (!itemsRef.value?.contains(target)) api.closeMenu() if (active !== document.body && active?.contains(target)) return // Keep focus on newly clicked/focused element - if (!event.defaultPrevented) buttonRef.value?.focus() + if (!event.defaultPrevented) buttonRef.value?.focus({ preventScroll: true }) } window.addEventListener('click', handler) @@ -202,7 +202,7 @@ export const MenuButton = defineComponent({ event.preventDefault() api.openMenu() nextTick(() => { - api.itemsRef.value?.focus() + api.itemsRef.value?.focus({ preventScroll: true }) api.goToItem(Focus.First) }) break @@ -211,7 +211,7 @@ export const MenuButton = defineComponent({ event.preventDefault() api.openMenu() nextTick(() => { - api.itemsRef.value?.focus() + api.itemsRef.value?.focus({ preventScroll: true }) api.goToItem(Focus.Last) }) break @@ -222,11 +222,11 @@ export const MenuButton = defineComponent({ if (props.disabled) return if (api.menuState.value === MenuStates.Open) { api.closeMenu() - nextTick(() => api.buttonRef.value?.focus()) + nextTick(() => api.buttonRef.value?.focus({ preventScroll: true })) } else { event.preventDefault() api.openMenu() - nextFrame(() => api.itemsRef.value?.focus()) + nextFrame(() => api.itemsRef.value?.focus({ preventScroll: true })) } } @@ -297,7 +297,7 @@ export const MenuItems = defineComponent({ document.getElementById(id)?.click() } api.closeMenu() - nextTick(() => api.buttonRef.value?.focus()) + nextTick(() => api.buttonRef.value?.focus({ preventScroll: true })) break case Keys.ArrowDown: @@ -321,7 +321,7 @@ export const MenuItems = defineComponent({ case Keys.Escape: event.preventDefault() api.closeMenu() - nextTick(() => api.buttonRef.value?.focus()) + nextTick(() => api.buttonRef.value?.focus({ preventScroll: true })) break case Keys.Tab: @@ -373,7 +373,7 @@ export const MenuItem = defineComponent({ function handleClick(event: MouseEvent) { if (disabled) return event.preventDefault() api.closeMenu() - nextTick(() => api.buttonRef.value?.focus()) + nextTick(() => api.buttonRef.value?.focus({ preventScroll: true })) } function handleFocus() { diff --git a/packages/@headlessui-vue/src/components/switch/switch.ts b/packages/@headlessui-vue/src/components/switch/switch.ts index 81af7bf..8b8511a 100644 --- a/packages/@headlessui-vue/src/components/switch/switch.ts +++ b/packages/@headlessui-vue/src/components/switch/switch.ts @@ -136,7 +136,7 @@ export const SwitchLabel = defineComponent({ el: api.labelRef, handlePointerUp() { api.switchRef.value?.click() - api.switchRef.value?.focus() + api.switchRef.value?.focus({ preventScroll: true }) }, } },