@open-mercato/ui 0.7.1-develop.7122.1.421cefe668 → 0.7.1-develop.7130.1.fef2396fd8
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/.turbo/turbo-build.log +1 -1
- package/dist/ai/AiAssistantLauncher.js +7 -1
- package/dist/ai/AiAssistantLauncher.js.map +2 -2
- package/dist/ai/AiChatSessions.js +4 -1
- package/dist/ai/AiChatSessions.js.map +2 -2
- package/dist/ai/index.js +8 -0
- package/dist/ai/index.js.map +2 -2
- package/dist/ai/useAiAssistantAvailable.js +32 -0
- package/dist/ai/useAiAssistantAvailable.js.map +7 -0
- package/dist/backend/detail/addressFormat.js +18 -2
- package/dist/backend/detail/addressFormat.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js +108 -92
- package/dist/backend/inputs/ComboboxInput.js.map +2 -2
- package/package.json +3 -3
- package/src/ai/AiAssistantLauncher.tsx +26 -6
- package/src/ai/AiChatSessions.tsx +9 -1
- package/src/ai/__tests__/AiAssistantLauncher.test.tsx +67 -0
- package/src/ai/__tests__/AiChatSessions.test.tsx +77 -0
- package/src/ai/__tests__/useAiAssistantAvailable.test.tsx +96 -0
- package/src/ai/index.ts +5 -0
- package/src/ai/useAiAssistantAvailable.ts +50 -0
- package/src/backend/detail/__tests__/addressFormat.taxId.test.ts +33 -0
- package/src/backend/detail/addressFormat.tsx +59 -1
- package/src/backend/inputs/ComboboxInput.tsx +112 -79
- package/src/backend/inputs/__tests__/ComboboxInput.dialog.test.tsx +134 -0
- package/src/backend/inputs/__tests__/ComboboxInput.test.tsx +95 -0
- package/src/primitives/__tests__/zindex-overlay.test.tsx +4 -1
|
@@ -5,6 +5,7 @@ import { X } from 'lucide-react'
|
|
|
5
5
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
6
|
import { Button } from '../../primitives/button'
|
|
7
7
|
import { IconButton } from '../../primitives/icon-button'
|
|
8
|
+
import { Popover, PopoverAnchor, PopoverContent } from '../../primitives/popover'
|
|
8
9
|
|
|
9
10
|
export type ComboboxOption = {
|
|
10
11
|
value: string
|
|
@@ -412,82 +413,114 @@ export function ComboboxInput({
|
|
|
412
413
|
&& (loading || filteredSuggestions.length > 0 || (touched && input.trim().length > 0))
|
|
413
414
|
|
|
414
415
|
return (
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
416
|
+
// The suggestion list goes through the DS Popover so it is portaled out of any
|
|
417
|
+
// scrolling ancestor. Rendered in place it was clipped by a Dialog's
|
|
418
|
+
// `overflow-y-auto`, where z-index cannot help. `open` stays fully controlled
|
|
419
|
+
// (no `onOpenChange`) so the blur timer, Escape and selection keep owning
|
|
420
|
+
// dismissal exactly as before.
|
|
421
|
+
<Popover open={listboxVisible}>
|
|
422
|
+
<PopoverAnchor asChild>
|
|
423
|
+
<div className="relative w-full">
|
|
424
|
+
{/* Use raw <input> here instead of the DS Input primitive: ComboboxInput's
|
|
425
|
+
focus / suggestions-popup interplay relies on the trigger being a plain
|
|
426
|
+
input element. The DS wrapper introduces a <div> that desyncs autocomplete
|
|
427
|
+
on this specific surface. Keeps the rest of the form on Input primitive. */}
|
|
428
|
+
<input
|
|
429
|
+
ref={inputRef}
|
|
430
|
+
type="text"
|
|
431
|
+
className={[
|
|
432
|
+
'w-full h-9 rounded-md border border-input bg-background px-3 text-sm shadow-xs transition-colors outline-none placeholder:text-muted-foreground focus-visible:shadow-focus focus-visible:border-foreground disabled:bg-bg-disabled disabled:border-border-disabled disabled:text-muted-foreground disabled:cursor-not-allowed',
|
|
433
|
+
showClearButton ? 'pr-9' : '',
|
|
434
|
+
]
|
|
435
|
+
.filter(Boolean)
|
|
436
|
+
.join(' ')}
|
|
437
|
+
value={input}
|
|
438
|
+
placeholder={resolvedPlaceholder}
|
|
439
|
+
autoFocus={autoFocus}
|
|
440
|
+
data-crud-focus-target=""
|
|
441
|
+
disabled={disabled}
|
|
442
|
+
role="combobox"
|
|
443
|
+
aria-expanded={listboxVisible}
|
|
444
|
+
aria-controls={listboxVisible && !loading && filteredSuggestions.length > 0 ? listboxId : undefined}
|
|
445
|
+
// The listbox is portaled out of the input's subtree, so `aria-owns` is what
|
|
446
|
+
// makes it a logical descendant and keeps `aria-activedescendant` below valid.
|
|
447
|
+
aria-owns={listboxVisible && !loading && filteredSuggestions.length > 0 ? listboxId : undefined}
|
|
448
|
+
aria-autocomplete="list"
|
|
449
|
+
aria-activedescendant={listboxVisible && selectedIndex >= 0 ? optionDomId(selectedIndex) : undefined}
|
|
450
|
+
onFocus={() => {
|
|
451
|
+
setTouched(true)
|
|
452
|
+
if (suppressOpenOnFocusRef.current) {
|
|
453
|
+
suppressOpenOnFocusRef.current = false
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
resetBlurCloseState()
|
|
457
|
+
if (loadSuggestions && availableOptions.length === 0) {
|
|
458
|
+
setLoading(true)
|
|
459
|
+
}
|
|
460
|
+
setShowSuggestions(true)
|
|
461
|
+
}}
|
|
462
|
+
onChange={(event) => {
|
|
463
|
+
setTouched(true)
|
|
464
|
+
userTypedRef.current = true
|
|
465
|
+
setInput(event.target.value)
|
|
466
|
+
setShowSuggestions(true)
|
|
467
|
+
setSelectedIndex(-1)
|
|
468
|
+
}}
|
|
469
|
+
onKeyDown={handleKeyDown}
|
|
470
|
+
onBlur={() => {
|
|
471
|
+
// Delay closing so clicks on the popup can resolve first. If async
|
|
472
|
+
// suggestions are still loading, keep the dropdown open instead of
|
|
473
|
+
// closing before the first payload arrives.
|
|
474
|
+
userTypedRef.current = false
|
|
475
|
+
blurClosePendingRef.current = true
|
|
476
|
+
clearBlurCloseTimer()
|
|
477
|
+
if (loadingRef.current) {
|
|
478
|
+
blurCloseTimerRef.current = window.setTimeout(closeAfterBlur, blurCloseMaxDelayMs)
|
|
479
|
+
return
|
|
480
|
+
}
|
|
481
|
+
blurCloseTimerRef.current = window.setTimeout(attemptBlurClose, blurCloseDelayMs)
|
|
482
|
+
}}
|
|
483
|
+
/>
|
|
484
|
+
|
|
485
|
+
{showClearButton ? (
|
|
486
|
+
<IconButton
|
|
487
|
+
type="button"
|
|
488
|
+
variant="ghost"
|
|
489
|
+
size="xs"
|
|
490
|
+
aria-label={resolvedClearLabel}
|
|
491
|
+
className="absolute right-1 top-1/2 -translate-y-1/2"
|
|
492
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
493
|
+
onClick={handleClear}
|
|
494
|
+
>
|
|
495
|
+
<X className="size-3" />
|
|
496
|
+
</IconButton>
|
|
497
|
+
) : null}
|
|
498
|
+
</div>
|
|
499
|
+
</PopoverAnchor>
|
|
500
|
+
|
|
501
|
+
{/* Unmount the content outright rather than leaning on Radix's exit animation:
|
|
502
|
+
a closing-but-still-mounted layer keeps swallowing Escape, which would
|
|
503
|
+
strand the popup's host (see AdvancedFilterPanel for that failure mode). */}
|
|
504
|
+
{listboxVisible ? (
|
|
505
|
+
<PopoverContent
|
|
506
|
+
// Radix hardcodes `role="dialog"` on popover content. This popup is a
|
|
507
|
+
// positioning shell around the listbox below, and a second dialog node
|
|
508
|
+
// would both misdescribe it and break `getByRole('dialog')` for anything
|
|
509
|
+
// that queries while suggestions happen to be open.
|
|
510
|
+
role="presentation"
|
|
511
|
+
// Focus must stay in the input: the blur-close timer and
|
|
512
|
+
// aria-activedescendant model depend on it, and Radix would otherwise
|
|
513
|
+
// move focus into the list on open and back to the trigger on close.
|
|
514
|
+
onOpenAutoFocus={(event) => event.preventDefault()}
|
|
515
|
+
onCloseAutoFocus={(event) => event.preventDefault()}
|
|
516
|
+
// A modal Dialog locks scrolling through `react-remove-scroll`, which
|
|
517
|
+
// cancels document-level `wheel` and `touchmove` outside its own content
|
|
518
|
+
// node. The portaled list is not part of that exemption, so without these
|
|
519
|
+
// a long list inside a dialog cannot be scrolled by wheel or by touch.
|
|
520
|
+
// `overscroll-contain` stops the scroll chaining to the page at the ends.
|
|
521
|
+
onWheel={(event) => event.stopPropagation()}
|
|
522
|
+
onTouchMove={(event) => event.stopPropagation()}
|
|
523
|
+
className="w-[var(--radix-popover-trigger-width)] min-w-0 max-h-48 sm:max-h-60 overflow-auto overscroll-contain border-input p-2"
|
|
491
524
|
>
|
|
492
525
|
{loading && touched ? (
|
|
493
526
|
<div className="px-2 py-1.5 text-xs text-muted-foreground" role="status">{loadingLabel}</div>
|
|
@@ -525,8 +558,8 @@ export function ComboboxInput({
|
|
|
525
558
|
))}
|
|
526
559
|
</div>
|
|
527
560
|
)}
|
|
528
|
-
</
|
|
529
|
-
)}
|
|
530
|
-
</
|
|
561
|
+
</PopoverContent>
|
|
562
|
+
) : null}
|
|
563
|
+
</Popover>
|
|
531
564
|
)
|
|
532
565
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
|
|
3
|
+
jest.mock('@open-mercato/shared/lib/i18n/context', () => ({
|
|
4
|
+
useT: () => (_key: string, fallback: string) => fallback,
|
|
5
|
+
}))
|
|
6
|
+
|
|
7
|
+
import * as React from 'react'
|
|
8
|
+
import { act, fireEvent, render, screen } from '@testing-library/react'
|
|
9
|
+
import { Dialog, DialogContent, DialogTitle } from '../../../primitives/dialog'
|
|
10
|
+
import { ComboboxInput } from '../ComboboxInput'
|
|
11
|
+
|
|
12
|
+
function DialogHarness() {
|
|
13
|
+
const [open, setOpen] = React.useState(true)
|
|
14
|
+
const [value, setValue] = React.useState('')
|
|
15
|
+
return (
|
|
16
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
17
|
+
<DialogContent>
|
|
18
|
+
<DialogTitle>Move inventory</DialogTitle>
|
|
19
|
+
<ComboboxInput
|
|
20
|
+
value={value}
|
|
21
|
+
onChange={setValue}
|
|
22
|
+
suggestions={[
|
|
23
|
+
{ value: 'red', label: 'Red' },
|
|
24
|
+
{ value: 'green', label: 'Green' },
|
|
25
|
+
]}
|
|
26
|
+
/>
|
|
27
|
+
<output data-testid="value">{value}</output>
|
|
28
|
+
</DialogContent>
|
|
29
|
+
</Dialog>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function openSuggestions() {
|
|
34
|
+
const input = screen.getByRole('combobox')
|
|
35
|
+
fireEvent.focus(input)
|
|
36
|
+
fireEvent.change(input, { target: { value: 'gre' } })
|
|
37
|
+
return input
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('ComboboxInput inside a Dialog', () => {
|
|
41
|
+
it('renders the suggestion list outside the dialog scroll container', () => {
|
|
42
|
+
render(<DialogHarness />)
|
|
43
|
+
openSuggestions()
|
|
44
|
+
|
|
45
|
+
const dialogContent = document.querySelector('[data-dialog-content]')
|
|
46
|
+
const listbox = screen.getByRole('listbox')
|
|
47
|
+
|
|
48
|
+
expect(dialogContent).not.toBeNull()
|
|
49
|
+
expect(dialogContent!.contains(listbox)).toBe(false)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// `react-remove-scroll` (the dialog's scroll lock) listens on the document for
|
|
53
|
+
// both `wheel` and `touchmove` and cancels either one when it is raised outside
|
|
54
|
+
// the dialog content, which would freeze the portaled list.
|
|
55
|
+
it.each(['wheel', 'touchMove'] as const)('keeps %s events off the dialog scroll lock so a long list stays scrollable', (eventName) => {
|
|
56
|
+
render(<DialogHarness />)
|
|
57
|
+
openSuggestions()
|
|
58
|
+
|
|
59
|
+
const domEventName = eventName === 'wheel' ? 'wheel' : 'touchmove'
|
|
60
|
+
// The scroll lock reads coordinates off the event, so give it usable ones.
|
|
61
|
+
const init = eventName === 'wheel'
|
|
62
|
+
? { deltaY: 120 }
|
|
63
|
+
: { touches: [{ clientX: 0, clientY: 0 }], changedTouches: [{ clientX: 0, clientY: 0 }] }
|
|
64
|
+
const onDocumentEvent = jest.fn()
|
|
65
|
+
document.addEventListener(domEventName, onDocumentEvent)
|
|
66
|
+
try {
|
|
67
|
+
fireEvent[eventName](screen.getByRole('listbox'), init)
|
|
68
|
+
expect(onDocumentEvent).not.toHaveBeenCalled()
|
|
69
|
+
|
|
70
|
+
// control: the same event anywhere else still reaches the document listener
|
|
71
|
+
fireEvent[eventName](screen.getByRole('combobox'), init)
|
|
72
|
+
expect(onDocumentEvent).toHaveBeenCalledTimes(1)
|
|
73
|
+
} finally {
|
|
74
|
+
document.removeEventListener(domEventName, onDocumentEvent)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('does not add a second dialog node while the suggestions are open', () => {
|
|
79
|
+
render(<DialogHarness />)
|
|
80
|
+
openSuggestions()
|
|
81
|
+
|
|
82
|
+
// Radix hardcodes role="dialog" on popover content; ~35 test files resolve
|
|
83
|
+
// `getByRole('dialog')`, which is strict about multiple matches.
|
|
84
|
+
expect(screen.getByRole('listbox')).toBeInTheDocument()
|
|
85
|
+
expect(document.querySelectorAll('[role="dialog"]')).toHaveLength(1)
|
|
86
|
+
expect(screen.getByRole('dialog')).toHaveAttribute('data-dialog-content')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('keeps the dialog open when an option is picked', async () => {
|
|
90
|
+
render(<DialogHarness />)
|
|
91
|
+
openSuggestions()
|
|
92
|
+
|
|
93
|
+
// Radix registers its outside-interaction listener on a macrotask, so let it
|
|
94
|
+
// land before simulating the pointer sequence a real click produces.
|
|
95
|
+
await act(async () => {
|
|
96
|
+
await Promise.resolve()
|
|
97
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const option = screen.getByRole('option', { name: /green/i })
|
|
101
|
+
fireEvent.pointerDown(option, { bubbles: true })
|
|
102
|
+
fireEvent.mouseDown(option, { bubbles: true })
|
|
103
|
+
fireEvent.click(option)
|
|
104
|
+
|
|
105
|
+
expect(screen.getByTestId('value')).toHaveTextContent('green')
|
|
106
|
+
expect(document.querySelector('[data-dialog-content]')).not.toBeNull()
|
|
107
|
+
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('still dismisses the dialog on a genuinely outside pointer down', async () => {
|
|
111
|
+
render(<DialogHarness />)
|
|
112
|
+
openSuggestions()
|
|
113
|
+
|
|
114
|
+
await act(async () => {
|
|
115
|
+
await Promise.resolve()
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// Control for the case above: the suppression must be scoped to the portaled
|
|
120
|
+
// list, not a blanket block on the dialog's outside-interaction handling.
|
|
121
|
+
const outside = document.createElement('button')
|
|
122
|
+
document.body.appendChild(outside)
|
|
123
|
+
try {
|
|
124
|
+
await act(async () => {
|
|
125
|
+
fireEvent.pointerDown(outside, { bubbles: true })
|
|
126
|
+
fireEvent.mouseDown(outside, { bubbles: true })
|
|
127
|
+
fireEvent.click(outside)
|
|
128
|
+
})
|
|
129
|
+
expect(document.querySelector('[data-dialog-content]')).toBeNull()
|
|
130
|
+
} finally {
|
|
131
|
+
outside.remove()
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
})
|
|
@@ -557,5 +557,100 @@ describe('ComboboxInput accessibility', () => {
|
|
|
557
557
|
const listbox = screen.getByRole('listbox')
|
|
558
558
|
expect(within(listbox).getAllByRole('option')).toHaveLength(2)
|
|
559
559
|
expect(input).toHaveAttribute('aria-controls', listbox.id)
|
|
560
|
+
// The listbox is portaled out of the input's subtree, so aria-activedescendant
|
|
561
|
+
// is only valid while aria-owns makes it a logical descendant.
|
|
562
|
+
expect(input).toHaveAttribute('aria-owns', listbox.id)
|
|
563
|
+
expect(listbox.contains(input)).toBe(false)
|
|
564
|
+
})
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
describe('ComboboxInput suggestion popup placement', () => {
|
|
568
|
+
it('renders the suggestion list outside the field wrapper so an overflow ancestor cannot clip it', () => {
|
|
569
|
+
const { container } = render(<Harness />)
|
|
570
|
+
const input = getInput(container)
|
|
571
|
+
|
|
572
|
+
fireEvent.focus(input)
|
|
573
|
+
fireEvent.change(input, { target: { value: 're' } })
|
|
574
|
+
|
|
575
|
+
const listbox = screen.getByRole('listbox')
|
|
576
|
+
expect(container.contains(listbox)).toBe(false)
|
|
577
|
+
expect(document.body.contains(listbox)).toBe(true)
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
it('keeps focus on the input while the popup is open', () => {
|
|
581
|
+
const { container } = render(<Harness />)
|
|
582
|
+
const input = getInput(container)
|
|
583
|
+
|
|
584
|
+
act(() => {
|
|
585
|
+
input.focus()
|
|
586
|
+
fireEvent.focus(input)
|
|
587
|
+
})
|
|
588
|
+
fireEvent.change(input, { target: { value: 're' } })
|
|
589
|
+
|
|
590
|
+
expect(screen.getByRole('listbox')).toBeInTheDocument()
|
|
591
|
+
expect(document.activeElement).toBe(input)
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
it('selects a portaled option by click', () => {
|
|
595
|
+
render(<Harness />)
|
|
596
|
+
const input = screen.getByRole('combobox')
|
|
597
|
+
|
|
598
|
+
fireEvent.focus(input)
|
|
599
|
+
fireEvent.change(input, { target: { value: 'gre' } })
|
|
600
|
+
fireEvent.click(screen.getByRole('option', { name: /green/i }))
|
|
601
|
+
|
|
602
|
+
expect(screen.getByTestId('value')).toHaveTextContent('green')
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
it('selects a portaled option by keyboard', () => {
|
|
606
|
+
render(<Harness />)
|
|
607
|
+
const input = screen.getByRole('combobox')
|
|
608
|
+
|
|
609
|
+
fireEvent.focus(input)
|
|
610
|
+
fireEvent.change(input, { target: { value: 'gre' } })
|
|
611
|
+
fireEvent.keyDown(input, { key: 'ArrowDown' })
|
|
612
|
+
fireEvent.keyDown(input, { key: 'Enter' })
|
|
613
|
+
|
|
614
|
+
expect(screen.getByTestId('value')).toHaveTextContent('green')
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
it('closes the popup on Escape without letting the key reach an enclosing surface', () => {
|
|
618
|
+
const onSurfaceKeyDown = jest.fn()
|
|
619
|
+
render(
|
|
620
|
+
<div onKeyDown={onSurfaceKeyDown}>
|
|
621
|
+
<Harness />
|
|
622
|
+
</div>,
|
|
623
|
+
)
|
|
624
|
+
const input = screen.getByRole('combobox')
|
|
625
|
+
|
|
626
|
+
fireEvent.focus(input)
|
|
627
|
+
fireEvent.change(input, { target: { value: 're' } })
|
|
628
|
+
expect(screen.getByRole('listbox')).toBeInTheDocument()
|
|
629
|
+
|
|
630
|
+
fireEvent.keyDown(input, { key: 'Escape' })
|
|
631
|
+
|
|
632
|
+
expect(screen.queryByRole('listbox')).toBeNull()
|
|
633
|
+
expect(onSurfaceKeyDown).not.toHaveBeenCalled()
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
it('removes the popup from the document once it closes', () => {
|
|
637
|
+
jest.useFakeTimers()
|
|
638
|
+
try {
|
|
639
|
+
render(<Harness />)
|
|
640
|
+
const input = screen.getByRole('combobox')
|
|
641
|
+
|
|
642
|
+
fireEvent.focus(input)
|
|
643
|
+
fireEvent.change(input, { target: { value: 're' } })
|
|
644
|
+
expect(document.body.querySelector('[role="listbox"]')).not.toBeNull()
|
|
645
|
+
|
|
646
|
+
blurAndFlush(input)
|
|
647
|
+
|
|
648
|
+
expect(document.body.querySelector('[role="listbox"]')).toBeNull()
|
|
649
|
+
} finally {
|
|
650
|
+
act(() => {
|
|
651
|
+
jest.runOnlyPendingTimers()
|
|
652
|
+
})
|
|
653
|
+
jest.useRealTimers()
|
|
654
|
+
}
|
|
560
655
|
})
|
|
561
656
|
})
|
|
@@ -87,8 +87,11 @@ describe('Issue #1836: portaled overlay primitives sit above modals (z-popover >
|
|
|
87
87
|
fireEvent.change(input!, { target: { value: 'a' } })
|
|
88
88
|
})
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
// The list is portaled to document.body (via the DS Popover) so a Dialog's
|
|
91
|
+
// overflow cannot clip it -- so look it up on the document, not on `container`.
|
|
92
|
+
const suggestions = document.body.querySelector('[class*="z-popover"]')
|
|
91
93
|
expect(suggestions).not.toBeNull()
|
|
94
|
+
expect(container.contains(suggestions)).toBe(false)
|
|
92
95
|
const token = findZIndexToken((suggestions as HTMLElement).className)
|
|
93
96
|
expect(token).toBe('z-popover')
|
|
94
97
|
expect(Z_INDEX_BY_TOKEN[token!]).toBeGreaterThan(Z_INDEX_BY_TOKEN['z-modal'])
|