diff --git a/collections/forms/i18n/en.pot b/collections/forms/i18n/en.pot index 6c0a78ad62..336699f285 100644 --- a/collections/forms/i18n/en.pot +++ b/collections/forms/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-01-12T11:22:32.560Z\n" -"PO-Revision-Date: 2026-01-12T11:22:32.562Z\n" +"POT-Creation-Date: 2026-06-22T14:39:53.502Z\n" +"PO-Revision-Date: 2026-06-22T14:39:53.503Z\n" msgid "Upload file" msgstr "Upload file" diff --git a/collections/ui/API.md b/collections/ui/API.md index 32fac8ce5b..5c9e347f6a 100644 --- a/collections/ui/API.md +++ b/collections/ui/API.md @@ -1509,8 +1509,6 @@ import { Popover } from '@dhis2/ui' |dataTest|string|``'dhis2-uicore-popover'``||| |elevation|string|``elevations.popover``||Box-shadow to create appearance of elevation. Use `elevations` constants from the UI library.| |maxWidth|number|``360``||| -|observePopperResize|boolean|||| -|observeReferenceResize|boolean|||| |placement|custom|``'top'``||| |reference|custom|||A React ref that refers to the element the Popover should position against| |onClickOutside|function|||| @@ -1534,13 +1532,10 @@ import { Popper } from '@dhis2/ui' |children|node||*|Content inside the Popper| |className|string|||| |dataTest|string|``'dhis2-uicore-popper'``||| -|modifiers|`arrayOf(\{
"name": "string",
"options": "object"
})`|``[]``||A property of the `createPopper` options. See [popper docs](https://popper.js.org/docs/v2/constructors/)| -|observePopperResize|boolean|||Makes the Popper update position when the **Popper content** changes size| -|observeReferenceResize|boolean|||Makes the Popper update position when the **reference element** changes size| -|placement|custom|``'auto'``||A property of the `createPopper` options. See [popper docs](https://popper.js.org/docs/v2/constructors/)| -|reference|custom|||A React ref, DOM node, or [virtual element](https://popper.js.org/docs/v2/virtual-elements/) for the popper to position itself against| -|strategy|'absolute' │ 'fixed'|||A property of the `createPopper` options. See [popper docs](https://popper.js.org/docs/v2/constructors/)| -|onFirstUpdate|function|||A property of the `createPopper` options. See [popper docs](https://popper.js.org/docs/v2/constructors/)| +|middleware|`arrayOf(object)`|||Floating UI middleware array. See https://floating-ui.com/docs/middleware| +|placement|custom|||Where to place the popper relative to its reference| +|reference|custom|||A React ref, DOM node, or virtual element for the popper to position itself against| +|strategy|'absolute' │ 'fixed'|||Positioning strategy. See https://floating-ui.com/docs/usefloating#strategy| ### Radio diff --git a/components/calendar/package.json b/components/calendar/package.json index b5b8e9dc37..92ebe67d64 100644 --- a/components/calendar/package.json +++ b/components/calendar/package.json @@ -42,6 +42,7 @@ "@dhis2/prop-types": "^3.1.2", "@dhis2/ui-constants": "10.16.3", "@dhis2/ui-icons": "10.16.3", + "@floating-ui/react-dom": "^2.1.8", "classnames": "^2.3.1", "prop-types": "^15.7.2" }, diff --git a/components/calendar/src/calendar-input/calendar-input.js b/components/calendar/src/calendar-input/calendar-input.js index fd722cf43b..235177398a 100644 --- a/components/calendar/src/calendar-input/calendar-input.js +++ b/components/calendar/src/calendar-input/calendar-input.js @@ -7,18 +7,14 @@ import { Button } from '@dhis2-ui/button' import { InputField } from '@dhis2-ui/input' import { Layer } from '@dhis2-ui/layer' import { Popper } from '@dhis2-ui/popper' +import { offset } from '@floating-ui/react-dom' import cx from 'classnames' import PropTypes from 'prop-types' import React, { useRef, useState, useMemo, useEffect } from 'react' import { CalendarContainer } from '../calendar/calendar-container.js' import i18n from '../locales/index.js' -const offsetModifier = { - name: 'offset', - options: { - offset: [0, 2], - }, -} +const calendarMiddleware = [offset(2)] export const CalendarInput = ({ onDateSelect: parentOnDateSelect, @@ -186,10 +182,8 @@ export const CalendarInput = ({ { - popperRef.current = component?.elements?.popper - }} + middleware={calendarMiddleware} + ref={popperRef} > { + const diagonal = Math.sqrt(2 * Math.pow(ARROW_SIZE, 2)) + const overflowInPx = (diagonal - ARROW_SIZE) / 2 + return Math.ceil(BORDER_RADIUS + overflowInPx) +} + +export const hideArrowWhenDisplaced = { + name: 'hideArrowWhenDisplaced', + fn({ rects, x, y, placement }) { + const popperTop = y + const popperBottom = y + rects.floating.height + const popperLeft = x + const popperRight = x + rects.floating.width + const refTop = rects.reference.y + const refBottom = rects.reference.y + rects.reference.height + const refLeft = rects.reference.x + const refRight = rects.reference.x + rects.reference.width + + const side = placement.split('-')[0] + let hidden = false + if (side === 'top') { + hidden = popperBottom > refBottom + } else if (side === 'bottom') { + hidden = popperTop < refTop + } else if (side === 'left') { + hidden = popperRight > refRight + } else if (side === 'right') { + hidden = popperLeft < refLeft + } + return { data: { hidden } } + }, +} + +const sharedDetectOverflowOptions = { + boundary: document.body, + rootBoundary: 'document', +} + +export const combineMiddleware = (showArrow, arrowElement) => { + const flipMiddleware = flip({ + ...sharedDetectOverflowOptions, + fallbackStrategy: 'initialPlacement', + }) + const shiftMiddleware = shift({ + ...sharedDetectOverflowOptions, + crossAxis: true, + }) + if (!showArrow) { + return [flipMiddleware, shiftMiddleware] + } + return [ + offset(ARROW_SIZE), + flipMiddleware, + shiftMiddleware, + floatingArrow({ + element: arrowElement, + padding: computeArrowPadding(), + }), + hideArrowWhenDisplaced, + ] +} diff --git a/components/popover/src/modifiers.js b/components/popover/src/modifiers.js deleted file mode 100644 index 979e9738be..0000000000 --- a/components/popover/src/modifiers.js +++ /dev/null @@ -1,62 +0,0 @@ -import { getBaseModifiers } from '@dhis2-ui/popper' -import { ARROW_SIZE } from './arrow.js' - -const BORDER_RADIUS = 4 - -const computeArrowPadding = () => { - // pythagoras - const diagonal = Math.sqrt(2 * Math.pow(ARROW_SIZE, 2)) - const overflowInPx = (diagonal - ARROW_SIZE) / 2 - const padding = BORDER_RADIUS + overflowInPx - - return Math.ceil(padding) -} - -const hideArrowWhenDisplaced = ({ state }) => { - const halfArrow = ARROW_SIZE / 2 - const displacement = state.modifiersData.preventOverflow - const referenceRect = state.rects.reference - const shouldHideArrow = - Math.abs(displacement.x) >= referenceRect.width + halfArrow || - Math.abs(displacement.y) >= referenceRect.height + halfArrow - - if (typeof state.attributes.arrow !== 'object') { - state.attributes.arrow = {} - } - - state.attributes.arrow['data-arrow-hidden'] = shouldHideArrow - - return state -} - -export const combineModifiers = (arrow, arrowElement, resizeObservers) => { - const baseModifiers = getBaseModifiers(resizeObservers) - - if (!arrow) { - return baseModifiers - } - - return [ - ...baseModifiers, - { - name: 'offset', - options: { - offset: [0, ARROW_SIZE], - }, - }, - { - name: 'arrow', - options: { - padding: computeArrowPadding(), - element: arrowElement, - }, - }, - { - name: 'hideArrowWhenDisplaced', - enabled: true, - phase: 'main', - fn: hideArrowWhenDisplaced, - requires: ['preventOverflow'], - }, - ] -} diff --git a/components/popover/src/popover.js b/components/popover/src/popover.js index 1fe3b59514..cf859f2d6e 100644 --- a/components/popover/src/popover.js +++ b/components/popover/src/popover.js @@ -1,10 +1,11 @@ import { colors, elevations, sharedPropTypes } from '@dhis2/ui-constants' import { Layer } from '@dhis2-ui/layer' -import { getReferenceElement, usePopper } from '@dhis2-ui/popper' +import { getReferenceElement } from '@dhis2-ui/popper' +import { autoUpdate, useFloating } from '@floating-ui/react-dom' import PropTypes from 'prop-types' -import React, { useState, useMemo } from 'react' +import React, { useMemo, useState } from 'react' import { Arrow } from './arrow.js' -import { combineModifiers } from './modifiers.js' +import { combineMiddleware } from './middleware.js' const Popover = ({ children, @@ -14,49 +15,58 @@ const Popover = ({ dataTest = 'dhis2-uicore-popover', elevation = elevations.popover, maxWidth = 360, - observePopperResize, - observeReferenceResize, placement = 'top', onClickOutside, }) => { const referenceElement = getReferenceElement(reference) - const [popperElement, setPopperElement] = useState(null) const [arrowElement, setArrowElement] = useState(null) - const modifiers = useMemo( - () => - combineModifiers(arrow, arrowElement, { - observePopperResize, - observeReferenceResize, - }), - [arrow, arrowElement, observePopperResize, observeReferenceResize] + + const middleware = useMemo( + () => combineMiddleware(arrow, arrowElement), + [arrow, arrowElement] ) - const { styles, attributes } = usePopper(referenceElement, popperElement, { + + const { + refs, + floatingStyles, + placement: actualPlacement, + middlewareData, + } = useFloating({ + elements: { reference: referenceElement }, placement, - modifiers, + middleware, + whileElementsMounted: autoUpdate, }) + const arrowStyles = useMemo(() => { + const arrowData = middlewareData.arrow + if (!arrowData) { + return undefined + } + return { + position: 'absolute', + ...(arrowData.x != null && { left: `${arrowData.x}px` }), + ...(arrowData.y != null && { top: `${arrowData.y}px` }), + } + }, [middlewareData.arrow]) + + const arrowHidden = Boolean(middlewareData.hideArrowWhenDisplaced?.hidden) + return (
{children} {arrow && (