fix: consider scroll in portal elements and handle stale portal containers

This commit is contained in:
jongomez 2023-10-19 14:18:01 +01:00 committed by Jon
parent 85b148a8c9
commit 55d4bda014
5 changed files with 35 additions and 33 deletions

View File

@ -16,6 +16,7 @@ import {
omitCommonProps, omitCommonProps,
} from '../../utils/useCommonProps' } from '../../utils/useCommonProps'
import { TooltipBase } from '../TooltipBase' import { TooltipBase } from '../TooltipBase'
import { useUpdatePositionStyle } from '../../utils/useUpdatePositionStyle'
export const CALENDAR_MIN_YEAR = 1850 export const CALENDAR_MIN_YEAR = 1850
export const CALENDAR_MAX_YEAR = 2100 export const CALENDAR_MAX_YEAR = 2100
@ -64,7 +65,6 @@ export const Calendar: React.FC<CalendarProps> & {
}) => { }) => {
const commonProps = useCommonProps(props) const commonProps = useCommonProps(props)
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
const [style, setStyle] = useState<React.CSSProperties>({})
const [startDate, setStartDate] = useState<Date | null>( const [startDate, setStartDate] = useState<Date | null>(
startDateProp startDateProp
? safeConvertDate(startDateProp, minDate, maxDate).date ? safeConvertDate(startDateProp, minDate, maxDate).date
@ -147,19 +147,7 @@ export const Calendar: React.FC<CalendarProps> & {
} }
}, [endDate]) }, [endDate])
const updateStyle = () => { const positionStyle = useUpdatePositionStyle(handleRef, open)
const { width, height, top, left } =
handleRef.current!.getBoundingClientRect()
setStyle({
left,
width,
top: top + height,
})
}
useEffect(() => {
updateStyle()
}, [open])
return ( return (
<CalendarContext.Provider <CalendarContext.Provider
@ -189,7 +177,7 @@ export const Calendar: React.FC<CalendarProps> & {
disabled && calendarClasses.disabled, disabled && calendarClasses.disabled,
)} )}
rootRef={ref} rootRef={ref}
style={{ ...style, ...(props.style ?? {}) }} style={{ ...positionStyle, ...(props.style ?? {}) }}
arrowOffset={tooltipArrowOffset} arrowOffset={tooltipArrowOffset}
> >
<div className={clsx(calendarClasses.container)}> <div className={clsx(calendarClasses.container)}>

View File

@ -70,7 +70,6 @@ export const DatePicker: React.FC<DatePickerProps> & {
<div <div
id={inputId} id={inputId}
ref={ref} ref={ref}
{...props}
className={clsx( className={clsx(
{ ...omitCommonProps(props) }, { ...omitCommonProps(props) },
props.className, props.className,

View File

@ -7,6 +7,7 @@ import {
useCommonProps, useCommonProps,
} from '../../utils/useCommonProps' } from '../../utils/useCommonProps'
import { dropdownMenuClasses } from './DropdownMenu.classes' import { dropdownMenuClasses } from './DropdownMenu.classes'
import { useUpdatePositionStyle } from '../../utils/useUpdatePositionStyle'
export type DropdownMenuProps = CommonProps & export type DropdownMenuProps = CommonProps &
Omit<React.HTMLAttributes<HTMLUListElement>, 'label'> & { Omit<React.HTMLAttributes<HTMLUListElement>, 'label'> & {
@ -30,7 +31,6 @@ export const DropdownMenu: React.FC<DropdownMenuProps> & {
}) => { }) => {
const commonProps = useCommonProps(props) const commonProps = useCommonProps(props)
const ref = useRef<HTMLUListElement>(null) const ref = useRef<HTMLUListElement>(null)
const [style, setStyle] = useState<React.CSSProperties>({})
useClickAway(ref, (event) => { useClickAway(ref, (event) => {
if (!open || event.composedPath().includes(handleRef.current!)) return if (!open || event.composedPath().includes(handleRef.current!)) return
@ -38,20 +38,7 @@ export const DropdownMenu: React.FC<DropdownMenuProps> & {
onClose && onClose() onClose && onClose()
}) })
const updateStyle = () => { const positionStyle = useUpdatePositionStyle(handleRef, open)
const { width, height, top, left } =
handleRef.current!.getBoundingClientRect()
setStyle({
left,
width,
top: top + height,
})
}
useEffect(() => {
updateStyle()
}, [open])
return ( return (
<ul <ul
@ -59,7 +46,7 @@ export const DropdownMenu: React.FC<DropdownMenuProps> & {
ref={ref} ref={ref}
role="listbox" role="listbox"
aria-label={label} aria-label={label}
style={{ ...style, ...(props.style ?? {}) }} style={{ ...positionStyle, ...(props.style ?? {}) }}
className={clsx( className={clsx(
commonProps.className, commonProps.className,
props.className, props.className,

View File

@ -13,7 +13,14 @@ export const usePortal = ({ parentId }: Props) => {
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined' || !elementRef.current) return if (typeof window === 'undefined' || !elementRef.current) return
document.getElementById(parentId)?.appendChild(elementRef.current)
const parentElements = document.querySelectorAll(`#${parentId}`)
// In some places (e.g. storybook), there may be multiple portal containers when a component
// is rendered multiple times. Here, we append only to the last found parent element.
// This is because usually the last parent is the most recently rendered one.
// Without this, we may append to a parent that is about to be removed from the DOM.
parentElements[parentElements.length - 1]?.appendChild(elementRef.current)
return () => { return () => {
try { try {

View File

@ -0,0 +1,21 @@
import { useEffect, useState } from 'react'
export const useUpdatePositionStyle = (
handleRef: React.RefObject<HTMLElement>,
tiggerUpdate: boolean | undefined,
): React.CSSProperties => {
const [style, setStyle] = useState<React.CSSProperties>({})
useEffect(() => {
const { width, height, top, left } =
handleRef.current!.getBoundingClientRect()
setStyle({
left: left + window.scrollX,
width,
top: top + height + window.scrollY,
})
}, [tiggerUpdate])
return style
}