Skip to content

Commit d950146

Browse files
RobinMalfaitJarrku
andauthored
add use tree walker hook (#316)
* add useTreeWalker hooks We got a PR to fix the createTreeWalker so that it also works in IE11. We don't actively support IE11, so if things work (with polyfills) then it's good but I don't want to maintain IE11 specific code. That said, I wanted to abstract the createTreeWalker code to a nice little hook. The fix for IE is also pretty small, it uses a function instead of an object and it has a last argument that is deprecated, but has no obvious effect for our use cases. Since the incoming PR was based on the `main` branch (where we only had 1 reference to createTreeWalker), I wanted to make sure that we got all the references on the latest `develop` branch. Closes: #295 Co-authored-by: Simon VDB <[email protected]> * use useTreeWalker hook Co-authored-by: Simon VDB <[email protected]>
1 parent acbc4d7 commit d950146

File tree

6 files changed

+121
-65
lines changed

6 files changed

+121
-65
lines changed

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

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { Focus, calculateActiveIndex } from '../../utils/calculate-active-index'
3232
import { isDisabledReactIssue7711 } from '../../utils/bugs'
3333
import { isFocusableElement, FocusableMode } from '../../utils/focus-management'
3434
import { useWindowEvent } from '../../hooks/use-window-event'
35+
import { useTreeWalker } from '../../hooks/use-tree-walker'
3536

3637
enum MenuStates {
3738
Open,
@@ -338,23 +339,18 @@ let Items = forwardRefWithAs(function Items<TTag extends ElementType = typeof DE
338339
container.focus({ preventScroll: true })
339340
}, [state.menuState, state.itemsRef])
340341

341-
useIsoMorphicEffect(() => {
342-
let container = state.itemsRef.current
343-
if (!container) return
344-
if (state.menuState !== MenuStates.Open) return
345-
346-
let walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
347-
acceptNode(node: HTMLElement) {
348-
if (node.getAttribute('role') === 'menuitem') return NodeFilter.FILTER_REJECT
349-
if (node.hasAttribute('role')) return NodeFilter.FILTER_SKIP
350-
return NodeFilter.FILTER_ACCEPT
351-
},
352-
})
353-
354-
while (walker.nextNode()) {
355-
;(walker.currentNode as HTMLElement).setAttribute('role', 'none')
356-
}
357-
}, [state.menuState, state.itemsRef])
342+
useTreeWalker({
343+
container: state.itemsRef.current,
344+
enabled: state.menuState === MenuStates.Open,
345+
accept(node) {
346+
if (node.getAttribute('role') === 'menuitem') return NodeFilter.FILTER_REJECT
347+
if (node.hasAttribute('role')) return NodeFilter.FILTER_SKIP
348+
return NodeFilter.FILTER_ACCEPT
349+
},
350+
walk(node) {
351+
node.setAttribute('role', 'none')
352+
},
353+
})
358354

359355
let handleKeyDown = useCallback(
360356
(event: ReactKeyboardEvent<HTMLDivElement>) => {

packages/@headlessui-react/src/components/radio-group/radio-group.tsx

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { focusIn, Focus, FocusResult } from '../../utils/focus-management'
2323
import { useFlags } from '../../hooks/use-flags'
2424
import { Label, useLabels } from '../../components/label/label'
2525
import { Description, useDescriptions } from '../../components/description/description'
26+
import { useTreeWalker } from '../../hooks/use-tree-walker'
2627

2728
interface Option {
2829
id: string
@@ -131,22 +132,17 @@ export function RadioGroup<
131132
[onChange, value]
132133
)
133134

134-
useIsoMorphicEffect(() => {
135-
let container = radioGroupRef.current
136-
if (!container) return
137-
138-
let walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
139-
acceptNode(node: HTMLElement) {
140-
if (node.getAttribute('role') === 'radio') return NodeFilter.FILTER_REJECT
141-
if (node.hasAttribute('role')) return NodeFilter.FILTER_SKIP
142-
return NodeFilter.FILTER_ACCEPT
143-
},
144-
})
145-
146-
while (walker.nextNode()) {
147-
;(walker.currentNode as HTMLElement).setAttribute('role', 'none')
148-
}
149-
}, [radioGroupRef])
135+
useTreeWalker({
136+
container: radioGroupRef.current,
137+
accept(node) {
138+
if (node.getAttribute('role') === 'radio') return NodeFilter.FILTER_REJECT
139+
if (node.hasAttribute('role')) return NodeFilter.FILTER_SKIP
140+
return NodeFilter.FILTER_ACCEPT
141+
},
142+
walk(node) {
143+
node.setAttribute('role', 'none')
144+
},
145+
})
150146

