Skip to content

Commit ca56a15

Browse files
Fix hover scroll (#1161)
* disable scroll when hover list item * change API a bit * fix scroll into view For keyboard only for Combobox, Listbox and Menu for both React and Vue. * update changelog Co-authored-by: yuta-ike <[email protected]>
1 parent 57e1ec8 commit ca56a15

File tree

10 files changed

+179
-55
lines changed

10 files changed

+179
-55
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Forward the `ref` to all components ([#1116](https://github.com/tailwindlabs/headlessui/pull/1116))
1313
- Ensure links are triggered inside `Popover Panel` components ([#1153](https://github.com/tailwindlabs/headlessui/pull/1153))
1414
- Improve SSR for `Tab` component ([#1155](https://github.com/tailwindlabs/headlessui/pull/1155))
15+
- Fix `hover` scroll ([#1161](https://github.com/tailwindlabs/headlessui/pull/1161))
1516

1617
## [Unreleased - @headlessui/vue]
1718

@@ -21,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2122
- Ensure that you can close the combobox initially ([#1148](https://github.com/tailwindlabs/headlessui/pull/1148))
2223
- Fix Dialog usage in Tabs ([#1149](https://github.com/tailwindlabs/headlessui/pull/1149))
2324
- Ensure links are triggered inside `Popover Panel` components ([#1153](https://github.com/tailwindlabs/headlessui/pull/1153))
25+
- Fix `hover` scroll ([#1161](https://github.com/tailwindlabs/headlessui/pull/1161))
2426

2527
## [@headlessui/react@v1.5.0] - 2022-02-17
2628

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

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ enum ComboboxStates {
4141
Closed,
4242
}
4343

44+
enum ActivationTrigger {
45+
Pointer,
46+
Other,
47+
}
48+
4449
type ComboboxOptionDataRef = MutableRefObject<{
4550
textValue?: string
4651
disabled: boolean
@@ -70,6 +75,7 @@ interface StateDefinition {
7075
disabled: boolean
7176
options: { id: string; dataRef: ComboboxOptionDataRef }[]
7277
activeOptionIndex: number | null
78+
activationTrigger: ActivationTrigger
7379
}
7480

7581
enum ActionTypes {
@@ -88,8 +94,12 @@ type Actions =
8894
| { type: ActionTypes.CloseCombobox }
8995
| { type: ActionTypes.OpenCombobox }
9096
| { type: ActionTypes.SetDisabled; disabled: boolean }
91-
| { type: ActionTypes.GoToOption; focus: Focus.Specific; id: string }
92-
| { type: ActionTypes.GoToOption; focus: Exclude<Focus, Focus.Specific> }
97+
| { type: ActionTypes.GoToOption; focus: Focus.Specific; id: string; trigger?: ActivationTrigger }
98+
| {
99+
type: ActionTypes.GoToOption
100+
focus: Exclude<Focus, Focus.Specific>
101+
trigger?: ActivationTrigger
102+
}
93103
| { type: ActionTypes.RegisterOption; id: string; dataRef: ComboboxOptionDataRef }
94104
| { type: ActionTypes.UnregisterOption; id: string }
95105

@@ -130,7 +140,11 @@ let reducers: {
130140
})
131141

132142
if (state.activeOptionIndex === activeOptionIndex) return state
133-
return { ...state, activeOptionIndex }
143+
return {
144+
...state,
145+
activeOptionIndex,
146+
activationTrigger: action.trigger ?? ActivationTrigger.Other,
147+
}
134148
},
135149
[ActionTypes.RegisterOption]: (state, action) => {
136150
let currentActiveOption =
@@ -158,6 +172,7 @@ let reducers: {
158172
// the correct index.
159173
return options.indexOf(currentActiveOption)
160174
})(),
175+
activationTrigger: ActivationTrigger.Other,
161176
}
162177

163178
if (
@@ -189,6 +204,7 @@ let reducers: {
189204
// fix this, we will find the correct (new) index position.
190205
return nextOptions.indexOf(currentActiveOption)
191206
})(),
207+
activationTrigger: ActivationTrigger.Other,
192208
}
193209
},
194210
}
@@ -275,6 +291,7 @@ let ComboboxRoot = forwardRefWithAs(function Combobox<
275291
disabled,
276292
options: [],
277293
activeOptionIndex: null,
294+
activationTrigger: ActivationTrigger.Other,
278295
} as StateDefinition)
279296
let [{ comboboxState, options, activeOptionIndex, optionsRef, inputRef, buttonRef }, dispatch] =
280297
reducerBag
@@ -882,12 +899,13 @@ let Option = forwardRefWithAs(function Option<
882899
if (state.comboboxState !== ComboboxStates.Open) return
883900
if (!active) return
884901
if (!enableScrollIntoView.current) return
902+
if (state.activationTrigger === ActivationTrigger.Pointer) return
885903
let d = disposables()
886904
d.requestAnimationFrame(() => {
887905
document.getElementById(id)?.scrollIntoView?.({ block: 'nearest' })
888906
})
889907
return d.dispose
890-
}, [id, active, state.comboboxState, /* We also want to trigger this when the position of the active item changes so that we can re-trigger the scrollIntoView */ state.activeOptionIndex])
908+
}, [id, active, state.comboboxState, state.activationTrigger, /* We also want to trigger this when the position of the active item changes so that we can re-trigger the scrollIntoView */ state.activeOptionIndex])
891909

892910
let handleClick = useCallback(
893911
(event: { preventDefault: Function }) => {
@@ -907,7 +925,12 @@ let Option = forwardRefWithAs(function Option<
907925
let handleMove = useCallback(() => {
908926
if (disabled) return
909927
if (active) return
910-
dispatch({ type: ActionTypes.GoToOption, focus: Focus.Specific, id })
928+
dispatch({
929+
type: ActionTypes.GoToOption,
930+
focus: Focus.Specific,
931+
id,
932+
trigger: ActivationTrigger.Pointer,
933+
})
911934
}, [disabled, active, id, dispatch])
912935

913936
let handleLeave = useCallback(() => {

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

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ enum ListboxStates {
3939
Closed,
4040
}
4141

42+
enum ActivationTrigger {
43+
Pointer,
44+
Other,
45+
}
46+
4247
type ListboxOptionDataRef = MutableRefObject<{
4348
textValue?: string
4449
disabled: boolean
@@ -59,6 +64,7 @@ interface StateDefinition {
5964
options: { id: string; dataRef: ListboxOptionDataRef }[]
6065
searchQuery: string
6166
activeOptionIndex: number | null
67+
activationTrigger: ActivationTrigger
6268
}
6369

6470
enum ActionTypes {
@@ -81,8 +87,12 @@ type Actions =
8187
| { type: ActionTypes.OpenListbox }
8288
| { type: ActionTypes.SetDisabled; disabled: boolean }
8389
| { type: ActionTypes.SetOrientation; orientation: StateDefinition['orientation'] }
84-
| { type: ActionTypes.GoToOption; focus: Focus.Specific; id: string }
85-
| { type: ActionTypes.GoToOption; focus: Exclude<Focus, Focus.Specific> }
90+
| { type: ActionTypes.GoToOption; focus: Focus.Specific; id: string; trigger?: ActivationTrigger }
91+
| {
92+
type: ActionTypes.GoToOption
93+
focus: Exclude<Focus, Focus.Specific>
94+
trigger?: ActivationTrigger
95+
}
8696
| { type: ActionTypes.Search; value: string }
8797
| { type: ActionTypes.ClearSearch }
8898
| { type: ActionTypes.RegisterOption; id: string; dataRef: ListboxOptionDataRef }
@@ -124,7 +134,12 @@ let reducers: {
124134
})
125135

126136
if (state.searchQuery === '' && state.activeOptionIndex === activeOptionIndex) return state
127-
return { ...state, searchQuery: '', activeOptionIndex }
137+
return {
138+
...state,
139+
searchQuery: '',
140+
activeOptionIndex,
141+
activationTrigger: action.trigger ?? ActivationTrigger.Other,
142+
}
128143
},
129144
[ActionTypes.Search]: (state, action) => {
130145
if (state.disabled) return state
@@ -151,7 +166,12 @@ let reducers: {
151166
let matchIdx = matchingOption ? state.options.indexOf(matchingOption) : -1
152167

153168
if (matchIdx === -1 || matchIdx === state.activeOptionIndex) return { ...state, searchQuery }
154-
return { ...state, searchQuery, activeOptionIndex: matchIdx }
169+
return {
170+
...state,
171+
searchQuery,
172+
activeOptionIndex: matchIdx,
173+
activationTrigger: ActivationTrigger.Other,
174+
}
155175
},
156176
[ActionTypes.ClearSearch](state) {
157177
if (state.disabled) return state
@@ -193,6 +213,7 @@ let reducers: {
193213
// fix this, we will find the correct (new) index position.
194214
return nextOptions.indexOf(currentActiveOption)
195215
})(),
216+
activationTrigger: ActivationTrigger.Other,
196217
}
197218
},
198219
}
@@ -249,6 +270,7 @@ let ListboxRoot = forwardRefWithAs(function Listbox<
249270
options: [],
250271
searchQuery: '',
251272
activeOptionIndex: null,
273+
activationTrigger: ActivationTrigger.Other,
252274
} as StateDefinition)
253275
let [{ listboxState, propsRef, optionsRef, buttonRef }, dispatch] = reducerBag
254276

@@ -674,12 +696,13 @@ let Option = forwardRefWithAs(function Option<
674696
useIsoMorphicEffect(() => {
675697
if (state.listboxState !== ListboxStates.Open) return
676698
if (!active) return
699+
if (state.activationTrigger === ActivationTrigger.Pointer) return
677700
let d = disposables()
678701
d.requestAnimationFrame(() => {
679702
document.getElementById(id)?.scrollIntoView?.({ block: 'nearest' })
680703
})
681704
return d.dispose
682-
}, [id, active, state.listboxState, /* We also want to trigger this when the position of the active item changes so that we can re-trigger the scrollIntoView */ state.activeOptionIndex])
705+
}, [id, active, state.listboxState, state.activationTrigger, /* We also want to trigger this when the position of the active item changes so that we can re-trigger the scrollIntoView */ state.activeOptionIndex])
683706

684707
let handleClick = useCallback(
685708
(event: { preventDefault: Function }) => {
@@ -699,7 +722,12 @@ let Option = forwardRefWithAs(function Option<
699722
let handleMove = useCallback(() => {
700723
if (disabled) return
701724
if (active) return
702-
dispatch({ type: ActionTypes.GoToOption, focus: Focus.Specific, id })
725+
dispatch({
726+
type: ActionTypes.GoToOption,
727+
focus: Focus.Specific,
728+
id,
729+
trigger: ActivationTrigger.Pointer,
730+
})
703731
}, [disabled, active, id, dispatch])
704732

705733
let handleLeave = useCallback(() => {

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

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ enum MenuStates {
4141
Closed,
4242
}
4343

44+
enum ActivationTrigger {
45+
Pointer,
46+
Other,
47+
}
48+
4449
type MenuItemDataRef = MutableRefObject<{ textValue?: string; disabled: boolean }>
4550

4651
interface StateDefinition {
@@ -50,6 +55,7 @@ interface StateDefinition {
5055
items: { id: string; dataRef: MenuItemDataRef }[]
5156
searchQuery: string
5257
activeItemIndex: number | null
58+
activationTrigger: ActivationTrigger
5359
}
5460

5561
enum ActionTypes {
@@ -66,8 +72,12 @@ enum ActionTypes {
6672
type Actions =
6773
| { type: ActionTypes.CloseMenu }
6874
| { type: ActionTypes.OpenMenu }
69-
| { type: ActionTypes.GoToItem; focus: Focus.Specific; id: string }
70-
| { type: ActionTypes.GoToItem; focus: Exclude<Focus, Focus.Specific> }
75+
| { type: ActionTypes.GoToItem; focus: Focus.Specific; id: string; trigger?: ActivationTrigger }
76+
| {
77+
type: ActionTypes.GoToItem
78+
focus: Exclude<Focus, Focus.Specific>
79+
trigger?: ActivationTrigger
80+
}
7181
| { type: ActionTypes.Search; value: string }
7282
| { type: ActionTypes.ClearSearch }
7383
| { type: ActionTypes.RegisterItem; id: string; dataRef: MenuItemDataRef }
@@ -96,7 +106,12 @@ let reducers: {
96106
})
97107

98108
if (state.searchQuery === '' && state.activeItemIndex === activeItemIndex) return state
99-
return { ...state, searchQuery: '', activeItemIndex }
109+
return {
110+
...state,
111+
searchQuery: '',
112+
activeItemIndex,
113+
activationTrigger: action.trigger ?? ActivationTrigger.Other,
114+
}
100115
},
101116
[ActionTypes.Search]: (state, action) => {
102117
let wasAlreadySearching = state.searchQuery !== ''
@@ -117,7 +132,12 @@ let reducers: {
117132

118133
let matchIdx = matchingItem ? state.items.indexOf(matchingItem) : -1
119134
if (matchIdx === -1 || matchIdx === state.activeItemIndex) return { ...state, searchQuery }
120-
return { ...state, searchQuery, activeItemIndex: matchIdx }
135+
return {
136+
...state,
137+
searchQuery,
138+
activeItemIndex: matchIdx,
139+
activationTrigger: ActivationTrigger.Other,
140+
}
121141
},
122142
[ActionTypes.ClearSearch](state) {
123143
if (state.searchQuery === '') return state
@@ -156,6 +176,7 @@ let reducers: {
156176
// fix this, we will find the correct (new) index position.
157177
return nextItems.indexOf(currentActiveItem)
158178
})(),
179+
activationTrigger: ActivationTrigger.Other,
159180
}
160181
},
161182
}
@@ -195,6 +216,7 @@ let MenuRoot = forwardRefWithAs(function Menu<TTag extends ElementType = typeof
195216
items: [],
196217
searchQuery: '',
197218
activeItemIndex: null,
219+
activationTrigger: ActivationTrigger.Other,
198220
} as StateDefinition)
199221
let [{ menuState, itemsRef, buttonRef }, dispatch] = reducerBag
200222
let menuRef = useSyncRefs(ref)
@@ -543,12 +565,13 @@ let Item = forwardRefWithAs(function Item<TTag extends ElementType = typeof DEFA
543565
useIsoMorphicEffect(() => {
544566
if (state.menuState !== MenuStates.Open) return
545567
if (!active) return
568+
if (state.activationTrigger === ActivationTrigger.Pointer) return
546569
let d = disposables()
547570
d.requestAnimationFrame(() => {
548571
document.getElementById(id)?.scrollIntoView?.({ block: 'nearest' })
549572
})
550573
return d.dispose
551-
}, [id, active, state.menuState, /* We also want to trigger this when the position of the active item changes so that we can re-trigger the scrollIntoView */ state.activeItemIndex])
574+
}, [id, active, state.menuState, state.activationTrigger, /* We also want to trigger this when the position of the active item changes so that we can re-trigger the scrollIntoView */ state.activeItemIndex])
552575

553576
let bag = useRef<MenuItemDataRef['current']>({ disabled })
554577

@@ -583,7 +606,12 @@ let Item = forwardRefWithAs(function Item<TTag extends ElementType = typeof DEFA
583606
let handleMove = useCallback(() => {
584607
if (disabled) return
585608
if (active) return
586-
dispatch({ type: ActionTypes.GoToItem, focus: Focus.Specific, id })
609+
dispatch({
610+
type: ActionTypes.GoToItem,
611+
focus: Focus.Specific,
612+
id,
613+
trigger: ActivationTrigger.Pointer,
614+
})
587615
}, [disabled, active, id, dispatch])
588616

589617
let handleLeave = useCallback(() => {

packages/@headlessui-react/src/components/popover/popover.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1862,7 +1862,7 @@ describe('Keyboard interactions', () => {
18621862
assertActiveElement(getPopoverButton())
18631863

18641864
// Verify that we got redirected to the href
1865-
expect(window.location.hash).toEqual('#closed')
1865+
expect(document.location.hash).toEqual('#closed')
18661866
})
18671867
)
18681868
})

0 commit comments

Comments
 (0)