From ef540fe8c19471a0b1e953b28bf9336081b82866 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Thu, 24 Sep 2026 22:18:07 +0300 Subject: [PATCH] Fix popovers not open in form for older Safari --- app/components/elements/Menu.tsx | 6 +++--- app/components/elements/Popover.tsx | 13 +++++++++---- app/components/elements/Select.tsx | 6 +++--- changelog/2026-09-24-dropdowns-older-safari.md | 4 ++++ 4 files changed, 19 insertions(+), 10 deletions(-) create mode 100644 changelog/2026-09-24-dropdowns-older-safari.md diff --git a/app/components/elements/Menu.tsx b/app/components/elements/Menu.tsx index 15b09dc66..a60ac7e0c 100644 --- a/app/components/elements/Menu.tsx +++ b/app/components/elements/Menu.tsx @@ -12,7 +12,7 @@ import styles from "./Menu.module.css"; import { focusLeftTo, isOwnToggle, - usePopoverTargetOnceHydrated, + usePopoverTriggerPropsOnceHydrated, useShowPopoverOnOpen, } from "./Popover"; import { useFloatingLayer } from "./useFloatingLayer"; @@ -44,7 +44,7 @@ export function SendouMenu({ eager, }: SendouMenuProps) { const popoverId = `${React.useId()}-menu`; - const popoverTarget = usePopoverTargetOnceHydrated(popoverId); + const popoverTriggerProps = usePopoverTriggerPropsOnceHydrated(popoverId); const topLayerStyle = useTopLayerViewTransitionStyle(); const [open, setOpen] = React.useState(false); @@ -114,7 +114,7 @@ export function SendouMenu({ onBlur={onBlur} > {React.cloneElement(trigger, { - popoverTarget, + ...popoverTriggerProps, "aria-expanded": open, "aria-haspopup": "menu", })} diff --git a/app/components/elements/Popover.tsx b/app/components/elements/Popover.tsx index 626c53bcf..1f579d74a 100644 --- a/app/components/elements/Popover.tsx +++ b/app/components/elements/Popover.tsx @@ -19,8 +19,13 @@ export function isOwnToggle(event: React.ToggleEvent) { return event.target === event.currentTarget; } -export function usePopoverTargetOnceHydrated(popoverId: string) { - return useHydrated() ? popoverId : undefined; +/** + * Props making the trigger open the popover natively once hydrated. `form=""` + * detaches the trigger from a surrounding form, as Safari before 18.2 ignores + * `popovertarget` on a button that has a form owner. + */ +export function usePopoverTriggerPropsOnceHydrated(popoverId: string) { + return useHydrated() ? { popoverTarget: popoverId, form: "" } : {}; } /** @@ -106,7 +111,7 @@ export function SendouPopover({ eager?: boolean; }) { const popoverId = `${React.useId()}-popover`; - const popoverTarget = usePopoverTargetOnceHydrated(popoverId); + const popoverTriggerProps = usePopoverTriggerPropsOnceHydrated(popoverId); const [isControlled] = React.useState(isOpen !== undefined); const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false); @@ -190,7 +195,7 @@ export function SendouPopover({ onBlur={onBlur} > {React.cloneElement(trigger, { - popoverTarget, + ...popoverTriggerProps, "aria-haspopup": "dialog", })} diff --git a/app/components/elements/Select.tsx b/app/components/elements/Select.tsx index 71faab644..7821f084d 100644 --- a/app/components/elements/Select.tsx +++ b/app/components/elements/Select.tsx @@ -10,7 +10,7 @@ import { useTopLayerViewTransitionStyle } from "~/utils/view-transition"; import { focusLeftTo, isOwnToggle, - usePopoverTargetOnceHydrated, + usePopoverTriggerPropsOnceHydrated, useShowPopoverOnOpen, } from "./Popover"; import styles from "./Select.module.css"; @@ -146,7 +146,7 @@ export function SendouSelect({ const uid = React.useId(); const topLayerStyle = useTopLayerViewTransitionStyle(); const popoverId = `${uid}-select-popover`; - const popoverTarget = usePopoverTargetOnceHydrated(popoverId); + const popoverTriggerProps = usePopoverTriggerPropsOnceHydrated(popoverId); const listboxId = `${uid}-select-listbox`; const labelId = label ? `${uid}-select-label` : undefined; const valueId = `${uid}-select-value`; @@ -536,7 +536,7 @@ export function SendouSelect({ : undefined } data-required={isRequired || undefined} - popoverTarget={popoverTarget} + {...popoverTriggerProps} onKeyDown={onTriggerKeyDown} >