151147
let handleKeyDown = useCallback(
152148
(event: ReactKeyboardEvent<HTMLButtonElement>) => {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useRef, useEffect } from 'react'
2+
import { useIsoMorphicEffect } from './use-iso-morphic-effect'
3+
4+
type AcceptNode = (
5+
node: HTMLElement
6+
) =>
7+
| typeof NodeFilter.FILTER_ACCEPT
8+
| typeof NodeFilter.FILTER_SKIP
9+
| typeof NodeFilter.FILTER_REJECT
10+
11+
export function useTreeWalker({
12+
container,
13+
accept,
14+
walk,
15+
enabled = true,
16+
}: {
17+
container: HTMLElement | null
18+
accept: AcceptNode
19+
walk(node: HTMLElement): void
20+
enabled?: boolean
21+
}) {
22+
let acceptRef = useRef(accept)
23+
let walkRef = useRef(walk)
24+
25+
useEffect(() => {
26+
acceptRef.current = accept
27+
walkRef.current = walk
28+
}, [accept, walk])
29+
30+
useIsoMorphicEffect(() => {
31+
if (!container) return
32+
if (!enabled) return
33+
34+
let accept = acceptRef.current
35+
let walk = walkRef.current
36+
37+
let acceptNode = Object.assign((node: HTMLElement) => accept(node), { acceptNode: accept })
38+
let walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, acceptNode, false)
39+
40+
while (walker.nextNode()) walk(walker.currentNode as HTMLElement)
41+
}, [container, enabled, acceptRef, walkRef])
42+
}

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

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { Focus, calculateActiveIndex } from '../../utils/calculate-active-index'
1818
import { resolvePropValue } from '../../utils/resolve-prop-value'
1919
import { dom } from '../../utils/dom'
2020
import { useWindowEvent } from '../../hooks/use-window-event'
21+
import { useTreeWalker } from '../../hooks/use-tree-walker'
2122

2223
enum MenuStates {
2324
Open,
@@ -291,22 +292,17 @@ export let MenuItems = defineComponent({
291292
let id = `headlessui-menu-items-${useId()}`
292293
let searchDebounce = ref<ReturnType<typeof setTimeout> | null>(null)
293294

294-
watchEffect(() => {
295-
let container = dom(api.itemsRef)
296-
if (!container) return
297-
if (api.menuState.value !== MenuStates.Open) return
298-
299-
let walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
300-
acceptNode(node: HTMLElement) {
301-
if (node.getAttribute('role') === 'menuitem') return NodeFilter.FILTER_REJECT
302-
if (node.hasAttribute('role')) return NodeFilter.FILTER_SKIP
303-
return NodeFilter.FILTER_ACCEPT
304-
},
305-
})
306-
307-
while (walker.nextNode()) {
308-
;(walker.currentNode as HTMLElement).setAttribute('role', 'none')
309-
}
295+
useTreeWalker({
296+
container: computed(() => dom(api.itemsRef)),
297+
enabled: computed(() => api.menuState.value === MenuStates.Open),
298+
accept(node) {
299+
if (node.getAttribute('role') === 'menuitem') return NodeFilter.FILTER_REJECT
300+
if (node.hasAttribute('role')) return NodeFilter.FILTER_SKIP
301+
return NodeFilter.FILTER_ACCEPT
302+
},
303+
walk(node) {
304+
node.setAttribute('role', 'none')
305+
},
310306
})
311307

312308
function handleKeyDown(event: KeyboardEvent) {

packages/@headlessui-vue/src/components/radio-group/radio-group.ts

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
provide,
88
ref,
99
toRaw,
10-
watchEffect,
1110

1211
// Types
1312
InjectionKey,
@@ -22,6 +21,7 @@ import { render } from '../../utils/render'
2221
import { Label, useLabels } from '../label/label'
2322
import { Description, useDescriptions } from '../description/description'
2423
import { resolvePropValue } from '../../utils/resolve-prop-value'
24+
import { useTreeWalker } from '../../hooks/use-tree-walker'
2525

2626
interface Option {
2727
id: string
@@ -121,21 +121,16 @@ export let RadioGroup = defineComponent({
121121
// @ts-expect-error ...
122122
provide(RadioGroupContext, api)
123123

124-
watchEffect(() => {
125-
let container = dom(radioGroupRef)
126-
if (!container) return
127-
128-
let walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
129-
acceptNode(node: HTMLElement) {
130-
if (node.getAttribute('role') === 'radio') return NodeFilter.FILTER_REJECT
131-
if (node.hasAttribute('role')) return NodeFilter.FILTER_SKIP
132-
return NodeFilter.FILTER_ACCEPT
133-
},
134-
})
135-
136-
while (walker.nextNode()) {
137-
;(walker.currentNode as HTMLElement).setAttribute('role', 'none')
138-
}
124+
useTreeWalker({
125+
container: computed(() => dom(radioGroupRef)),
126+
accept(node) {
127+
if (node.getAttribute('role') === 'radio') return NodeFilter.FILTER_REJECT
128+
if (node.hasAttribute('role')) return NodeFilter.FILTER_SKIP
129+
return NodeFilter.FILTER_ACCEPT
130+
},
131+
walk(node) {
132+
node.setAttribute('role', 'none')
133+
},
139134
})
140135

141136
function handleKeyDown(event: KeyboardEvent) {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { watchEffect, ComputedRef } from 'vue'
2+
3+
type AcceptNode = (
4+
node: HTMLElement
5+
) =>
6+
| typeof NodeFilter.FILTER_ACCEPT
7+
| typeof NodeFilter.FILTER_SKIP
8+
| typeof NodeFilter.FILTER_REJECT
9+
10+
export function useTreeWalker({
11+
container,
12+
accept,
13+
walk,
14+
enabled,
15+
}: {
16+
container: ComputedRef<HTMLElement | null>
17+
accept: AcceptNode
18+
walk(node: HTMLElement): void
19+
enabled?: ComputedRef<boolean>
20+
}) {
21+
watchEffect(() => {
22+
let root = container.value
23+
if (!root) return
24+
if (enabled !== undefined && !enabled.value) return
25+
26+
let acceptNode = Object.assign((node: HTMLElement) => accept(node), { acceptNode: accept })
27+
let walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, acceptNode, false)
28+
29+
while (walker.nextNode()) walk(walker.currentNode as HTMLElement)
30+
})
31+
}

0 commit comments

Comments
 (0)