From b695d28a3fcc8be1e0334978a6b0979d771a0b8a Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 22 Apr 2024 20:09:14 +0200 Subject: [PATCH 1/4] make `anchor` prop a boolean as well - If `anchor` or `anchor={true}` is used, then the anchoring functionality will be enabled but with the default (empty) configuration. - If `anchor={false}` is used, then the anchoring functionality will be disabled. --- .../src/components/combobox/combobox.tsx | 6 ++++-- .../src/components/listbox/listbox.tsx | 13 ++++++++----- .../@headlessui-react/src/components/menu/menu.tsx | 12 ++++++------ .../src/components/popover/popover.tsx | 12 ++++++------ .../src/components/tooltip/tooltip.tsx | 6 ++++-- 5 files changed, 28 insertions(+), 21 deletions(-) diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx index 596860434..daf5fe83e 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx @@ -1534,7 +1534,7 @@ export type ComboboxOptionsProps & { hold?: boolean - anchor?: AnchorProps + anchor?: boolean | AnchorProps } > @@ -1552,7 +1552,9 @@ function OptionsFn( let data = useData('Combobox.Options') let actions = useActions('Combobox.Options') - let [floatingRef, style] = useFloatingPanel(anchor) + let [floatingRef, style] = useFloatingPanel( + anchor === false ? undefined : anchor === true ? {} : anchor + ) let getFloatingPanelProps = useFloatingPanelProps() let optionsRef = useSyncRefs(data.optionsRef, ref, anchor ? floatingRef : null) diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx index 6cbf26d49..e65deb394 100644 --- a/packages/@headlessui-react/src/components/listbox/listbox.tsx +++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx @@ -868,7 +868,7 @@ export type ListboxOptionsProps > @@ -881,10 +881,8 @@ function OptionsFn( let { id = `headlessui-listbox-options-${internalId}`, anchor, modal, ...theirProps } = props // Always use `modal` when `anchor` is passed in - if (anchor != null && modal == null) { - modal = true - } else if (modal == null) { - modal = false + if (modal == null) { + modal = Boolean(anchor) } let data = useData('Listbox.Options') @@ -905,6 +903,8 @@ function OptionsFn( let initialOption = useRef(null) useEffect(() => { + if (typeof anchor === 'boolean') return + if (!anchor?.to?.includes('selection')) return if (!visible) { @@ -938,6 +938,9 @@ function OptionsFn( let panelEnabled = didButtonMove ? false : visible let anchorOptions = (() => { + if (anchor === false) return undefined + if (anchor === true) anchor = {} + if (anchor == null) return undefined if (data.listRef.current.size <= 0) return { ...anchor, inner: undefined } diff --git a/packages/@headlessui-react/src/components/menu/menu.tsx b/packages/@headlessui-react/src/components/menu/menu.tsx index 329b303a9..76b3c3a79 100644 --- a/packages/@headlessui-react/src/components/menu/menu.tsx +++ b/packages/@headlessui-react/src/components/menu/menu.tsx @@ -575,7 +575,7 @@ export type MenuItemsProps ItemsRenderPropArg, ItemsPropsWeControl, { - anchor?: AnchorProps + anchor?: boolean | AnchorProps modal?: boolean // ItemsRenderFeatures @@ -591,16 +591,16 @@ function ItemsFn( let internalId = useId() let { id = `headlessui-menu-items-${internalId}`, anchor, modal, ...theirProps } = props let [state, dispatch] = useMenuContext('Menu.Items') - let [floatingRef, style] = useFloatingPanel(anchor) + let [floatingRef, style] = useFloatingPanel( + anchor === false ? undefined : anchor === true ? {} : anchor + ) let getFloatingPanelProps = useFloatingPanelProps() let itemsRef = useSyncRefs(state.itemsRef, ref, anchor ? floatingRef : null) let ownerDocument = useOwnerDocument(state.itemsRef) // Always use `modal` when `anchor` is passed in - if (anchor != null && modal == null) { - modal = true - } else if (modal == null) { - modal = false + if (modal == null) { + modal = Boolean(anchor) } let searchDisposables = useDisposables() diff --git a/packages/@headlessui-react/src/components/popover/popover.tsx b/packages/@headlessui-react/src/components/popover/popover.tsx index 829f10911..cb3d970f8 100644 --- a/packages/@headlessui-react/src/components/popover/popover.tsx +++ b/packages/@headlessui-react/src/components/popover/popover.tsx @@ -798,7 +798,7 @@ export type PopoverPanelProps( let afterPanelSentinelId = `headlessui-focus-sentinel-after-${internalId}` let internalPanelRef = useRef(null) - let [floatingRef, style] = useFloatingPanel(anchor) + let [floatingRef, style] = useFloatingPanel( + anchor === false ? undefined : anchor === true ? {} : anchor + ) let getFloatingPanelProps = useFloatingPanelProps() // Always use `modal` when `anchor` is passed in - if (anchor != null && modal == null) { - modal = true - } else if (modal == null) { - modal = false + if (modal == null) { + modal = Boolean(anchor) } let panelRef = useSyncRefs(internalPanelRef, ref, anchor ? floatingRef : null, (panel) => { diff --git a/packages/@headlessui-react/src/components/tooltip/tooltip.tsx b/packages/@headlessui-react/src/components/tooltip/tooltip.tsx index d277955a6..bec52255d 100644 --- a/packages/@headlessui-react/src/components/tooltip/tooltip.tsx +++ b/packages/@headlessui-react/src/components/tooltip/tooltip.tsx @@ -415,7 +415,7 @@ export type TooltipPanelProps + { anchor?: boolean | AnchorProps } & PropsForFeatures > function PanelFn( @@ -443,7 +443,9 @@ function PanelFn( })() let internalPanelRef = useRef(null) - let [floatingRef, style] = useFloatingPanel(visible ? anchor : undefined) + let [floatingRef, style] = useFloatingPanel( + visible && anchor !== false ? (anchor === true ? {} : anchor) : undefined + ) let panelRef = useSyncRefs(internalPanelRef, ref, floatingRef) let ourProps = { From 8296df1ba2a2ecb141f4326b2505f13968fe6d1a Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 22 Apr 2024 22:17:26 +0200 Subject: [PATCH 2/4] move boolean `anchor` type + handling into Floating file --- .../src/internal/floating.tsx | 65 ++++++++++++------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/packages/@headlessui-react/src/internal/floating.tsx b/packages/@headlessui-react/src/internal/floating.tsx index e602d8107..e7c8200b4 100644 --- a/packages/@headlessui-react/src/internal/floating.tsx +++ b/packages/@headlessui-react/src/internal/floating.tsx @@ -37,25 +37,29 @@ type BaseAnchorProps = { padding: number | string // For `var()` support } -export type AnchorProps = Partial< - BaseAnchorProps & { - /** - * The `to` value defines which side of the trigger the panel should be placed on and its - * alignment. - */ - to: `${Placement}` | `${Placement} ${Align}` - } -> - -export type AnchorPropsWithSelection = Partial< - BaseAnchorProps & { - /** - * The `to` value defines which side of the trigger the panel should be placed on and its - * alignment. - */ - to: `${Placement | 'selection'}` | `${Placement | 'selection'} ${Align}` - } -> +export type AnchorProps = + | boolean // Enable with defaults, or disable entirely + | Partial< + BaseAnchorProps & { + /** + * The `to` value defines which side of the trigger the panel should be placed on and its + * alignment. + */ + to: `${Placement}` | `${Placement} ${Align}` + } + > + +export type AnchorPropsWithSelection = + | boolean // Enable with defaults, or disable entirely + | Partial< + BaseAnchorProps & { + /** + * The `to` value defines which side of the trigger the panel should be placed on and its + * alignment. + */ + to: `${Placement | 'selection'}` | `${Placement | 'selection'} ${Align}` + } + > export type InternalFloatingPanelProps = Partial<{ inner: { @@ -82,11 +86,21 @@ let FloatingContext = createContext<{ slot: {}, }) FloatingContext.displayName = 'FloatingContext' -let PlacementContext = createContext<((value: AnchorPropsWithSelection | null) => void) | null>( - null -) +let PlacementContext = createContext< + ((value: Exclude | null) => void) | null +>(null) PlacementContext.displayName = 'PlacementContext' +export function useResolvedAnchor( + anchor?: T +): Exclude | null { + return useMemo(() => { + if (anchor === true) return {} as Exclude // Enable with defaults + if (!anchor) return null // Disable entirely + return anchor as Exclude // User-provided value + }, [anchor]) +} + export function useFloatingReference() { return useContext(FloatingContext).setReference } @@ -108,8 +122,11 @@ export function useFloatingPanelProps() { } export function useFloatingPanel( - placement?: AnchorPropsWithSelection & InternalFloatingPanelProps + placement: (AnchorPropsWithSelection & InternalFloatingPanelProps) | null = null ) { + if (placement === true) placement = {} // Enable with defaults + if (placement === false) placement = null // Disable entirely + let updatePlacementConfig = useContext(PlacementContext) let stablePlacement = useMemo( () => placement, @@ -372,7 +389,7 @@ function useFixScrollingPixel(element: HTMLElement | null) { } function useResolvedConfig( - config: (AnchorPropsWithSelection & InternalFloatingPanelProps) | null, + config: (Exclude & InternalFloatingPanelProps) | null, element?: HTMLElement | null ) { let gap = useResolvePxValue(config?.gap, element) From 270dbc732f5d349cc5ef4e817e1df76af319b953 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 22 Apr 2024 22:18:31 +0200 Subject: [PATCH 3/4] simplify `anchor` handling per-component --- .../src/components/combobox/combobox.tsx | 10 +++++----- .../src/components/listbox/listbox.tsx | 16 +++++++++------- .../src/components/menu/menu.tsx | 15 ++++++++++----- .../src/components/popover/popover.tsx | 10 +++++----- .../src/components/tooltip/tooltip.tsx | 18 +++++------------- 5 files changed, 34 insertions(+), 35 deletions(-) diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx index daf5fe83e..8236c8b08 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx @@ -46,6 +46,7 @@ import { useFloatingPanel, useFloatingPanelProps, useFloatingReference, + useResolvedAnchor, type AnchorProps, } from '../../internal/floating' import { FormFields } from '../../internal/form-fields' @@ -1534,7 +1535,7 @@ export type ComboboxOptionsProps & { hold?: boolean - anchor?: boolean | AnchorProps + anchor?: AnchorProps } > @@ -1546,15 +1547,14 @@ function OptionsFn( let { id = `headlessui-combobox-options-${internalId}`, hold = false, - anchor, + anchor: rawAnchor, ...theirProps } = props let data = useData('Combobox.Options') let actions = useActions('Combobox.Options') + let anchor = useResolvedAnchor(rawAnchor) - let [floatingRef, style] = useFloatingPanel( - anchor === false ? undefined : anchor === true ? {} : anchor - ) + let [floatingRef, style] = useFloatingPanel(anchor) let getFloatingPanelProps = useFloatingPanelProps() let optionsRef = useSyncRefs(data.optionsRef, ref, anchor ? floatingRef : null) diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx index e65deb394..a8f3fa87e 100644 --- a/packages/@headlessui-react/src/components/listbox/listbox.tsx +++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx @@ -44,6 +44,7 @@ import { useFloatingPanelProps, useFloatingReference, useFloatingReferenceProps, + useResolvedAnchor, type AnchorPropsWithSelection, } from '../../internal/floating' import { FormFields } from '../../internal/form-fields' @@ -868,7 +869,7 @@ export type ListboxOptionsProps > @@ -878,7 +879,13 @@ function OptionsFn( ref: Ref ) { let internalId = useId() - let { id = `headlessui-listbox-options-${internalId}`, anchor, modal, ...theirProps } = props + let { + id = `headlessui-listbox-options-${internalId}`, + anchor: rawAnchor, + modal, + ...theirProps + } = props + let anchor = useResolvedAnchor(rawAnchor) // Always use `modal` when `anchor` is passed in if (modal == null) { @@ -903,8 +910,6 @@ function OptionsFn( let initialOption = useRef(null) useEffect(() => { - if (typeof anchor === 'boolean') return - if (!anchor?.to?.includes('selection')) return if (!visible) { @@ -938,9 +943,6 @@ function OptionsFn( let panelEnabled = didButtonMove ? false : visible let anchorOptions = (() => { - if (anchor === false) return undefined - if (anchor === true) anchor = {} - if (anchor == null) return undefined if (data.listRef.current.size <= 0) return { ...anchor, inner: undefined } diff --git a/packages/@headlessui-react/src/components/menu/menu.tsx b/packages/@headlessui-react/src/components/menu/menu.tsx index 76b3c3a79..0e085c5f2 100644 --- a/packages/@headlessui-react/src/components/menu/menu.tsx +++ b/packages/@headlessui-react/src/components/menu/menu.tsx @@ -41,6 +41,7 @@ import { useFloatingPanelProps, useFloatingReference, useFloatingReferenceProps, + useResolvedAnchor, type AnchorProps, } from '../../internal/floating' import { Modal, ModalFeatures, type ModalProps } from '../../internal/modal' @@ -575,7 +576,7 @@ export type MenuItemsProps ItemsRenderPropArg, ItemsPropsWeControl, { - anchor?: boolean | AnchorProps + anchor?: AnchorProps modal?: boolean // ItemsRenderFeatures @@ -589,11 +590,15 @@ function ItemsFn( ref: Ref ) { let internalId = useId() - let { id = `headlessui-menu-items-${internalId}`, anchor, modal, ...theirProps } = props + let { + id = `headlessui-menu-items-${internalId}`, + anchor: rawAnchor, + modal, + ...theirProps + } = props + let anchor = useResolvedAnchor(rawAnchor) let [state, dispatch] = useMenuContext('Menu.Items') - let [floatingRef, style] = useFloatingPanel( - anchor === false ? undefined : anchor === true ? {} : anchor - ) + let [floatingRef, style] = useFloatingPanel(anchor) let getFloatingPanelProps = useFloatingPanelProps() let itemsRef = useSyncRefs(state.itemsRef, ref, anchor ? floatingRef : null) let ownerDocument = useOwnerDocument(state.itemsRef) diff --git a/packages/@headlessui-react/src/components/popover/popover.tsx b/packages/@headlessui-react/src/components/popover/popover.tsx index cb3d970f8..3cf529adc 100644 --- a/packages/@headlessui-react/src/components/popover/popover.tsx +++ b/packages/@headlessui-react/src/components/popover/popover.tsx @@ -42,6 +42,7 @@ import { useFloatingPanel, useFloatingPanelProps, useFloatingReference, + useResolvedAnchor, type AnchorProps, } from '../../internal/floating' import { Hidden, HiddenFeatures } from '../../internal/hidden' @@ -798,7 +799,7 @@ export type PopoverPanelProps( let { id = `headlessui-popover-panel-${internalId}`, focus = false, - anchor, + anchor: rawAnchor, modal, ...theirProps } = props @@ -827,9 +828,8 @@ function PanelFn( let afterPanelSentinelId = `headlessui-focus-sentinel-after-${internalId}` let internalPanelRef = useRef(null) - let [floatingRef, style] = useFloatingPanel( - anchor === false ? undefined : anchor === true ? {} : anchor - ) + let anchor = useResolvedAnchor(rawAnchor) + let [floatingRef, style] = useFloatingPanel(anchor) let getFloatingPanelProps = useFloatingPanelProps() // Always use `modal` when `anchor` is passed in diff --git a/packages/@headlessui-react/src/components/tooltip/tooltip.tsx b/packages/@headlessui-react/src/components/tooltip/tooltip.tsx index bec52255d..2a06f9997 100644 --- a/packages/@headlessui-react/src/components/tooltip/tooltip.tsx +++ b/packages/@headlessui-react/src/components/tooltip/tooltip.tsx @@ -23,6 +23,7 @@ import { FloatingProvider, useFloatingPanel, useFloatingReference, + useResolvedAnchor, type AnchorProps, } from '../../internal/floating' import { State, useOpenClosed } from '../../internal/open-closed' @@ -415,22 +416,14 @@ export type TooltipPanelProps + { anchor?: AnchorProps } & PropsForFeatures > function PanelFn( props: TooltipPanelProps, ref: Ref ) { - let { - anchor = { - to: 'top', - padding: 8, - gap: 8, - offset: -4, - } as AnchorProps, - ...theirProps - } = props + let { anchor: rawAnchor, ...theirProps } = props let data = useData('TooltipPanel') let usesOpenClosedState = useOpenClosed() @@ -443,9 +436,8 @@ function PanelFn( })() let internalPanelRef = useRef(null) - let [floatingRef, style] = useFloatingPanel( - visible && anchor !== false ? (anchor === true ? {} : anchor) : undefined - ) + let anchor = useResolvedAnchor(rawAnchor ?? { to: 'top', padding: 8, gap: 8, offset: -4 }) + let [floatingRef, style] = useFloatingPanel(visible ? anchor : undefined) let panelRef = useSyncRefs(internalPanelRef, ref, floatingRef) let ourProps = { From 78aad136af3eb84b76bc6b49b7c1003aafd3aabd Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 22 Apr 2024 20:14:13 +0200 Subject: [PATCH 4/4] update changelog --- packages/@headlessui-react/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md index ab91c27bc..322bd4eb5 100644 --- a/packages/@headlessui-react/CHANGELOG.md +++ b/packages/@headlessui-react/CHANGELOG.md @@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Expose the `--button-width` CSS variable on the `PopoverPanel` component ([#3058](https://github.com/tailwindlabs/headlessui/pull/3058)) - Close the `Combobox`, `Dialog`, `Listbox`, `Menu` and `Popover` components when the trigger disappears ([#3075](https://github.com/tailwindlabs/headlessui/pull/3075)) - Add new `CloseButton` component and `useClose` hook ([#3096](https://github.com/tailwindlabs/headlessui/pull/3096)) +- Allow passing a boolean to the `anchor` prop ([#3121](https://github.com/tailwindlabs/headlessui/pull/3121)) ## [1.7.19] - 2024-04-15