Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5ee82f8
refactor(popper)!: migrate from popper.js to @floating-ui/react-dom
HendrikThePendric Jun 18, 2026
7001979
fix(popper, popover): enable shift on the placement axis
HendrikThePendric Jun 18, 2026
c5401c8
fix(popper, popover): use document.body as positioning boundary
HendrikThePendric Jun 18, 2026
8374ad9
fix(popover): rewrite hideArrowWhenDisplaced to use geometric check
HendrikThePendric Jun 18, 2026
12988e4
fix(popper, popover): drop document.body boundary, use FUI defaults
HendrikThePendric Jun 18, 2026
bbd5a62
fix(popper, popover): restore popper.js v2 boundary and flip semantics
HendrikThePendric Jun 22, 2026
533f615
fix(popper, popover): add rootBoundary: document to flip/shift
HendrikThePendric Jun 22, 2026
cfe172b
feat(popper)!: expose middleware data via render-prop
HendrikThePendric Jun 22, 2026
b7a0eb4
docs(popper): add story demonstrating custom middleware and render-prop
HendrikThePendric Jun 22, 2026
984178b
refactor(popper)!: expose usePopper hook, drop render-prop on <Popper>
HendrikThePendric Jun 22, 2026
4f291cd
fix(tooltip): restore data-test='dhis2-uicore-popper' wrapper
HendrikThePendric Jun 22, 2026
d2476bb
docs: regenerate API.md files
HendrikThePendric Jun 22, 2026
a01ada9
chore: re-trigger CI
HendrikThePendric Jun 22, 2026
16d3206
chore(popover, selector-bar, tooltip): sort package.json dependencies
HendrikThePendric Jun 22, 2026
18977ec
fix(tooltip): scope tooltip-body styles to inner content div
HendrikThePendric Jun 22, 2026
2883413
feat(popper): restore placement='auto' / 'auto-start' / 'auto-end'
HendrikThePendric Jun 22, 2026
95e8bad
chore(popper): prettier-fix use-popper.js after auto-placement change
HendrikThePendric Jun 22, 2026
8035c34
chore(popper): replace nested ternary with lookup table
HendrikThePendric Jun 22, 2026
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
4 changes: 2 additions & 2 deletions collections/forms/i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
13 changes: 4 additions & 9 deletions collections/ui/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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||||
Expand All @@ -1534,13 +1532,10 @@ import { Popper } from '@dhis2/ui'
|children|node||*|Content inside the Popper|
|className|string||||
|dataTest|string|``'dhis2-uicore-popper'``|||
|modifiers|`arrayOf(\{<br/> "name": "string",<br/> "options": "object"<br/>})`|``[]``||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

Expand Down
1 change: 1 addition & 0 deletions components/calendar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
14 changes: 4 additions & 10 deletions components/calendar/src/calendar-input/calendar-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -186,10 +182,8 @@ export const CalendarInput = ({
<Popper
reference={ref}
placement="bottom-start"
modifiers={[offsetModifier]}
onFirstUpdate={(component) => {
popperRef.current = component?.elements?.popper
}}
middleware={calendarMiddleware}
ref={popperRef}
>
<CalendarContainer
{...calendarProps}
Expand Down
2 changes: 0 additions & 2 deletions components/popover/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,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||||
3 changes: 2 additions & 1 deletion components/popover/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@
"styled-jsx": "^4"
},
"dependencies": {
"@dhis2/prop-types": "^3.1.2",
"@dhis2-ui/layer": "10.16.3",
"@dhis2-ui/popper": "10.16.3",
"@dhis2/prop-types": "^3.1.2",
"@dhis2/ui-constants": "10.16.3",
"@floating-ui/react-dom": "^2.1.8",
"classnames": "^2.3.1",
"prop-types": "^15.7.2"
},
Expand Down
71 changes: 71 additions & 0 deletions components/popover/src/middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {
arrow as floatingArrow,
flip,
offset,
shift,
} from '@floating-ui/react-dom'
import { ARROW_SIZE } from './arrow.js'

const BORDER_RADIUS = 4

const computeArrowPadding = () => {
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,
]
}
62 changes: 0 additions & 62 deletions components/popover/src/modifiers.js

This file was deleted.

66 changes: 37 additions & 29 deletions components/popover/src/popover.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 (
<Layer onBackdropClick={onClickOutside}>
<div
data-test={dataTest}
className={className}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
ref={refs.setFloating}
style={floatingStyles}
>
{children}
{arrow && (
<Arrow
hidden={
attributes.arrow &&
attributes.arrow['data-arrow-hidden']
}
popperPlacement={
attributes.popper &&
attributes.popper['data-popper-placement']
}
hidden={arrowHidden}
popperPlacement={actualPlacement}
ref={setArrowElement}
styles={styles.arrow}
styles={arrowStyles}
/>
)}
<style jsx>{`
Expand All @@ -81,8 +91,6 @@ Popover.propTypes = {
/** Box-shadow to create appearance of elevation. Use `elevations` constants from the UI library. */
elevation: PropTypes.string,
maxWidth: PropTypes.number,
observePopperResize: PropTypes.bool,
observeReferenceResize: PropTypes.bool,
placement: sharedPropTypes.popperPlacementPropType,
/** A React ref that refers to the element the Popover should position against */
reference: sharedPropTypes.popperReferencePropType,
Expand Down
6 changes: 2 additions & 4 deletions components/popover/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Placement, VirtualElement } from '@floating-ui/react-dom'
import * as React from 'react'
import { VirtualElement, Options as PopperOptions } from '@popperjs/core'

type PopperReference = VirtualElement | Element
type ReferenceElement =
Expand All @@ -19,9 +19,7 @@ export interface PopoverProps {
*/
elevation?: string
maxWidth?: number
observePopperResize?: boolean
observeReferenceResize?: boolean
placement?: PopperOptions['placement']
placement?: Placement
/**
* A React ref that refers to the element the Popover should position against
*/
Expand Down
11 changes: 4 additions & 7 deletions components/popper/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ import { Popper } from '@dhis2/ui'
|children|node||*|Content inside the Popper|
|className|string||||
|dataTest|string|``'dhis2-uicore-popper'``|||
|modifiers|`arrayOf(\{<br/> "name": "string",<br/> "options": "object"<br/>})`|``[]``||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|
5 changes: 2 additions & 3 deletions components/popper/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,9 @@
"dependencies": {
"@dhis2/prop-types": "^3.1.2",
"@dhis2/ui-constants": "10.16.3",
"@popperjs/core": "^2.11.8",
"@floating-ui/react-dom": "^2.1.8",
"classnames": "^2.3.1",
"prop-types": "^15.7.2",
"resize-observer-polyfill": "^1.5.1"
"prop-types": "^15.7.2"
},
"files": [
"build",
Expand Down
2 changes: 1 addition & 1 deletion components/popper/src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { Popper } from './popper.js'
export { getReferenceElement } from './get-reference-element.js'
export { getBaseModifiers } from './modifiers.js'
export { getBaseMiddleware } from './middleware.js'
export { usePopper } from './use-popper.js'
Loading
Loading