@duro-app/ui 3.4.0 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duro-app/ui",
3
- "version": "3.4.0",
3
+ "version": "3.4.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -64,7 +64,7 @@
64
64
  },
65
65
  "dependencies": {
66
66
  "@tanstack/react-virtual": "^3.14.6",
67
- "@duro-app/tokens": "^3.4.0"
67
+ "@duro-app/tokens": "^3.4.1"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@babel/preset-typescript": "^7.28.0",
@@ -9,7 +9,8 @@ import {Inline} from '../Inline/Inline'
9
9
  import {Stack} from '../Stack/Stack'
10
10
  import {Input} from '../Input/Input'
11
11
  import {Field} from '../Field/Field'
12
- import {Heading} from '../Heading/Heading'
12
+ import {spacing} from '@duro-app/tokens/tokens/spacing.css'
13
+ import {colors} from '@duro-app/tokens/tokens/colors.css'
13
14
 
14
15
  const meta: Meta = {
15
16
  title: 'Components/Dialog',
@@ -321,3 +322,54 @@ function BackdropDismissDemo() {
321
322
  export const BackdropDismiss: Story = {
322
323
  render: () => <BackdropDismissDemo />,
323
324
  }
325
+
326
+ // --- Inside a transformed ancestor ---
327
+ // A fixed overlay inside an ancestor with transform / filter / backdrop-filter
328
+ // is positioned against that ancestor, not the viewport. A consumer's sticky
329
+ // blurred top bar is exactly that: the dialog must still centre on the screen,
330
+ // which it does because Portal renders into the ThemeProvider mount.
331
+
332
+ const barStyles = css.create({
333
+ bar: {
334
+ transform: 'translateZ(0)',
335
+ padding: spacing.sm,
336
+ borderWidth: 1,
337
+ borderStyle: 'solid',
338
+ borderColor: colors.border,
339
+ },
340
+ })
341
+
342
+ export const InsideTransformedAncestor: Story = {
343
+ render: () => (
344
+ <html.div style={barStyles.bar}>
345
+ <Dialog.Root>
346
+ <Dialog.Trigger>
347
+ <Button>Open from the bar</Button>
348
+ </Dialog.Trigger>
349
+ <Dialog.Portal size="sm">
350
+ <Dialog.Header>
351
+ <Dialog.Title>Centred on the screen</Dialog.Title>
352
+ <Dialog.Close />
353
+ </Dialog.Header>
354
+ <Dialog.Body>
355
+ <Text>Not on the bar it was opened from.</Text>
356
+ </Dialog.Body>
357
+ </Dialog.Portal>
358
+ </Dialog.Root>
359
+ </html.div>
360
+ ),
361
+ play: async ({canvasElement}) => {
362
+ const canvas = within(canvasElement)
363
+ await userEvent.click(canvas.getByText('Open from the bar'))
364
+ const doc = canvasElement.ownerDocument
365
+ const dialog = doc.querySelector('[role="dialog"]') as HTMLElement
366
+ await expect(dialog).not.toBeNull()
367
+ const bar = canvas.getByText('Open from the bar').closest('div') as HTMLElement
368
+ // rendered outside the bar…
369
+ await expect(bar.contains(dialog)).toBe(false)
370
+ // …and fully on screen, not centred on a bar at the top edge
371
+ const r = dialog.getBoundingClientRect()
372
+ await expect(r.top).toBeGreaterThanOrEqual(0)
373
+ await expect(r.bottom).toBeLessThanOrEqual(doc.defaultView!.innerHeight)
374
+ },
375
+ }
@@ -1,3 +1,4 @@
1
+ import {createPortal} from 'react-dom'
1
2
  import {
2
3
  type ReactNode,
3
4
  type RefObject,
@@ -12,6 +13,7 @@ import {
12
13
  import {html} from 'react-strict-dom'
13
14
  import {DURATION_MS, type DurationToken} from '@duro-app/tokens/keys'
14
15
  import {styles} from './styles.css'
16
+ import {usePortalMount} from '../ThemeProvider/ThemeProvider'
15
17
 
16
18
  // --- Types ---
17
19
 
@@ -194,6 +196,7 @@ function Portal({children, size = 'md'}: PortalProps) {
194
196
  descriptionId,
195
197
  popupRef,
196
198
  } = useDialog()
199
+ const mount = usePortalMount()
197
200
 
198
201
  const handleBackdropClick = useCallback(() => {
199
202
  if (dismissable) {
@@ -203,7 +206,7 @@ function Portal({children, size = 'md'}: PortalProps) {
203
206
 
204
207
  if (!open) return null
205
208
 
206
- return (
209
+ const node = (
207
210
  <>
208
211
  {/* Backdrop — click to dismiss */}
209
212
  <html.div
@@ -238,6 +241,13 @@ function Portal({children, size = 'md'}: PortalProps) {
238
241
  </html.div>
239
242
  </>
240
243
  )
244
+
245
+ // Out of the tree into the ThemeProvider mount (like Select and Combobox):
246
+ // a fixed overlay inside an ancestor with transform / filter /
247
+ // backdrop-filter is positioned against THAT ancestor, not the viewport —
248
+ // a dialog opened from a sticky blurred top bar centred on the bar. Without
249
+ // a mount (SSR, no ThemeProvider) it renders in place.
250
+ return mount ? createPortal(node, mount) : node
241
251
  }
242
252
 
243
253
  // --- Header ---
@@ -1,3 +1,4 @@
1
+ import {createPortal} from 'react-dom'
1
2
  import {
2
3
  type ReactNode,
3
4
  type RefObject,
@@ -12,6 +13,7 @@ import {
12
13
  import {html} from 'react-strict-dom'
13
14
  import {DURATION_MS, type DurationToken} from '@duro-app/tokens/keys'
14
15
  import {styles} from './styles.css'
16
+ import {usePortalMount} from '../ThemeProvider/ThemeProvider'
15
17
  import {useSwipeDismiss} from './useSwipeDismiss'
16
18
 
17
19
  // --- Types ---
@@ -230,6 +232,7 @@ function Portal({children, size = 'md'}: PortalProps) {
230
232
  descriptionId,
231
233
  panelRef,
232
234
  } = useDrawer()
235
+ const mount = usePortalMount()
233
236
 
234
237
  const handleBackdropClick = useCallback(() => {
235
238
  if (dismissable) {
@@ -249,7 +252,7 @@ function Portal({children, size = 'md'}: PortalProps) {
249
252
 
250
253
  const isHorizontal = anchor === 'left' || anchor === 'right'
251
254
 
252
- return (
255
+ const node = (
253
256
  <>
254
257
  {/* Backdrop — click to dismiss */}
255
258
  <html.div
@@ -287,6 +290,13 @@ function Portal({children, size = 'md'}: PortalProps) {
287
290
  </html.div>
288
291
  </>
289
292
  )
293
+
294
+ // Out of the tree into the ThemeProvider mount (like Select and Combobox):
295
+ // a fixed overlay inside an ancestor with transform / filter /
296
+ // backdrop-filter is positioned against THAT ancestor, not the viewport —
297
+ // a dialog opened from a sticky blurred top bar centred on the bar. Without
298
+ // a mount (SSR, no ThemeProvider) it renders in place.
299
+ return mount ? createPortal(node, mount) : node
290
300
  }
291
301
 
292
302
  // --- Header ---