General/random internal cleanup (part 1) (#1484)
* sort React imports * improve type signature of the `useEvent` hook * use more correct `useIsoMorphicEffect` check in `useEvent` * refactor `useCallback` to cleaner `useEvent` * convert `const` to `let` Just for consistency.. * cleanup `Tabs` code Created explicit functions that can be called from child components instead of calling `dispatch` directly. Introduced a `useData` and `useActions` hook to make child components easier. The seperation of `useData` allows us to pass down props directly instead of going via the `useReducer` hook and dispatching actions to make values up to date. * cleanup `Combobox` code * cleanup `RadioGroup` code
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
@@ -16,6 +15,7 @@ import { useId } from '../../hooks/use-id'
|
||||
import { forwardRefWithAs, render } from '../../utils/render'
|
||||
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
|
||||
import { useSyncRefs } from '../../hooks/use-sync-refs'
|
||||
import { useEvent } from '../../hooks/use-event'
|
||||
|
||||
// ---
|
||||
|
||||
@@ -58,7 +58,7 @@ export function useDescriptions(): [
|
||||
// The provider component
|
||||
useMemo(() => {
|
||||
return function DescriptionProvider(props: DescriptionProviderProps) {
|
||||
let register = useCallback((value: string) => {
|
||||
let register = useEvent((value: string) => {
|
||||
setDescriptionIds((existing) => [...existing, value])
|
||||
|
||||
return () =>
|
||||
@@ -68,7 +68,7 @@ export function useDescriptions(): [
|
||||
if (idx !== -1) clone.splice(idx, 1)
|
||||
return clone
|
||||
})
|
||||
}, [])
|
||||
})
|
||||
|
||||
let contextBag = useMemo(
|
||||
() => ({ register, slot: props.slot, name: props.name, props: props.props }),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// WAI-ARIA: https://www.w3.org/TR/wai-aria-practices-1.2/#dialog_modal
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
createRef,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
@@ -15,7 +15,6 @@ import React, {
|
||||
MouseEvent as ReactMouseEvent,
|
||||
MutableRefObject,
|
||||
Ref,
|
||||
createRef,
|
||||
} from 'react'
|
||||
|
||||
import { Props } from '../../types'
|
||||
@@ -38,6 +37,7 @@ import { getOwnerDocument } from '../../utils/owner'
|
||||
import { useOwnerDocument } from '../../hooks/use-owner'
|
||||
import { useEventListener } from '../../hooks/use-event-listener'
|
||||
import { Hidden, Features as HiddenFeatures } from '../../internal/hidden'
|
||||
import { useEvent } from '../../hooks/use-event'
|
||||
|
||||
enum DialogStates {
|
||||
Open,
|
||||
@@ -184,12 +184,9 @@ let DialogRoot = forwardRefWithAs(function Dialog<
|
||||
panelRef: createRef(),
|
||||
} as StateDefinition)
|
||||
|
||||
let close = useCallback(() => onClose(false), [onClose])
|
||||
let close = useEvent(() => onClose(false))
|
||||
|
||||
let setTitleId = useCallback(
|
||||
(id: string | null) => dispatch({ type: ActionTypes.SetTitleId, id }),
|
||||
[dispatch]
|
||||
)
|
||||
let setTitleId = useEvent((id: string | null) => dispatch({ type: ActionTypes.SetTitleId, id }))
|
||||
|
||||
let ready = useServerHandoffComplete()
|
||||
let enabled = ready ? (__demoMode ? false : dialogState === DialogStates.Open) : false
|
||||
@@ -323,7 +320,7 @@ let DialogRoot = forwardRefWithAs(function Dialog<
|
||||
<StackProvider
|
||||
type="Dialog"
|
||||
element={internalDialogRef}
|
||||
onUpdate={useCallback((message, type, element) => {
|
||||
onUpdate={useEvent((message, type, element) => {
|
||||
if (type !== 'Dialog') return
|
||||
|
||||
match(message, {
|
||||
@@ -336,7 +333,7 @@ let DialogRoot = forwardRefWithAs(function Dialog<
|
||||
setNestedDialogCount((count) => count - 1)
|
||||
},
|
||||
})
|
||||
}, [])}
|
||||
})}
|
||||
>
|
||||
<ForcePortalRoot force={true}>
|
||||
<Portal>
|
||||
@@ -393,16 +390,13 @@ let Overlay = forwardRefWithAs(function Overlay<
|
||||
|
||||
let id = `headlessui-dialog-overlay-${useId()}`
|
||||
|
||||
let handleClick = useCallback(
|
||||
(event: ReactMouseEvent) => {
|
||||
if (event.target !== event.currentTarget) return
|
||||
if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
close()
|
||||
},
|
||||
[close]
|
||||
)
|
||||
let handleClick = useEvent((event: ReactMouseEvent) => {
|
||||
if (event.target !== event.currentTarget) return
|
||||
if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
close()
|
||||
})
|
||||
|
||||
let slot = useMemo<OverlayRenderPropArg>(
|
||||
() => ({ open: dialogState === DialogStates.Open }),
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import React, {
|
||||
Fragment,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
@@ -10,13 +9,13 @@ import React, {
|
||||
useRef,
|
||||
|
||||
// Types
|
||||
ContextType,
|
||||
Dispatch,
|
||||
ElementType,
|
||||
KeyboardEvent as ReactKeyboardEvent,
|
||||
MouseEvent as ReactMouseEvent,
|
||||
Ref,
|
||||
MutableRefObject,
|
||||
ContextType,
|
||||
Ref,
|
||||
} from 'react'
|
||||
|
||||
import { Props } from '../../types'
|
||||
@@ -29,6 +28,7 @@ import { isDisabledReactIssue7711 } from '../../utils/bugs'
|
||||
import { OpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed'
|
||||
import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
|
||||
import { getOwnerDocument } from '../../utils/owner'
|
||||
import { useEvent } from '../../hooks/use-event'
|
||||
|
||||
enum DisclosureStates {
|
||||
Open,
|
||||
@@ -188,24 +188,21 @@ let DisclosureRoot = forwardRefWithAs(function Disclosure<
|
||||
useEffect(() => dispatch({ type: ActionTypes.SetButtonId, buttonId }), [buttonId, dispatch])
|
||||
useEffect(() => dispatch({ type: ActionTypes.SetPanelId, panelId }), [panelId, dispatch])
|
||||
|
||||
let close = useCallback(
|
||||
(focusableElement?: HTMLElement | MutableRefObject<HTMLElement | null>) => {
|
||||
dispatch({ type: ActionTypes.CloseDisclosure })
|
||||
let ownerDocument = getOwnerDocument(internalDisclosureRef)
|
||||
if (!ownerDocument) return
|
||||
let close = useEvent((focusableElement?: HTMLElement | MutableRefObject<HTMLElement | null>) => {
|
||||
dispatch({ type: ActionTypes.CloseDisclosure })
|
||||
let ownerDocument = getOwnerDocument(internalDisclosureRef)
|
||||
if (!ownerDocument) return
|
||||
|
||||
let restoreElement = (() => {
|
||||
if (!focusableElement) return ownerDocument.getElementById(buttonId)
|
||||
if (focusableElement instanceof HTMLElement) return focusableElement
|
||||
if (focusableElement.current instanceof HTMLElement) return focusableElement.current
|
||||
let restoreElement = (() => {
|
||||
if (!focusableElement) return ownerDocument.getElementById(buttonId)
|
||||
if (focusableElement instanceof HTMLElement) return focusableElement
|
||||
if (focusableElement.current instanceof HTMLElement) return focusableElement.current
|
||||
|
||||
return ownerDocument.getElementById(buttonId)
|
||||
})()
|
||||
return ownerDocument.getElementById(buttonId)
|
||||
})()
|
||||
|
||||
restoreElement?.focus()
|
||||
},
|
||||
[dispatch, buttonId]
|
||||
)
|
||||
restoreElement?.focus()
|
||||
})
|
||||
|
||||
let api = useMemo<ContextType<typeof DisclosureAPIContext>>(() => ({ close }), [close])
|
||||
|
||||
@@ -265,35 +262,32 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
|
||||
let internalButtonRef = useRef<HTMLButtonElement | null>(null)
|
||||
let buttonRef = useSyncRefs(internalButtonRef, ref, !isWithinPanel ? state.buttonRef : null)
|
||||
|
||||
let handleKeyDown = useCallback(
|
||||
(event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||
if (isWithinPanel) {
|
||||
if (state.disclosureState === DisclosureStates.Closed) return
|
||||
let handleKeyDown = useEvent((event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||
if (isWithinPanel) {
|
||||
if (state.disclosureState === DisclosureStates.Closed) return
|
||||
|
||||
switch (event.key) {
|
||||
case Keys.Space:
|
||||
case Keys.Enter:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
dispatch({ type: ActionTypes.ToggleDisclosure })
|
||||
state.buttonRef.current?.focus()
|
||||
break
|
||||
}
|
||||
} else {
|
||||
switch (event.key) {
|
||||
case Keys.Space:
|
||||
case Keys.Enter:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
dispatch({ type: ActionTypes.ToggleDisclosure })
|
||||
break
|
||||
}
|
||||
switch (event.key) {
|
||||
case Keys.Space:
|
||||
case Keys.Enter:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
dispatch({ type: ActionTypes.ToggleDisclosure })
|
||||
state.buttonRef.current?.focus()
|
||||
break
|
||||
}
|
||||
},
|
||||
[dispatch, isWithinPanel, state.disclosureState, state.buttonRef]
|
||||
)
|
||||
} else {
|
||||
switch (event.key) {
|
||||
case Keys.Space:
|
||||
case Keys.Enter:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
dispatch({ type: ActionTypes.ToggleDisclosure })
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let handleKeyUp = useCallback((event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||
let handleKeyUp = useEvent((event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||
switch (event.key) {
|
||||
case Keys.Space:
|
||||
// Required for firefox, event.preventDefault() in handleKeyDown for
|
||||
@@ -302,22 +296,19 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
|
||||
event.preventDefault()
|
||||
break
|
||||
}
|
||||
}, [])
|
||||
})
|
||||
|
||||
let handleClick = useCallback(
|
||||
(event: ReactMouseEvent) => {
|
||||
if (isDisabledReactIssue7711(event.currentTarget)) return
|
||||
if (props.disabled) return
|
||||
let handleClick = useEvent((event: ReactMouseEvent) => {
|
||||
if (isDisabledReactIssue7711(event.currentTarget)) return
|
||||
if (props.disabled) return
|
||||
|
||||
if (isWithinPanel) {
|
||||
dispatch({ type: ActionTypes.ToggleDisclosure })
|
||||
state.buttonRef.current?.focus()
|
||||
} else {
|
||||
dispatch({ type: ActionTypes.ToggleDisclosure })
|
||||
}
|
||||
},
|
||||
[dispatch, props.disabled, state.buttonRef, isWithinPanel]
|
||||
)
|
||||
if (isWithinPanel) {
|
||||
dispatch({ type: ActionTypes.ToggleDisclosure })
|
||||
state.buttonRef.current?.focus()
|
||||
} else {
|
||||
dispatch({ type: ActionTypes.ToggleDisclosure })
|
||||
}
|
||||
})
|
||||
|
||||
let slot = useMemo<ButtonRenderPropArg>(
|
||||
() => ({ open: state.disclosureState === DisclosureStates.Open }),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, {
|
||||
useRef,
|
||||
useEffect,
|
||||
useRef,
|
||||
|
||||
// Types
|
||||
ElementType,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
@@ -16,6 +15,7 @@ import { useId } from '../../hooks/use-id'
|
||||
import { forwardRefWithAs, render } from '../../utils/render'
|
||||
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
|
||||
import { useSyncRefs } from '../../hooks/use-sync-refs'
|
||||
import { useEvent } from '../../hooks/use-event'
|
||||
|
||||
// ---
|
||||
|
||||
@@ -53,7 +53,7 @@ export function useLabels(): [string | undefined, (props: LabelProviderProps) =>
|
||||
// The provider component
|
||||
useMemo(() => {
|
||||
return function LabelProvider(props: LabelProviderProps) {
|
||||
let register = useCallback((value: string) => {
|
||||
let register = useEvent((value: string) => {
|
||||
setLabelIds((existing) => [...existing, value])
|
||||
|
||||
return () =>
|
||||
@@ -63,7 +63,7 @@ export function useLabels(): [string | undefined, (props: LabelProviderProps) =>
|
||||
if (idx !== -1) clone.splice(idx, 1)
|
||||
return clone
|
||||
})
|
||||
}, [])
|
||||
})
|
||||
|
||||
let contextBag = useMemo(
|
||||
() => ({ register, slot: props.slot, name: props.name, props: props.props }),
|
||||
|
||||
@@ -2,8 +2,8 @@ import React, {
|
||||
Fragment,
|
||||
createContext,
|
||||
createRef,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useRef,
|
||||
@@ -15,7 +15,6 @@ import React, {
|
||||
MouseEvent as ReactMouseEvent,
|
||||
MutableRefObject,
|
||||
Ref,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
|
||||
import { useDisposables } from '../../hooks/use-disposables'
|
||||
@@ -473,36 +472,33 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
|
||||
let id = `headlessui-listbox-button-${useId()}`
|
||||
let d = useDisposables()
|
||||
|
||||
let handleKeyDown = useCallback(
|
||||
(event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||
switch (event.key) {
|
||||
// Ref: https://www.w3.org/TR/wai-aria-practices-1.2/#keyboard-interaction-13
|
||||
let handleKeyDown = useEvent((event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||
switch (event.key) {
|
||||
// Ref: https://www.w3.org/TR/wai-aria-practices-1.2/#keyboard-interaction-13
|
||||
|
||||
case Keys.Space:
|
||||
case Keys.Enter:
|
||||
case Keys.ArrowDown:
|
||||
event.preventDefault()
|
||||
dispatch({ type: ActionTypes.OpenListbox })
|
||||
d.nextFrame(() => {
|
||||
if (!state.propsRef.current.value)
|
||||
dispatch({ type: ActionTypes.GoToOption, focus: Focus.First })
|
||||
})
|
||||
break
|
||||
case Keys.Space:
|
||||
case Keys.Enter:
|
||||
case Keys.ArrowDown:
|
||||
event.preventDefault()
|
||||
dispatch({ type: ActionTypes.OpenListbox })
|
||||
d.nextFrame(() => {
|
||||
if (!state.propsRef.current.value)
|
||||
dispatch({ type: ActionTypes.GoToOption, focus: Focus.First })
|
||||
})
|
||||
break
|
||||
|
||||
case Keys.ArrowUp:
|
||||
event.preventDefault()
|
||||
dispatch({ type: ActionTypes.OpenListbox })
|
||||
d.nextFrame(() => {
|
||||
if (!state.propsRef.current.value)
|
||||
dispatch({ type: ActionTypes.GoToOption, focus: Focus.Last })
|
||||
})
|
||||
break
|
||||
}
|
||||
},
|
||||
[dispatch, state, d]
|
||||
)
|
||||
case Keys.ArrowUp:
|
||||
event.preventDefault()
|
||||
dispatch({ type: ActionTypes.OpenListbox })
|
||||
d.nextFrame(() => {
|
||||
if (!state.propsRef.current.value)
|
||||
dispatch({ type: ActionTypes.GoToOption, focus: Focus.Last })
|
||||
})
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
let handleKeyUp = useCallback((event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||
let handleKeyUp = useEvent((event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||
switch (event.key) {
|
||||
case Keys.Space:
|
||||
// Required for firefox, event.preventDefault() in handleKeyDown for
|
||||
@@ -511,21 +507,18 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
|
||||
event.preventDefault()
|
||||
break
|
||||
}
|
||||
}, [])
|
||||
})
|
||||
|
||||
let handleClick = useCallback(
|
||||
(event: ReactMouseEvent) => {
|
||||
if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
|
||||
if (state.listboxState === ListboxStates.Open) {
|
||||
dispatch({ type: ActionTypes.CloseListbox })
|
||||
d.nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
|
||||
} else {
|
||||
event.preventDefault()
|
||||
dispatch({ type: ActionTypes.OpenListbox })
|
||||
}
|
||||
},
|
||||
[dispatch, d, state]
|
||||
)
|
||||
let handleClick = useEvent((event: ReactMouseEvent) => {
|
||||
if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
|
||||
if (state.listboxState === ListboxStates.Open) {
|
||||
dispatch({ type: ActionTypes.CloseListbox })
|
||||
d.nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
|
||||
} else {
|
||||
event.preventDefault()
|
||||
dispatch({ type: ActionTypes.OpenListbox })
|
||||
}
|
||||
})
|
||||
|
||||
let labelledby = useComputed(() => {
|
||||
if (!state.labelRef.current) return undefined
|
||||
@@ -577,10 +570,7 @@ let Label = forwardRefWithAs(function Label<TTag extends ElementType = typeof DE
|
||||
let id = `headlessui-listbox-label-${useId()}`
|
||||
let labelRef = useSyncRefs(state.labelRef, ref)
|
||||
|
||||
let handleClick = useCallback(
|
||||
() => state.buttonRef.current?.focus({ preventScroll: true }),
|
||||
[state.buttonRef]
|
||||
)
|
||||
let handleClick = useEvent(() => state.buttonRef.current?.focus({ preventScroll: true }))
|
||||
|
||||
let slot = useMemo<LabelRenderPropArg>(
|
||||
() => ({ open: state.listboxState === ListboxStates.Open, disabled: state.disabled }),
|
||||
@@ -647,78 +637,75 @@ let Options = forwardRefWithAs(function Options<
|
||||
container.focus({ preventScroll: true })
|
||||
}, [state.listboxState, state.optionsRef])
|
||||
|
||||
let handleKeyDown = useCallback(
|
||||
(event: ReactKeyboardEvent<HTMLUListElement>) => {
|
||||
searchDisposables.dispose()
|
||||
let handleKeyDown = useEvent((event: ReactKeyboardEvent<HTMLUListElement>) => {
|
||||
searchDisposables.dispose()
|
||||
|
||||
switch (event.key) {
|
||||
// Ref: https://www.w3.org/TR/wai-aria-practices-1.2/#keyboard-interaction-12
|
||||
switch (event.key) {
|
||||
// Ref: https://www.w3.org/TR/wai-aria-practices-1.2/#keyboard-interaction-12
|
||||
|
||||
// @ts-expect-error Fallthrough is expected here
|
||||
case Keys.Space:
|
||||
if (state.searchQuery !== '') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return dispatch({ type: ActionTypes.Search, value: event.key })
|
||||
}
|
||||
// When in type ahead mode, fallthrough
|
||||
case Keys.Enter:
|
||||
// @ts-expect-error Fallthrough is expected here
|
||||
case Keys.Space:
|
||||
if (state.searchQuery !== '') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return dispatch({ type: ActionTypes.Search, value: event.key })
|
||||
}
|
||||
// When in type ahead mode, fallthrough
|
||||
case Keys.Enter:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
if (state.activeOptionIndex !== null) {
|
||||
let { dataRef } = state.options[state.activeOptionIndex]
|
||||
state.propsRef.current.onChange(dataRef.current.value)
|
||||
}
|
||||
if (state.propsRef.current.mode === ValueMode.Single) {
|
||||
dispatch({ type: ActionTypes.CloseListbox })
|
||||
disposables().nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
|
||||
}
|
||||
break
|
||||
|
||||
case match(state.orientation, { vertical: Keys.ArrowDown, horizontal: Keys.ArrowRight }):
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return dispatch({ type: ActionTypes.GoToOption, focus: Focus.Next })
|
||||
|
||||
case match(state.orientation, { vertical: Keys.ArrowUp, horizontal: Keys.ArrowLeft }):
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return dispatch({ type: ActionTypes.GoToOption, focus: Focus.Previous })
|
||||
|
||||
case Keys.Home:
|
||||
case Keys.PageUp:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return dispatch({ type: ActionTypes.GoToOption, focus: Focus.First })
|
||||
|
||||
case Keys.End:
|
||||
case Keys.PageDown:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return dispatch({ type: ActionTypes.GoToOption, focus: Focus.Last })
|
||||
|
||||
case Keys.Escape:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (state.activeOptionIndex !== null) {
|
||||
let { dataRef } = state.options[state.activeOptionIndex]
|
||||
state.propsRef.current.onChange(dataRef.current.value)
|
||||
}
|
||||
if (state.propsRef.current.mode === ValueMode.Single) {
|
||||
dispatch({ type: ActionTypes.CloseListbox })
|
||||
return d.nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
|
||||
disposables().nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
|
||||
}
|
||||
break
|
||||
|
||||
case Keys.Tab:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
break
|
||||
case match(state.orientation, { vertical: Keys.ArrowDown, horizontal: Keys.ArrowRight }):
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return dispatch({ type: ActionTypes.GoToOption, focus: Focus.Next })
|
||||
|
||||
default:
|
||||
if (event.key.length === 1) {
|
||||
dispatch({ type: ActionTypes.Search, value: event.key })
|
||||
searchDisposables.setTimeout(() => dispatch({ type: ActionTypes.ClearSearch }), 350)
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
[d, dispatch, searchDisposables, state]
|
||||
)
|
||||
case match(state.orientation, { vertical: Keys.ArrowUp, horizontal: Keys.ArrowLeft }):
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return dispatch({ type: ActionTypes.GoToOption, focus: Focus.Previous })
|
||||
|
||||
case Keys.Home:
|
||||
case Keys.PageUp:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return dispatch({ type: ActionTypes.GoToOption, focus: Focus.First })
|
||||
|
||||
case Keys.End:
|
||||
case Keys.PageDown:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return dispatch({ type: ActionTypes.GoToOption, focus: Focus.Last })
|
||||
|
||||
case Keys.Escape:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
dispatch({ type: ActionTypes.CloseListbox })
|
||||
return d.nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
|
||||
|
||||
case Keys.Tab:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
break
|
||||
|
||||
default:
|
||||
if (event.key.length === 1) {
|
||||
dispatch({ type: ActionTypes.Search, value: event.key })
|
||||
searchDisposables.setTimeout(() => dispatch({ type: ActionTypes.ClearSearch }), 350)
|
||||
}
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
let labelledby = useComputed(
|
||||
() => state.labelRef.current?.id ?? state.buttonRef.current?.id,
|
||||
@@ -826,31 +813,28 @@ let Option = forwardRefWithAs(function Option<
|
||||
bag.current.textValue = internalOptionRef.current?.textContent?.toLowerCase()
|
||||
}, [bag, internalOptionRef])
|
||||
|
||||
let select = useCallback(() => state.propsRef.current.onChange(value), [state.propsRef, value])
|
||||
let select = useEvent(() => state.propsRef.current.onChange(value))
|
||||
|
||||
useIsoMorphicEffect(() => {
|
||||
dispatch({ type: ActionTypes.RegisterOption, id, dataRef: bag })
|
||||
return () => dispatch({ type: ActionTypes.UnregisterOption, id })
|
||||
}, [bag, id])
|
||||
|
||||
let handleClick = useCallback(
|
||||
(event: { preventDefault: Function }) => {
|
||||
if (disabled) return event.preventDefault()
|
||||
select()
|
||||
if (state.propsRef.current.mode === ValueMode.Single) {
|
||||
dispatch({ type: ActionTypes.CloseListbox })
|
||||
disposables().nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
|
||||
}
|
||||
},
|
||||
[dispatch, state.buttonRef, disabled, select]
|
||||
)
|
||||
let handleClick = useEvent((event: { preventDefault: Function }) => {
|
||||
if (disabled) return event.preventDefault()
|
||||
select()
|
||||
if (state.propsRef.current.mode === ValueMode.Single) {
|
||||
dispatch({ type: ActionTypes.CloseListbox })
|
||||
disposables().nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
|
||||
}
|
||||
})
|
||||
|
||||
let handleFocus = useCallback(() => {
|
||||
let handleFocus = useEvent(() => {
|
||||
if (disabled) return dispatch({ type: ActionTypes.GoToOption, focus: Focus.Nothing })
|
||||
dispatch({ type: ActionTypes.GoToOption, focus: Focus.Specific, id })
|
||||
}, [disabled, id, dispatch])
|
||||
})
|
||||
|
||||
let handleMove = useCallback(() => {
|
||||
let handleMove = useEvent(() => {
|
||||
if (disabled) return
|
||||
if (active) return
|
||||
dispatch({
|
||||
@@ -859,13 +843,13 @@ let Option = forwardRefWithAs(function Option<
|
||||
id,
|
||||
trigger: ActivationTrigger.Pointer,
|
||||
})
|
||||
}, [disabled, active, id, dispatch])
|
||||
})
|
||||
|
||||
let handleLeave = useCallback(() => {
|
||||
let handleLeave = useEvent(() => {
|
||||
if (disabled) return
|
||||
if (!active) return
|
||||
dispatch({ type: ActionTypes.GoToOption, focus: Focus.Nothing })
|
||||
}, [disabled, active, dispatch])
|
||||
})
|
||||
|
||||
let slot = useMemo<OptionRenderPropArg>(
|
||||
() => ({ active, selected, disabled }),
|
||||
|
||||
@@ -3,7 +3,6 @@ import React, {
|
||||
Fragment,
|
||||
createContext,
|
||||
createRef,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
@@ -36,6 +35,7 @@ import { useTreeWalker } from '../../hooks/use-tree-walker'
|
||||
import { useOpenClosed, State, OpenClosedProvider } from '../../internal/open-closed'
|
||||
import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
|
||||
import { useOwnerDocument } from '../../hooks/use-owner'
|
||||
import { useEvent } from '../../hooks/use-event'
|
||||
|
||||
enum MenuStates {
|
||||
Open,
|
||||
@@ -303,32 +303,29 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
|
||||
let id = `headlessui-menu-button-${useId()}`
|
||||
let d = useDisposables()
|
||||
|
||||
let handleKeyDown = useCallback(
|
||||
(event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||
switch (event.key) {
|
||||
// Ref: https://www.w3.org/TR/wai-aria-practices-1.2/#keyboard-interaction-13
|
||||
let handleKeyDown = useEvent((event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||
switch (event.key) {
|
||||
// Ref: https://www.w3.org/TR/wai-aria-practices-1.2/#keyboard-interaction-13
|
||||
|
||||
case Keys.Space:
|
||||
case Keys.Enter:
|
||||
case Keys.ArrowDown:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
dispatch({ type: ActionTypes.OpenMenu })
|
||||
d.nextFrame(() => dispatch({ type: ActionTypes.GoToItem, focus: Focus.First }))
|
||||
break
|
||||
case Keys.Space:
|
||||
case Keys.Enter:
|
||||
case Keys.ArrowDown:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
dispatch({ type: ActionTypes.OpenMenu })
|
||||
d.nextFrame(() => dispatch({ type: ActionTypes.GoToItem, focus: Focus.First }))
|
||||
break
|
||||
|
||||
case Keys.ArrowUp:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
dispatch({ type: ActionTypes.OpenMenu })
|
||||
d.nextFrame(() => dispatch({ type: ActionTypes.GoToItem, focus: Focus.Last }))
|
||||
break
|
||||
}
|
||||
},
|
||||
[dispatch, d]
|
||||
)
|
||||
case Keys.ArrowUp:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
dispatch({ type: ActionTypes.OpenMenu })
|
||||
d.nextFrame(() => dispatch({ type: ActionTypes.GoToItem, focus: Focus.Last }))
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
let handleKeyUp = useCallback((event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||
let handleKeyUp = useEvent((event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||
switch (event.key) {
|
||||
case Keys.Space:
|
||||
// Required for firefox, event.preventDefault() in handleKeyDown for
|
||||
@@ -337,23 +334,20 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
|
||||
event.preventDefault()
|
||||
break
|
||||
}
|
||||
}, [])
|
||||
})
|
||||
|
||||
let handleClick = useCallback(
|
||||
(event: ReactMouseEvent) => {
|
||||
if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
|
||||
if (props.disabled) return
|
||||
if (state.menuState === MenuStates.Open) {
|
||||
dispatch({ type: ActionTypes.CloseMenu })
|
||||
d.nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
|
||||
} else {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
dispatch({ type: ActionTypes.OpenMenu })
|
||||
}
|
||||
},
|
||||
[dispatch, d, state, props.disabled]
|
||||
)
|
||||
let handleClick = useEvent((event: ReactMouseEvent) => {
|
||||
if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
|
||||
if (props.disabled) return
|
||||
if (state.menuState === MenuStates.Open) {
|
||||
dispatch({ type: ActionTypes.CloseMenu })
|
||||
d.nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
|
||||
} else {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
dispatch({ type: ActionTypes.OpenMenu })
|
||||
}
|
||||
})
|
||||
|
||||
let slot = useMemo<ButtonRenderPropArg>(
|
||||
() => ({ open: state.menuState === MenuStates.Open }),
|
||||
@@ -440,78 +434,75 @@ let Items = forwardRefWithAs(function Items<TTag extends ElementType = typeof DE
|
||||
},
|
||||
})
|
||||
|
||||
let handleKeyDown = useCallback(
|
||||
(event: ReactKeyboardEvent<HTMLDivElement>) => {
|
||||
searchDisposables.dispose()
|
||||
let handleKeyDown = useEvent((event: ReactKeyboardEvent<HTMLDivElement>) => {
|
||||
searchDisposables.dispose()
|
||||
|
||||
switch (event.key) {
|
||||
// Ref: https://www.w3.org/TR/wai-aria-practices-1.2/#keyboard-interaction-12
|
||||
switch (event.key) {
|
||||
// Ref: https://www.w3.org/TR/wai-aria-practices-1.2/#keyboard-interaction-12
|
||||
|
||||
// @ts-expect-error Fallthrough is expected here
|
||||
case Keys.Space:
|
||||
if (state.searchQuery !== '') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return dispatch({ type: ActionTypes.Search, value: event.key })
|
||||
}
|
||||
// When in type ahead mode, fallthrough
|
||||
case Keys.Enter:
|
||||
// @ts-expect-error Fallthrough is expected here
|
||||
case Keys.Space:
|
||||
if (state.searchQuery !== '') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
dispatch({ type: ActionTypes.CloseMenu })
|
||||
if (state.activeItemIndex !== null) {
|
||||
let { dataRef } = state.items[state.activeItemIndex]
|
||||
dataRef.current?.domRef.current?.click()
|
||||
}
|
||||
disposables().nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
|
||||
break
|
||||
return dispatch({ type: ActionTypes.Search, value: event.key })
|
||||
}
|
||||
// When in type ahead mode, fallthrough
|
||||
case Keys.Enter:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
dispatch({ type: ActionTypes.CloseMenu })
|
||||
if (state.activeItemIndex !== null) {
|
||||
let { dataRef } = state.items[state.activeItemIndex]
|
||||
dataRef.current?.domRef.current?.click()
|
||||
}
|
||||
disposables().nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
|
||||
break
|
||||
|
||||
case Keys.ArrowDown:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return dispatch({ type: ActionTypes.GoToItem, focus: Focus.Next })
|
||||
case Keys.ArrowDown:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return dispatch({ type: ActionTypes.GoToItem, focus: Focus.Next })
|
||||
|
||||
case Keys.ArrowUp:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return dispatch({ type: ActionTypes.GoToItem, focus: Focus.Previous })
|
||||
case Keys.ArrowUp:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return dispatch({ type: ActionTypes.GoToItem, focus: Focus.Previous })
|
||||
|
||||
case Keys.Home:
|
||||
case Keys.PageUp:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return dispatch({ type: ActionTypes.GoToItem, focus: Focus.First })
|
||||
case Keys.Home:
|
||||
case Keys.PageUp:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return dispatch({ type: ActionTypes.GoToItem, focus: Focus.First })
|
||||
|
||||
case Keys.End:
|
||||
case Keys.PageDown:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return dispatch({ type: ActionTypes.GoToItem, focus: Focus.Last })
|
||||
case Keys.End:
|
||||
case Keys.PageDown:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return dispatch({ type: ActionTypes.GoToItem, focus: Focus.Last })
|
||||
|
||||
case Keys.Escape:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
dispatch({ type: ActionTypes.CloseMenu })
|
||||
disposables().nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
|
||||
break
|
||||
case Keys.Escape:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
dispatch({ type: ActionTypes.CloseMenu })
|
||||
disposables().nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
|
||||
break
|
||||
|
||||
case Keys.Tab:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
break
|
||||
case Keys.Tab:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
break
|
||||
|
||||
default:
|
||||
if (event.key.length === 1) {
|
||||
dispatch({ type: ActionTypes.Search, value: event.key })
|
||||
searchDisposables.setTimeout(() => dispatch({ type: ActionTypes.ClearSearch }), 350)
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
[dispatch, searchDisposables, state, ownerDocument]
|
||||
)
|
||||
default:
|
||||
if (event.key.length === 1) {
|
||||
dispatch({ type: ActionTypes.Search, value: event.key })
|
||||
searchDisposables.setTimeout(() => dispatch({ type: ActionTypes.ClearSearch }), 350)
|
||||
}
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
let handleKeyUp = useCallback((event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||
let handleKeyUp = useEvent((event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||
switch (event.key) {
|
||||
case Keys.Space:
|
||||
// Required for firefox, event.preventDefault() in handleKeyDown for
|
||||
@@ -520,7 +511,7 @@ let Items = forwardRefWithAs(function Items<TTag extends ElementType = typeof DE
|
||||
event.preventDefault()
|
||||
break
|
||||
}
|
||||
}, [])
|
||||
})
|
||||
|
||||
let slot = useMemo<ItemsRenderPropArg>(
|
||||
() => ({ open: state.menuState === MenuStates.Open }),
|
||||
@@ -607,21 +598,18 @@ let Item = forwardRefWithAs(function Item<TTag extends ElementType = typeof DEFA
|
||||
return () => dispatch({ type: ActionTypes.UnregisterItem, id })
|
||||
}, [bag, id])
|
||||
|
||||
let handleClick = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
if (disabled) return event.preventDefault()
|
||||
dispatch({ type: ActionTypes.CloseMenu })
|
||||
disposables().nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
|
||||
},
|
||||
[dispatch, state.buttonRef, disabled]
|
||||
)
|
||||
let handleClick = useEvent((event: MouseEvent) => {
|
||||
if (disabled) return event.preventDefault()
|
||||
dispatch({ type: ActionTypes.CloseMenu })
|
||||
disposables().nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
|
||||
})
|
||||
|
||||
let handleFocus = useCallback(() => {
|
||||
let handleFocus = useEvent(() => {
|
||||
if (disabled) return dispatch({ type: ActionTypes.GoToItem, focus: Focus.Nothing })
|
||||
dispatch({ type: ActionTypes.GoToItem, focus: Focus.Specific, id })
|
||||
}, [disabled, id, dispatch])
|
||||
})
|
||||
|
||||
let handleMove = useCallback(() => {
|
||||
let handleMove = useEvent(() => {
|
||||
if (disabled) return
|
||||
if (active) return
|
||||
dispatch({
|
||||
@@ -630,13 +618,13 @@ let Item = forwardRefWithAs(function Item<TTag extends ElementType = typeof DEFA
|
||||
id,
|
||||
trigger: ActivationTrigger.Pointer,
|
||||
})
|
||||
}, [disabled, active, id, dispatch])
|
||||
})
|
||||
|
||||
let handleLeave = useCallback(() => {
|
||||
let handleLeave = useEvent(() => {
|
||||
if (disabled) return
|
||||
if (!active) return
|
||||
dispatch({ type: ActionTypes.GoToItem, focus: Focus.Nothing })
|
||||
}, [disabled, active, dispatch])
|
||||
})
|
||||
|
||||
let slot = useMemo<ItemRenderPropArg>(() => ({ active, disabled }), [active, disabled])
|
||||
let ourProps = {
|
||||
|
||||
@@ -12,9 +12,9 @@ import React, {
|
||||
ContextType,
|
||||
Dispatch,
|
||||
ElementType,
|
||||
FocusEvent as ReactFocusEvent,
|
||||
KeyboardEvent as ReactKeyboardEvent,
|
||||
MouseEvent as ReactMouseEvent,
|
||||
FocusEvent as ReactFocusEvent,
|
||||
MutableRefObject,
|
||||
Ref,
|
||||
} from 'react'
|
||||
|
||||
@@ -3,13 +3,13 @@ import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
|
||||
// Types
|
||||
ElementType,
|
||||
MutableRefObject,
|
||||
Ref,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useRef,
|
||||
|
||||
// Types
|
||||
ElementType,
|
||||
MutableRefObject,
|
||||
KeyboardEvent as ReactKeyboardEvent,
|
||||
ContextType,
|
||||
ElementType,
|
||||
KeyboardEvent as ReactKeyboardEvent,
|
||||
MutableRefObject,
|
||||
Ref,
|
||||
} from 'react'
|
||||
|
||||
@@ -31,14 +30,14 @@ import { attemptSubmit, objectToFormEntries } from '../../utils/form'
|
||||
import { getOwnerDocument } from '../../utils/owner'
|
||||
import { useEvent } from '../../hooks/use-event'
|
||||
|
||||
interface Option {
|
||||
interface Option<T = unknown> {
|
||||
id: string
|
||||
element: MutableRefObject<HTMLElement | null>
|
||||
propsRef: MutableRefObject<{ value: unknown; disabled: boolean }>
|
||||
propsRef: MutableRefObject<{ value: T; disabled: boolean }>
|
||||
}
|
||||
|
||||
interface StateDefinition {
|
||||
options: Option[]
|
||||
interface StateDefinition<T = unknown> {
|
||||
options: Option<T>[]
|
||||
}
|
||||
|
||||
enum ActionTypes {
|
||||
@@ -97,7 +96,7 @@ function useRadioGroupContext(component: string) {
|
||||
return context
|
||||
}
|
||||
|
||||
function stateReducer(state: StateDefinition, action: Actions) {
|
||||
function stateReducer<T>(state: StateDefinition<T>, action: Actions) {
|
||||
return match(action.type, reducers, state, action)
|
||||
}
|
||||
|
||||
@@ -133,9 +132,8 @@ let RadioGroupRoot = forwardRefWithAs(function RadioGroup<
|
||||
}
|
||||
: by
|
||||
)
|
||||
let [{ options }, dispatch] = useReducer(stateReducer, {
|
||||
options: [],
|
||||
} as StateDefinition)
|
||||
let [state, dispatch] = useReducer(stateReducer, { options: [] } as StateDefinition<TType>)
|
||||
let options = state.options as unknown as Option<TType>[]
|
||||
let [labelledby, LabelProvider] = useLabels()
|
||||
let [describedby, DescriptionProvider] = useDescriptions()
|
||||
let id = `headlessui-radiogroup-${useId()}`
|
||||
@@ -155,20 +153,17 @@ let RadioGroupRoot = forwardRefWithAs(function RadioGroup<
|
||||
[options, value]
|
||||
)
|
||||
|
||||
let triggerChange = useCallback(
|
||||
(nextValue) => {
|
||||
if (disabled) return false
|
||||
if (compare(nextValue, value)) return false
|
||||
let nextOption = options.find((option) =>
|
||||
compare(option.propsRef.current.value as TType, nextValue)
|
||||
)?.propsRef.current
|
||||
if (nextOption?.disabled) return false
|
||||
let triggerChange = useEvent((nextValue: TType) => {
|
||||
if (disabled) return false
|
||||
if (compare(nextValue, value)) return false
|
||||
let nextOption = options.find((option) =>
|
||||
compare(option.propsRef.current.value as TType, nextValue)
|
||||
)?.propsRef.current
|
||||
if (nextOption?.disabled) return false
|
||||
|
||||
onChange(nextValue)
|
||||
return true
|
||||
},
|
||||
[onChange, value, disabled, options]
|
||||
)
|
||||
onChange(nextValue)
|
||||
return true
|
||||
})
|
||||
|
||||
useTreeWalker({
|
||||
container: internalRadioGroupRef.current,
|
||||
@@ -182,78 +177,72 @@ let RadioGroupRoot = forwardRefWithAs(function RadioGroup<
|
||||
},
|
||||
})
|
||||
|
||||
let handleKeyDown = useCallback(
|
||||
(event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||
let container = internalRadioGroupRef.current
|
||||
if (!container) return
|
||||
let handleKeyDown = useEvent((event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||
let container = internalRadioGroupRef.current
|
||||
if (!container) return
|
||||
|
||||
let ownerDocument = getOwnerDocument(container)
|
||||
let ownerDocument = getOwnerDocument(container)
|
||||
|
||||
let all = options
|
||||
.filter((option) => option.propsRef.current.disabled === false)
|
||||
.map((radio) => radio.element.current) as HTMLElement[]
|
||||
let all = options
|
||||
.filter((option) => option.propsRef.current.disabled === false)
|
||||
.map((radio) => radio.element.current) as HTMLElement[]
|
||||
|
||||
switch (event.key) {
|
||||
case Keys.Enter:
|
||||
attemptSubmit(event.currentTarget)
|
||||
break
|
||||
case Keys.ArrowLeft:
|
||||
case Keys.ArrowUp:
|
||||
{
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
switch (event.key) {
|
||||
case Keys.Enter:
|
||||
attemptSubmit(event.currentTarget)
|
||||
break
|
||||
case Keys.ArrowLeft:
|
||||
case Keys.ArrowUp:
|
||||
{
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
let result = focusIn(all, Focus.Previous | Focus.WrapAround)
|
||||
|
||||
if (result === FocusResult.Success) {
|
||||
let activeOption = options.find(
|
||||
(option) => option.element.current === ownerDocument?.activeElement
|
||||
)
|
||||
if (activeOption) triggerChange(activeOption.propsRef.current.value)
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case Keys.ArrowRight:
|
||||
case Keys.ArrowDown:
|
||||
{
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
let result = focusIn(all, Focus.Next | Focus.WrapAround)
|
||||
|
||||
if (result === FocusResult.Success) {
|
||||
let activeOption = options.find(
|
||||
(option) => option.element.current === ownerDocument?.activeElement
|
||||
)
|
||||
if (activeOption) triggerChange(activeOption.propsRef.current.value)
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case Keys.Space:
|
||||
{
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
let result = focusIn(all, Focus.Previous | Focus.WrapAround)
|
||||
|
||||
if (result === FocusResult.Success) {
|
||||
let activeOption = options.find(
|
||||
(option) => option.element.current === ownerDocument?.activeElement
|
||||
)
|
||||
if (activeOption) triggerChange(activeOption.propsRef.current.value)
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
[internalRadioGroupRef, options, triggerChange]
|
||||
)
|
||||
}
|
||||
break
|
||||
|
||||
let registerOption = useCallback(
|
||||
(option: Option) => {
|
||||
dispatch({ type: ActionTypes.RegisterOption, ...option })
|
||||
return () => dispatch({ type: ActionTypes.UnregisterOption, id: option.id })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
case Keys.ArrowRight:
|
||||
case Keys.ArrowDown:
|
||||
{
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
let result = focusIn(all, Focus.Next | Focus.WrapAround)
|
||||
|
||||
if (result === FocusResult.Success) {
|
||||
let activeOption = options.find(
|
||||
(option) => option.element.current === ownerDocument?.activeElement
|
||||
)
|
||||
if (activeOption) triggerChange(activeOption.propsRef.current.value)
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case Keys.Space:
|
||||
{
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
let activeOption = options.find(
|
||||
(option) => option.element.current === ownerDocument?.activeElement
|
||||
)
|
||||
if (activeOption) triggerChange(activeOption.propsRef.current.value)
|
||||
}
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
let registerOption = useEvent((option: Option) => {
|
||||
dispatch({ type: ActionTypes.RegisterOption, ...option })
|
||||
return () => dispatch({ type: ActionTypes.UnregisterOption, id: option.id })
|
||||
})
|
||||
|
||||
let api = useMemo<ContextType<typeof RadioGroupContext>>(
|
||||
() => ({
|
||||
@@ -378,15 +367,15 @@ let Option = forwardRefWithAs(function Option<
|
||||
[id, registerOption, internalOptionRef, props]
|
||||
)
|
||||
|
||||
let handleClick = useCallback(() => {
|
||||
let handleClick = useEvent(() => {
|
||||
if (!change(value)) return
|
||||
|
||||
addFlag(OptionState.Active)
|
||||
internalOptionRef.current?.focus()
|
||||
}, [addFlag, change, value])
|
||||
})
|
||||
|
||||
let handleFocus = useCallback(() => addFlag(OptionState.Active), [addFlag])
|
||||
let handleBlur = useCallback(() => removeFlag(OptionState.Active), [removeFlag])
|
||||
let handleFocus = useEvent(() => addFlag(OptionState.Active))
|
||||
let handleBlur = useEvent(() => removeFlag(OptionState.Active))
|
||||
|
||||
let isFirstOption = firstOption?.id === id
|
||||
let isDisabled = radioGroupDisabled || disabled
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, {
|
||||
Fragment,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
useRef,
|
||||
useState,
|
||||
|
||||
// Types
|
||||
ElementType,
|
||||
@@ -25,6 +24,7 @@ import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
|
||||
import { useSyncRefs } from '../../hooks/use-sync-refs'
|
||||
import { Hidden, Features as HiddenFeatures } from '../../internal/hidden'
|
||||
import { attemptSubmit } from '../../utils/form'
|
||||
import { useEvent } from '../../hooks/use-event'
|
||||
|
||||
interface StateDefinition {
|
||||
switch: HTMLButtonElement | null
|
||||
@@ -121,32 +121,23 @@ let SwitchRoot = forwardRefWithAs(function Switch<
|
||||
groupContext === null ? null : groupContext.setSwitch
|
||||
)
|
||||
|
||||
let toggle = useCallback(() => onChange(!checked), [onChange, checked])
|
||||
let handleClick = useCallback(
|
||||
(event: ReactMouseEvent) => {
|
||||
if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
|
||||
let toggle = useEvent(() => onChange(!checked))
|
||||
let handleClick = useEvent((event: ReactMouseEvent) => {
|
||||
if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
|
||||
event.preventDefault()
|
||||
toggle()
|
||||
})
|
||||
let handleKeyUp = useEvent((event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||
if (event.key === Keys.Space) {
|
||||
event.preventDefault()
|
||||
toggle()
|
||||
},
|
||||
[toggle]
|
||||
)
|
||||
let handleKeyUp = useCallback(
|
||||
(event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||
if (event.key === Keys.Space) {
|
||||
event.preventDefault()
|
||||
toggle()
|
||||
} else if (event.key === Keys.Enter) {
|
||||
attemptSubmit(event.currentTarget)
|
||||
}
|
||||
},
|
||||
[toggle]
|
||||
)
|
||||
} else if (event.key === Keys.Enter) {
|
||||
attemptSubmit(event.currentTarget)
|
||||
}
|
||||
})
|
||||
|
||||
// This is needed so that we can "cancel" the click event when we use the `Enter` key on a button.
|
||||
let handleKeyPress = useCallback(
|
||||
(event: ReactKeyboardEvent<HTMLElement>) => event.preventDefault(),
|
||||
[]
|
||||
)
|
||||
let handleKeyPress = useEvent((event: ReactKeyboardEvent<HTMLElement>) => event.preventDefault())
|
||||
|
||||
let slot = useMemo<SwitchRenderPropArg>(() => ({ checked }), [checked])
|
||||
let ourProps = {
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import React, {
|
||||
Fragment,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useRef,
|
||||
useEffect,
|
||||
|
||||
// Types
|
||||
ElementType,
|
||||
MutableRefObject,
|
||||
MouseEvent as ReactMouseEvent,
|
||||
KeyboardEvent as ReactKeyboardEvent,
|
||||
Dispatch,
|
||||
ContextType,
|
||||
ElementType,
|
||||
KeyboardEvent as ReactKeyboardEvent,
|
||||
MouseEvent as ReactMouseEvent,
|
||||
MutableRefObject,
|
||||
Ref,
|
||||
} from 'react'
|
||||
|
||||
@@ -29,21 +26,17 @@ import { useSyncRefs } from '../../hooks/use-sync-refs'
|
||||
import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
|
||||
import { useLatestValue } from '../../hooks/use-latest-value'
|
||||
import { FocusSentinel } from '../../internal/focus-sentinel'
|
||||
import { useEvent } from '../../hooks/use-event'
|
||||
|
||||
interface StateDefinition {
|
||||
selectedIndex: number
|
||||
|
||||
orientation: 'horizontal' | 'vertical'
|
||||
activation: 'auto' | 'manual'
|
||||
|
||||
tabs: MutableRefObject<HTMLElement | null>[]
|
||||
panels: MutableRefObject<HTMLElement | null>[]
|
||||
}
|
||||
|
||||
enum ActionTypes {
|
||||
SetSelectedIndex,
|
||||
SetOrientation,
|
||||
SetActivation,
|
||||
|
||||
RegisterTab,
|
||||
UnregisterTab,
|
||||
@@ -56,8 +49,6 @@ enum ActionTypes {
|
||||
|
||||
type Actions =
|
||||
| { type: ActionTypes.SetSelectedIndex; index: number }
|
||||
| { type: ActionTypes.SetOrientation; orientation: StateDefinition['orientation'] }
|
||||
| { type: ActionTypes.SetActivation; activation: StateDefinition['activation'] }
|
||||
| { type: ActionTypes.RegisterTab; tab: MutableRefObject<HTMLElement | null> }
|
||||
| { type: ActionTypes.UnregisterTab; tab: MutableRefObject<HTMLElement | null> }
|
||||
| { type: ActionTypes.RegisterPanel; panel: MutableRefObject<HTMLElement | null> }
|
||||
@@ -95,14 +86,6 @@ let reducers: {
|
||||
|
||||
return { ...state, selectedIndex: state.tabs.indexOf(next) }
|
||||
},
|
||||
[ActionTypes.SetOrientation](state, action) {
|
||||
if (state.orientation === action.orientation) return state
|
||||
return { ...state, orientation: action.orientation }
|
||||
},
|
||||
[ActionTypes.SetActivation](state, action) {
|
||||
if (state.activation === action.activation) return state
|
||||
return { ...state, activation: action.activation }
|
||||
},
|
||||
[ActionTypes.RegisterTab](state, action) {
|
||||
if (state.tabs.includes(action.tab)) return state
|
||||
return { ...state, tabs: sortByDomNode([...state.tabs, action.tab], (tab) => tab.current) }
|
||||
@@ -128,11 +111,6 @@ let reducers: {
|
||||
},
|
||||
}
|
||||
|
||||
let TabsContext = createContext<
|
||||
[StateDefinition, { change(index: number): void; dispatch: Dispatch<Actions> }] | null
|
||||
>(null)
|
||||
TabsContext.displayName = 'TabsContext'
|
||||
|
||||
let TabsSSRContext = createContext<MutableRefObject<{ tabs: string[]; panels: string[] }> | null>(
|
||||
null
|
||||
)
|
||||
@@ -148,11 +126,38 @@ function useSSRTabsCounter(component: string) {
|
||||
return context
|
||||
}
|
||||
|
||||
function useTabsContext(component: string) {
|
||||
let context = useContext(TabsContext)
|
||||
let TabsDataContext = createContext<
|
||||
| ({
|
||||
orientation: 'horizontal' | 'vertical'
|
||||
activation: 'auto' | 'manual'
|
||||
} & StateDefinition)
|
||||
| null
|
||||
>(null)
|
||||
TabsDataContext.displayName = 'TabsDataContext'
|
||||
|
||||
function useData(component: string) {
|
||||
let context = useContext(TabsDataContext)
|
||||
if (context === null) {
|
||||
let err = new Error(`<${component} /> is missing a parent <Tab.Group /> component.`)
|
||||
if (Error.captureStackTrace) Error.captureStackTrace(err, useTabsContext)
|
||||
if (Error.captureStackTrace) Error.captureStackTrace(err, useData)
|
||||
throw err
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
let TabsActionsContext = createContext<{
|
||||
registerTab(tab: MutableRefObject<HTMLElement | null>): () => void
|
||||
registerPanel(panel: MutableRefObject<HTMLElement | null>): () => void
|
||||
change(index: number): void
|
||||
forceRerender(): void
|
||||
} | null>(null)
|
||||
TabsActionsContext.displayName = 'TabsActionsContext'
|
||||
|
||||
function useActions(component: string) {
|
||||
let context = useContext(TabsActionsContext)
|
||||
if (context === null) {
|
||||
let err = new Error(`<${component} /> is missing a parent <Tab.Group /> component.`)
|
||||
if (Error.captureStackTrace) Error.captureStackTrace(err, useActions)
|
||||
throw err
|
||||
}
|
||||
return context
|
||||
@@ -195,79 +200,72 @@ let Tabs = forwardRefWithAs(function Tabs<TTag extends ElementType = typeof DEFA
|
||||
selectedIndex: selectedIndex ?? defaultIndex,
|
||||
tabs: [],
|
||||
panels: [],
|
||||
orientation,
|
||||
activation,
|
||||
} as StateDefinition)
|
||||
})
|
||||
let slot = useMemo(() => ({ selectedIndex: state.selectedIndex }), [state.selectedIndex])
|
||||
let onChangeRef = useLatestValue(onChange || (() => {}))
|
||||
let stableTabsRef = useLatestValue(state.tabs)
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ type: ActionTypes.SetOrientation, orientation })
|
||||
}, [orientation])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ type: ActionTypes.SetActivation, activation })
|
||||
}, [activation])
|
||||
|
||||
useIsoMorphicEffect(() => {
|
||||
let indexToSet = selectedIndex ?? defaultIndex
|
||||
dispatch({ type: ActionTypes.SetSelectedIndex, index: indexToSet })
|
||||
}, [selectedIndex /* Deliberately skipping defaultIndex */])
|
||||
|
||||
let lastChangedIndex = useRef(state.selectedIndex)
|
||||
useEffect(() => {
|
||||
lastChangedIndex.current = state.selectedIndex
|
||||
}, [state.selectedIndex])
|
||||
|
||||
let providerBag = useMemo<ContextType<typeof TabsContext>>(
|
||||
() => [
|
||||
state,
|
||||
{
|
||||
dispatch,
|
||||
change(index: number) {
|
||||
if (lastChangedIndex.current !== index) onChangeRef.current(index)
|
||||
lastChangedIndex.current = index
|
||||
|
||||
dispatch({ type: ActionTypes.SetSelectedIndex, index })
|
||||
},
|
||||
},
|
||||
],
|
||||
[state, dispatch]
|
||||
let tabsData = useMemo<ContextType<typeof TabsDataContext>>(
|
||||
() => ({ orientation, activation, ...state }),
|
||||
[orientation, activation, state]
|
||||
)
|
||||
|
||||
let SSRCounter = useRef({
|
||||
tabs: [],
|
||||
panels: [],
|
||||
})
|
||||
let lastChangedIndex = useLatestValue(state.selectedIndex)
|
||||
let tabsActions: ContextType<typeof TabsActionsContext> = useMemo(
|
||||
() => ({
|
||||
registerTab(tab) {
|
||||
dispatch({ type: ActionTypes.RegisterTab, tab })
|
||||
return () => dispatch({ type: ActionTypes.UnregisterTab, tab })
|
||||
},
|
||||
registerPanel(panel) {
|
||||
dispatch({ type: ActionTypes.RegisterPanel, panel })
|
||||
return () => dispatch({ type: ActionTypes.UnregisterPanel, panel })
|
||||
},
|
||||
forceRerender() {
|
||||
dispatch({ type: ActionTypes.ForceRerender })
|
||||
},
|
||||
change(index: number) {
|
||||
if (lastChangedIndex.current !== index) onChangeRef.current(index)
|
||||
lastChangedIndex.current = index
|
||||
|
||||
let ourProps = {
|
||||
ref: tabsRef,
|
||||
}
|
||||
dispatch({ type: ActionTypes.SetSelectedIndex, index })
|
||||
},
|
||||
}),
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
useIsoMorphicEffect(() => {
|
||||
dispatch({ type: ActionTypes.SetSelectedIndex, index: selectedIndex ?? defaultIndex })
|
||||
}, [selectedIndex /* Deliberately skipping defaultIndex */])
|
||||
|
||||
let SSRCounter = useRef({ tabs: [], panels: [] })
|
||||
let ourProps = { ref: tabsRef }
|
||||
|
||||
return (
|
||||
<TabsSSRContext.Provider value={SSRCounter}>
|
||||
<TabsContext.Provider value={providerBag}>
|
||||
<FocusSentinel
|
||||
onFocus={() => {
|
||||
for (let tab of stableTabsRef.current) {
|
||||
if (tab.current?.tabIndex === 0) {
|
||||
tab.current?.focus()
|
||||
return true
|
||||
<TabsActionsContext.Provider value={tabsActions}>
|
||||
<TabsDataContext.Provider value={tabsData}>
|
||||
<FocusSentinel
|
||||
onFocus={() => {
|
||||
for (let tab of stableTabsRef.current) {
|
||||
if (tab.current?.tabIndex === 0) {
|
||||
tab.current?.focus()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}}
|
||||
/>
|
||||
{render({
|
||||
ourProps,
|
||||
theirProps,
|
||||
slot,
|
||||
defaultTag: DEFAULT_TABS_TAG,
|
||||
name: 'Tabs',
|
||||
})}
|
||||
</TabsContext.Provider>
|
||||
return false
|
||||
}}
|
||||
/>
|
||||
{render({
|
||||
ourProps,
|
||||
theirProps,
|
||||
slot,
|
||||
defaultTag: DEFAULT_TABS_TAG,
|
||||
name: 'Tabs',
|
||||
})}
|
||||
</TabsDataContext.Provider>
|
||||
</TabsActionsContext.Provider>
|
||||
</TabsSSRContext.Provider>
|
||||
)
|
||||
})
|
||||
@@ -284,7 +282,7 @@ let List = forwardRefWithAs(function List<TTag extends ElementType = typeof DEFA
|
||||
props: Props<TTag, ListRenderPropArg, ListPropsWeControl> & {},
|
||||
ref: Ref<HTMLElement>
|
||||
) {
|
||||
let [{ selectedIndex, orientation }] = useTabsContext('Tab.List')
|
||||
let { orientation, selectedIndex } = useData('Tab.List')
|
||||
let listRef = useSyncRefs(ref)
|
||||
|
||||
let slot = { selectedIndex }
|
||||
@@ -319,20 +317,17 @@ let TabRoot = forwardRefWithAs(function Tab<TTag extends ElementType = typeof DE
|
||||
) {
|
||||
let id = `headlessui-tabs-tab-${useId()}`
|
||||
|
||||
let [{ selectedIndex, tabs, panels, orientation, activation }, { dispatch, change }] =
|
||||
useTabsContext('Tab')
|
||||
let { orientation, activation, selectedIndex, tabs, panels } = useData('Tab')
|
||||
let actions = useActions('Tab')
|
||||
let SSRContext = useSSRTabsCounter('Tab')
|
||||
|
||||
let internalTabRef = useRef<HTMLElement>(null)
|
||||
let internalTabRef = useRef<HTMLElement | null>(null)
|
||||
let tabRef = useSyncRefs(internalTabRef, ref, (element) => {
|
||||
if (!element) return
|
||||
dispatch({ type: ActionTypes.ForceRerender })
|
||||
actions.forceRerender()
|
||||
})
|
||||
|
||||
useIsoMorphicEffect(() => {
|
||||
dispatch({ type: ActionTypes.RegisterTab, tab: internalTabRef })
|
||||
return () => dispatch({ type: ActionTypes.UnregisterTab, tab: internalTabRef })
|
||||
}, [dispatch, internalTabRef])
|
||||
useIsoMorphicEffect(() => actions.registerTab(internalTabRef), [actions, internalTabRef])
|
||||
|
||||
let mySSRIndex = SSRContext.current.tabs.indexOf(id)
|
||||
if (mySSRIndex === -1) mySSRIndex = SSRContext.current.tabs.push(id) - 1
|
||||
@@ -341,65 +336,62 @@ let TabRoot = forwardRefWithAs(function Tab<TTag extends ElementType = typeof DE
|
||||
if (myIndex === -1) myIndex = mySSRIndex
|
||||
let selected = myIndex === selectedIndex
|
||||
|
||||
let handleKeyDown = useCallback(
|
||||
(event: ReactKeyboardEvent<HTMLElement>) => {
|
||||
let list = tabs.map((tab) => tab.current).filter(Boolean) as HTMLElement[]
|
||||
let handleKeyDown = useEvent((event: ReactKeyboardEvent<HTMLElement>) => {
|
||||
let list = tabs.map((tab) => tab.current).filter(Boolean) as HTMLElement[]
|
||||
|
||||
if (event.key === Keys.Space || event.key === Keys.Enter) {
|
||||
if (event.key === Keys.Space || event.key === Keys.Enter) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
actions.change(myIndex)
|
||||
return
|
||||
}
|
||||
|
||||
switch (event.key) {
|
||||
case Keys.Home:
|
||||
case Keys.PageUp:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
change(myIndex)
|
||||
return focusIn(list, Focus.First)
|
||||
|
||||
case Keys.End:
|
||||
case Keys.PageDown:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
return focusIn(list, Focus.Last)
|
||||
}
|
||||
|
||||
return match(orientation, {
|
||||
vertical() {
|
||||
if (event.key === Keys.ArrowUp) return focusIn(list, Focus.Previous | Focus.WrapAround)
|
||||
if (event.key === Keys.ArrowDown) return focusIn(list, Focus.Next | Focus.WrapAround)
|
||||
return
|
||||
}
|
||||
},
|
||||
horizontal() {
|
||||
if (event.key === Keys.ArrowLeft) return focusIn(list, Focus.Previous | Focus.WrapAround)
|
||||
if (event.key === Keys.ArrowRight) return focusIn(list, Focus.Next | Focus.WrapAround)
|
||||
return
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
switch (event.key) {
|
||||
case Keys.Home:
|
||||
case Keys.PageUp:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
return focusIn(list, Focus.First)
|
||||
|
||||
case Keys.End:
|
||||
case Keys.PageDown:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
return focusIn(list, Focus.Last)
|
||||
}
|
||||
|
||||
return match(orientation, {
|
||||
vertical() {
|
||||
if (event.key === Keys.ArrowUp) return focusIn(list, Focus.Previous | Focus.WrapAround)
|
||||
if (event.key === Keys.ArrowDown) return focusIn(list, Focus.Next | Focus.WrapAround)
|
||||
return
|
||||
},
|
||||
horizontal() {
|
||||
if (event.key === Keys.ArrowLeft) return focusIn(list, Focus.Previous | Focus.WrapAround)
|
||||
if (event.key === Keys.ArrowRight) return focusIn(list, Focus.Next | Focus.WrapAround)
|
||||
return
|
||||
},
|
||||
})
|
||||
},
|
||||
[tabs, orientation, myIndex, change]
|
||||
)
|
||||
|
||||
let handleFocus = useCallback(() => {
|
||||
let handleFocus = useEvent(() => {
|
||||
internalTabRef.current?.focus()
|
||||
}, [internalTabRef])
|
||||
})
|
||||
|
||||
let handleSelection = useCallback(() => {
|
||||
let handleSelection = useEvent(() => {
|
||||
internalTabRef.current?.focus()
|
||||
change(myIndex)
|
||||
}, [change, myIndex, internalTabRef])
|
||||
actions.change(myIndex)
|
||||
})
|
||||
|
||||
// This is important because we want to only focus the tab when it gets focus
|
||||
// OR it finished the click event (mouseup). However, if you perform a `click`,
|
||||
// then you will first get the `focus` and then get the `click` event.
|
||||
let handleMouseDown = useCallback((event: ReactMouseEvent<HTMLElement>) => {
|
||||
let handleMouseDown = useEvent((event: ReactMouseEvent<HTMLElement>) => {
|
||||
event.preventDefault()
|
||||
}, [])
|
||||
})
|
||||
|
||||
let slot = useMemo(() => ({ selected }), [selected])
|
||||
|
||||
@@ -438,7 +430,7 @@ let Panels = forwardRefWithAs(function Panels<TTag extends ElementType = typeof
|
||||
props: Props<TTag, PanelsRenderPropArg>,
|
||||
ref: Ref<HTMLElement>
|
||||
) {
|
||||
let [{ selectedIndex }] = useTabsContext('Tab.Panels')
|
||||
let { selectedIndex } = useData('Tab.Panels')
|
||||
let panelsRef = useSyncRefs(ref)
|
||||
|
||||
let slot = useMemo(() => ({ selectedIndex }), [selectedIndex])
|
||||
@@ -469,20 +461,18 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
|
||||
PropsForFeatures<typeof PanelRenderFeatures>,
|
||||
ref: Ref<HTMLElement>
|
||||
) {
|
||||
let [{ selectedIndex, tabs, panels }, { dispatch }] = useTabsContext('Tab.Panel')
|
||||
let { selectedIndex, tabs, panels } = useData('Tab.Panel')
|
||||
let actions = useActions('Tab.Panel')
|
||||
let SSRContext = useSSRTabsCounter('Tab.Panel')
|
||||
|
||||
let id = `headlessui-tabs-panel-${useId()}`
|
||||
let internalPanelRef = useRef<HTMLElement>(null)
|
||||
let panelRef = useSyncRefs(internalPanelRef, ref, (element) => {
|
||||
if (!element) return
|
||||
dispatch({ type: ActionTypes.ForceRerender })
|
||||
actions.forceRerender()
|
||||
})
|
||||
|
||||
useIsoMorphicEffect(() => {
|
||||
dispatch({ type: ActionTypes.RegisterPanel, panel: internalPanelRef })
|
||||
return () => dispatch({ type: ActionTypes.UnregisterPanel, panel: internalPanelRef })
|
||||
}, [dispatch, internalPanelRef])
|
||||
useIsoMorphicEffect(() => actions.registerPanel(internalPanelRef), [actions, internalPanelRef])
|
||||
|
||||
let mySSRIndex = SSRContext.current.panels.indexOf(id)
|
||||
if (mySSRIndex === -1) mySSRIndex = SSRContext.current.panels.push(id) - 1
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import React from 'react'
|
||||
import { useLatestValue } from './use-latest-value'
|
||||
|
||||
export let useEvent =
|
||||
// TODO: Add React.useEvent ?? once the useEvent hook is available
|
||||
function useEvent<T, R>(cb: (...args: T[]) => R) {
|
||||
let cache = React.useRef(cb)
|
||||
cache.current = cb
|
||||
return React.useCallback((...args: T[]) => cache.current(...args), [cache])
|
||||
function useEvent<
|
||||
F extends (...args: any[]) => any,
|
||||
P extends any[] = Parameters<F>,
|
||||
R = ReturnType<F>
|
||||
>(cb: (...args: P) => R) {
|
||||
let cache = useLatestValue(cb)
|
||||
return React.useCallback((...args: P) => cache.current(...args), [cache])
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useEvent } from './use-event'
|
||||
|
||||
export function useFlags(initialFlags = 0) {
|
||||
let [flags, setFlags] = useState(initialFlags)
|
||||
|
||||
let addFlag = useCallback((flag: number) => setFlags((flags) => flags | flag), [setFlags])
|
||||
let hasFlag = useCallback((flag: number) => Boolean(flags & flag), [flags])
|
||||
let removeFlag = useCallback((flag: number) => setFlags((flags) => flags & ~flag), [setFlags])
|
||||
let toggleFlag = useCallback((flag: number) => setFlags((flags) => flags ^ flag), [setFlags])
|
||||
let addFlag = useEvent((flag: number) => setFlags((flags) => flags | flag))
|
||||
let hasFlag = useEvent((flag: number) => Boolean(flags & flag))
|
||||
let removeFlag = useEvent((flag: number) => setFlags((flags) => flags & ~flag))
|
||||
let toggleFlag = useEvent((flag: number) => setFlags((flags) => flags ^ flag))
|
||||
|
||||
return { addFlag, hasFlag, removeFlag, toggleFlag }
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { useLayoutEffect, useEffect } from 'react'
|
||||
|
||||
export const useIsoMorphicEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
|
||||
export let useIsoMorphicEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useRef, useEffect, useCallback } from 'react'
|
||||
import { useRef, useEffect } from 'react'
|
||||
import { useEvent } from './use-event'
|
||||
|
||||
let Optional = Symbol()
|
||||
|
||||
@@ -15,16 +16,13 @@ export function useSyncRefs<TType>(
|
||||
cache.current = refs
|
||||
}, [refs])
|
||||
|
||||
let syncRefs = useCallback(
|
||||
(value: TType) => {
|
||||
for (let ref of cache.current) {
|
||||
if (ref == null) continue
|
||||
if (typeof ref === 'function') ref(value)
|
||||
else ref.current = value
|
||||
}
|
||||
},
|
||||
[cache]
|
||||
)
|
||||
let syncRefs = useEvent((value: TType) => {
|
||||
for (let ref of cache.current) {
|
||||
if (ref == null) continue
|
||||
if (typeof ref === 'function') ref(value)
|
||||
else ref.current = value
|
||||
}
|
||||
})
|
||||
|
||||
return refs.every(
|
||||
(ref) =>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
|
||||
// Types
|
||||
@@ -8,6 +7,7 @@ import React, {
|
||||
ReactNode,
|
||||
} from 'react'
|
||||
import { useIsoMorphicEffect } from '../hooks/use-iso-morphic-effect'
|
||||
import { useEvent } from '../hooks/use-event'
|
||||
|
||||
type OnUpdate = (
|
||||
message: StackMessage,
|
||||
@@ -40,16 +40,13 @@ export function StackProvider({
|
||||
}) {
|
||||
let parentUpdate = useStackContext()
|
||||
|
||||
let notify = useCallback(
|
||||
(...args: Parameters<OnUpdate>) => {
|
||||
// Notify our layer
|
||||
onUpdate?.(...args)
|
||||
let notify = useEvent((...args: Parameters<OnUpdate>) => {
|
||||
// Notify our layer
|
||||
onUpdate?.(...args)
|
||||
|
||||
// Notify the parent
|
||||
parentUpdate(...args)
|
||||
},
|
||||
[parentUpdate, onUpdate]
|
||||
)
|
||||
// Notify the parent
|
||||
parentUpdate(...args)
|
||||
})
|
||||
|
||||
useIsoMorphicEffect(() => {
|
||||
notify(StackMessage.Add, type, element)
|
||||
|
||||
Reference in New Issue
Block a user