diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md index ab91c27bc9..322bd4eb55 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 diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx index 5968604349..8236c8b08a 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' @@ -1546,11 +1547,12 @@ 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) let getFloatingPanelProps = useFloatingPanelProps() diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx index 6cbf26d49e..a8f3fa87ea 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' @@ -878,13 +879,17 @@ 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 (anchor != null && modal == null) { - modal = true - } else if (modal == null) { - modal = false + if (modal == null) { + modal = Boolean(anchor) } let data = useData('Listbox.Options') diff --git a/packages/@headlessui-react/src/components/menu/menu.tsx b/packages/@headlessui-react/src/components/menu/menu.tsx index 329b303a9d..0e085c5f21 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' @@ -589,7 +590,13 @@ 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) let getFloatingPanelProps = useFloatingPanelProps() @@ -597,10 +604,8 @@ function ItemsFn( 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 829f10911a..3cf529adc2 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' @@ -815,7 +816,7 @@ function PanelFn( let { id = `headlessui-popover-panel-${internalId}`, focus = false, - anchor, + anchor: rawAnchor, modal, ...theirProps } = props @@ -827,14 +828,13 @@ function PanelFn( let afterPanelSentinelId = `headlessui-focus-sentinel-after-${internalId}` let internalPanelRef = useRef(null) + let anchor = useResolvedAnchor(rawAnchor) let [floatingRef, style] = useFloatingPanel(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 d277955a69..2a06f99979 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' @@ -422,15 +423,7 @@ 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,6 +436,7 @@ function PanelFn( })() let internalPanelRef = useRef(null) + 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) diff --git a/packages/@headlessui-react/src/internal/floating.tsx b/packages/@headlessui-react/src/internal/floating.tsx index e602d81074..e7c8200b4e 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)