Skip to content

Commit

Permalink
Allow passing a boolean to the anchor prop (#3121)
Browse files Browse the repository at this point in the history
  • Loading branch information
RobinMalfait authored Apr 22, 2024
1 parent 8a272c1 commit 6c9e4b2
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 49 deletions.
1 change: 1 addition & 0 deletions packages/@headlessui-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
useFloatingPanel,
useFloatingPanelProps,
useFloatingReference,
useResolvedAnchor,
type AnchorProps,
} from '../../internal/floating'
import { FormFields } from '../../internal/form-fields'
Expand Down Expand Up @@ -1546,11 +1547,12 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
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()
Expand Down
15 changes: 10 additions & 5 deletions packages/@headlessui-react/src/components/listbox/listbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
useFloatingPanelProps,
useFloatingReference,
useFloatingReferenceProps,
useResolvedAnchor,
type AnchorPropsWithSelection,
} from '../../internal/floating'
import { FormFields } from '../../internal/form-fields'
Expand Down Expand Up @@ -878,13 +879,17 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
ref: Ref<HTMLElement>
) {
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')
Expand Down
15 changes: 10 additions & 5 deletions packages/@headlessui-react/src/components/menu/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
useFloatingPanelProps,
useFloatingReference,
useFloatingReferenceProps,
useResolvedAnchor,
type AnchorProps,
} from '../../internal/floating'
import { Modal, ModalFeatures, type ModalProps } from '../../internal/modal'
Expand Down Expand Up @@ -589,18 +590,22 @@ function ItemsFn<TTag extends ElementType = typeof DEFAULT_ITEMS_TAG>(
ref: Ref<HTMLDivElement>
) {
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()
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()
Expand Down
10 changes: 5 additions & 5 deletions packages/@headlessui-react/src/components/popover/popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
useFloatingPanel,
useFloatingPanelProps,
useFloatingReference,
useResolvedAnchor,
type AnchorProps,
} from '../../internal/floating'
import { Hidden, HiddenFeatures } from '../../internal/hidden'
Expand Down Expand Up @@ -815,7 +816,7 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
let {
id = `headlessui-popover-panel-${internalId}`,
focus = false,
anchor,
anchor: rawAnchor,
modal,
...theirProps
} = props
Expand All @@ -827,14 +828,13 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
let afterPanelSentinelId = `headlessui-focus-sentinel-after-${internalId}`

let internalPanelRef = useRef<HTMLDivElement | null>(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) => {
Expand Down
12 changes: 3 additions & 9 deletions packages/@headlessui-react/src/components/tooltip/tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
FloatingProvider,
useFloatingPanel,
useFloatingReference,
useResolvedAnchor,
type AnchorProps,
} from '../../internal/floating'
import { State, useOpenClosed } from '../../internal/open-closed'
Expand Down Expand Up @@ -422,15 +423,7 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
props: TooltipPanelProps<TTag>,
ref: Ref<HTMLElement>
) {
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()
Expand All @@ -443,6 +436,7 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
})()

let internalPanelRef = useRef<HTMLElement | null>(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)

Expand Down
65 changes: 41 additions & 24 deletions packages/@headlessui-react/src/internal/floating.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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<AnchorPropsWithSelection, boolean> | null) => void) | null
>(null)
PlacementContext.displayName = 'PlacementContext'

export function useResolvedAnchor<T extends AnchorProps | AnchorPropsWithSelection>(
anchor?: T
): Exclude<T, boolean> | null {
return useMemo(() => {
if (anchor === true) return {} as Exclude<T, boolean> // Enable with defaults
if (!anchor) return null // Disable entirely
return anchor as Exclude<T, boolean> // User-provided value
}, [anchor])
}

export function useFloatingReference() {
return useContext(FloatingContext).setReference
}
Expand All @@ -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,
Expand Down Expand Up @@ -372,7 +389,7 @@ function useFixScrollingPixel(element: HTMLElement | null) {
}

function useResolvedConfig(
config: (AnchorPropsWithSelection & InternalFloatingPanelProps) | null,
config: (Exclude<AnchorPropsWithSelection, boolean> & InternalFloatingPanelProps) | null,
element?: HTMLElement | null
) {
let gap = useResolvePxValue(config?.gap, element)
Expand Down

0 comments on commit 6c9e4b2

Please sign in to comment.