Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow passing a boolean to the anchor prop #3121

Merged
merged 4 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/@headlessui-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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
Loading