Skip to content

Commit c3f1989

Browse files
authored
fix(Popup): transfer zIndex & fix floated elements (#4094)
1 parent 26cab91 commit c3f1989

File tree

8 files changed

+211
-20
lines changed

8 files changed

+211
-20
lines changed

cypress/integration/Popup/Popup.visual.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ describe('Popup: visual', () => {
88
cy.get('[data-tid="button-popup"]').click()
99
cy.get('[data-tid="popup-content"]').should('be.visible')
1010

11+
// This screenshot contains invalid position of an unknown reason
1112
cy.percySnapshot('Popup: inside a Modal')
1213
})
1314

@@ -19,4 +20,22 @@ describe('Popup: visual', () => {
1920

2021
cy.percySnapshot('Popup: floating Button')
2122
})
23+
24+
it('flowing', () => {
25+
cy.visit('/maximize/popup-example-flowing')
26+
27+
cy.get('.ui.button').click()
28+
cy.get('.ui.popup').should('be.visible')
29+
30+
cy.percySnapshot('Popup: flowing')
31+
})
32+
33+
it('positionFixed', () => {
34+
cy.visit('/maximize/popup-example-position-fixed')
35+
36+
cy.get('.ui.button').click()
37+
cy.get('.ui.popup').should('be.visible')
38+
39+
cy.percySnapshot('Popup: positionFixed')
40+
})
2241
})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from 'react'
2+
import { Button, Popup } from 'semantic-ui-react'
3+
4+
const PopupExamplePopper = () => (
5+
<Popup
6+
content={
7+
<>
8+
A wrapping element in this Popup will have custom <code>id</code> &{' '}
9+
<code>zIndex</code>.
10+
</>
11+
}
12+
on='click'
13+
popper={{ id: 'popper-container', style: { zIndex: 2000 } }}
14+
trigger={<Button>Open a popup</Button>}
15+
/>
16+
)
17+
18+
export default PopupExamplePopper
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React from 'react'
2+
import { Button, Popup } from 'semantic-ui-react'
3+
4+
const PopupExamplePositionFixed = () => (
5+
<Popup
6+
content={
7+
<>
8+
This Popup is positioned with <code>position: fixed</code>
9+
</>
10+
}
11+
on='click'
12+
positionFixed
13+
trigger={<Button>Open a popup</Button>}
14+
/>
15+
)
16+
17+
export default PopupExamplePositionFixed

docs/src/examples/modules/Popup/Usage/index.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,40 @@ const PopupUsageExamples = () => (
9494
examplePath='modules/Popup/Usage/PopupExamplePopperDependencies'
9595
renderHtml={false}
9696
/>
97+
<ComponentExample
98+
title={
99+
<>
100+
<code>popper</code> element
101+
</>
102+
}
103+
description={
104+
<>
105+
From <code>[email protected]</code> we are using an additional
106+
wrapping element around <code>Popup</code> for positioning, see{' '}
107+
<a href='https://github.com/Semantic-Org/Semantic-UI-React/pull/3947'>
108+
Semantic-Org/Semantic-UI-React#3947
109+
</a>{' '}
110+
for more details. To pass props to this element <code>popper</code>{' '}
111+
shorthand can be used.
112+
</>
113+
}
114+
examplePath='modules/Popup/Usage/PopupExamplePopper'
115+
/>
116+
<ComponentExample
117+
title={
118+
<>
119+
Positioning via <code>position: fixed</code>
120+
</>
121+
}
122+
description={
123+
<>
124+
If your reference element is in a <code>fixed</code> container, use{' '}
125+
<code>positionFixed</code>. This will prevent any jumpiness since no
126+
repositioning is needed.
127+
</>
128+
}
129+
examplePath='modules/Popup/Usage/PopupExamplePositionFixed'
130+
/>
97131
<ComponentExample
98132
title='Actions'
99133
description='A popup can be triggered on hover, click, focus or multiple actions.'

docs/src/pages/MigrationGuide.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ The `popperModifiers` prop should be defined as an array with changed format (se
3232
+<Popup popperModifiers={[{ name: 'preventOverflow', options: { padding: 0 } }]} />
3333
```
3434

35+
### a wrapping element around `Popup`
36+
37+
We started to use an additional wrapping element around `Popup` for positioning, see [Semantic-Org/Semantic-UI-React#3947](https://github.com/Semantic-Org/Semantic-UI-React/pull/3947) for more details. To pass props to this element `popper` shorthand can be used, see [an example](/modules/popup/#usage-position-fixed).
38+
39+
⚠️We also made a fix in [Semantic-Org/Semantic-UI-React#4094](https://github.com/Semantic-Org/Semantic-UI-React/pull/4094) to transfer `zIndex` value to avoid any breaks.
40+
3541
## `Responsive` component was removed
3642

3743
`Responsive` component was deprecated in 1.1.0. There are two main reasons for the removal: there is no connection between breakpoints and pure SSR (server side rendering) support.

src/modules/Popup/Popup.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ export interface StrictPopupProps extends StrictPortalProps {
119119
/** Tells `Popper.js` to use the `position: fixed` strategy to position the popover. */
120120
positionFixed?: boolean
121121

122+
/** A wrapping element for an actual content that will be used for positioning. */
123+
popper?: SemanticShorthandItem<React.HTMLAttributes<HTMLDivElement>>
124+
122125
/** An array containing custom settings for the Popper.js modifiers. */
123126
popperModifiers?: any[]
124127

src/modules/Popup/Popup.js

Lines changed: 70 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import EventStack from '@semantic-ui-react/event-stack'
22
import cx from 'clsx'
33
import _ from 'lodash'
44
import PropTypes from 'prop-types'
5-
import React, { Component, createRef } from 'react'
5+
import React, { Component } from 'react'
66
import { Popper } from 'react-popper'
77
import shallowEqual from 'shallowequal'
88

99
import {
1010
eventStack,
1111
childrenUtils,
12+
createHTMLDivision,
1213
customPropTypes,
1314
getElementType,
1415
getUnhandledProps,
@@ -32,7 +33,9 @@ export default class Popup extends Component {
3233
state = {}
3334

3435
open = false
35-
triggerRef = createRef()
36+
zIndexWasSynced = false
37+
38+
triggerRef = React.createRef()
3639

3740
static getDerivedStateFromProps(props, state) {
3841
if (state.closed || state.disabled) return {}
@@ -145,6 +148,7 @@ export default class Popup extends Component {
145148
flowing,
146149
header,
147150
inverted,
151+
popper,
148152
size,
149153
style,
150154
wide,
@@ -174,25 +178,36 @@ export default class Popup extends Component {
174178
...style,
175179
}
176180

177-
return (
178-
// https://github.com/popperjs/popper-core/blob/f1f9d1ab75b6b0e962f90a5b2a50f6cfd307d794/src/createPopper.js#L136-L137
179-
// Heads up!
180-
// A wrapping `div` there is a pure magic, it's required as Popper warns on margins that are
181-
// defined by SUI CSS. It also means that this `div` will be positioned instead of `content`.
182-
<div ref={popperRef} style={popperStyle}>
183-
<ElementType {...contentRestProps} className={classes} style={styles}>
184-
{childrenUtils.isNil(children) ? (
185-
<>
186-
{PopupHeader.create(header, { autoGenerateKey: false })}
187-
{PopupContent.create(content, { autoGenerateKey: false })}
188-
</>
189-
) : (
190-
children
191-
)}
192-
{hideOnScroll && <EventStack on={this.hideOnScroll} name='scroll' target='window' />}
193-
</ElementType>
194-
</div>
181+
const innerElement = (
182+
<ElementType {...contentRestProps} className={classes} style={styles}>
183+
{childrenUtils.isNil(children) ? (
184+
<>
185+
{PopupHeader.create(header, { autoGenerateKey: false })}
186+
{PopupContent.create(content, { autoGenerateKey: false })}
187+
</>
188+
) : (
189+
children
190+
)}
191+
{hideOnScroll && <EventStack on={this.hideOnScroll} name='scroll' target='window' />}
192+
</ElementType>
195193
)
194+
195+
// https://github.com/popperjs/popper-core/blob/f1f9d1ab75b6b0e962f90a5b2a50f6cfd307d794/src/createPopper.js#L136-L137
196+
// Heads up!
197+
// A wrapping `div` there is a pure magic, it's required as Popper warns on margins that are
198+
// defined by SUI CSS. It also means that this `div` will be positioned instead of `content`.
199+
return createHTMLDivision(popper || {}, {
200+
overrideProps: {
201+
children: innerElement,
202+
ref: popperRef,
203+
style: {
204+
// Fixes layout for floated elements
205+
// https://github.com/Semantic-Org/Semantic-UI-React/issues/4092
206+
display: 'flex',
207+
...popperStyle,
208+
},
209+
},
210+
})
196211
}
197212

198213
render() {
@@ -202,6 +217,7 @@ export default class Popup extends Component {
202217
eventsEnabled,
203218
offset,
204219
pinned,
220+
popper,
205221
popperModifiers,
206222
position,
207223
positionFixed,
@@ -220,6 +236,37 @@ export default class Popup extends Component {
220236
{ name: 'preventOverflow', enabled: !!offset },
221237
{ name: 'offset', enabled: !!offset, options: { offset } },
222238
...popperModifiers,
239+
240+
// We are syncing zIndex from `.ui.popup.content` to avoid layering issues as in SUIR we are using an additional
241+
// `div` for Popper.js
242+
// https://github.com/Semantic-Org/Semantic-UI-React/issues/4083
243+
{
244+
name: 'syncZIndex',
245+
enabled: true,
246+
phase: 'beforeRead',
247+
fn: ({ state }) => {
248+
if (this.zIndexWasSynced) {
249+
return
250+
}
251+
252+
// if zIndex defined in <Popup popper={{ style: {} }} /> there is no sense to override it
253+
const definedZIndex = popper?.style?.zIndex
254+
255+
if (_.isUndefined(definedZIndex)) {
256+
// eslint-disable-next-line no-param-reassign
257+
state.elements.popper.style.zIndex = window.getComputedStyle(
258+
state.elements.popper.firstChild,
259+
).zIndex
260+
}
261+
262+
this.zIndexWasSynced = true
263+
},
264+
effect: () => {
265+
return () => {
266+
this.zIndexWasSynced = false
267+
}
268+
},
269+
},
223270
]
224271
debug('popper modifiers:', modifiers)
225272

@@ -352,6 +399,9 @@ Popup.propTypes = {
352399
/** Tells `Popper.js` to use the `position: fixed` strategy to position the popover. */
353400
positionFixed: PropTypes.bool,
354401

402+
/** A wrapping element for an actual content that will be used for positioning. */
403+
popper: customPropTypes.itemShorthand,
404+
355405
/** An array containing custom settings for the Popper.js modifiers. */
356406
popperModifiers: PropTypes.array,
357407

test/specs/modules/Popup/Popup-test.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,50 @@ describe('Popup', () => {
343343
})
344344
})
345345

346+
describe('popper', () => {
347+
it('passes a zIndex value from .popup', (done) => {
348+
wrapperMount(<Popup open style={{ zIndex: 5000 }} />)
349+
350+
const popperElement = wrapper.find('Popper').childAt(0)
351+
const popperNode = popperElement.getDOMNode()
352+
353+
setTimeout(() => {
354+
// zIndex transfer is done in a Popper modifier which will be executed in next frame
355+
popperNode.style.zIndex.should.be.equal('5000')
356+
done()
357+
}, 0)
358+
})
359+
360+
it('zIndex passed to a shorthand wins', (done) => {
361+
wrapperMount(<Popup open popper={{ style: { zIndex: 100 } }} style={{ zIndex: 5000 }} />)
362+
363+
const popperElement = wrapper.find('Popper').childAt(0)
364+
const popperNode = popperElement.getDOMNode()
365+
366+
setTimeout(() => {
367+
// zIndex transfer is done in a Popper modifier which will be executed in next frame
368+
popperNode.style.zIndex.should.be.equal('100')
369+
done()
370+
}, 0)
371+
})
372+
373+
it('additional props can be passed via shorthand', () => {
374+
wrapperMount(<Popup open popper={{ className: 'foo', id: 'bar' }} />)
375+
const popperElement = wrapper.find('Popper').childAt(0)
376+
377+
popperElement.should.have.prop('className', 'foo')
378+
popperElement.should.have.prop('id', 'bar')
379+
})
380+
381+
it('"style" prop is merged', () => {
382+
wrapperMount(<Popup open popper={{ style: { color: 'red', display: 'block' } }} />)
383+
const popperElement = wrapper.find('Popper').childAt(0)
384+
385+
popperElement.should.have.style('color', 'red')
386+
popperElement.should.have.style('display', 'flex')
387+
})
388+
})
389+
346390
describe('popperModifiers', () => {
347391
it('are passed to Popper', () => {
348392
const modifierOffset = { name: 'offset', options: { offset: [0, 10] } }

0 commit comments

Comments
 (0)