@kaizen/components 3.3.10 → 3.3.12
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/dist/cjs/src/MultiSelect/subcomponents/Popover/Popover.cjs +15 -2
- package/dist/esm/src/MultiSelect/subcomponents/Popover/Popover.mjs +15 -2
- package/package.json +7 -7
- package/src/MultiSelect/subcomponents/Popover/Popover.spec.tsx +29 -0
- package/src/MultiSelect/subcomponents/Popover/Popover.tsx +13 -0
- package/src/SingleSelect/_docs/SingleSelect.stories.tsx +58 -0
|
@@ -14,7 +14,20 @@ function _interopDefault(e) {
|
|
|
14
14
|
}
|
|
15
15
|
var React__default = /*#__PURE__*/_interopDefault(React);
|
|
16
16
|
var classnames__default = /*#__PURE__*/_interopDefault(classnames);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* react-aria stamps this attribute on the overlays it owns. Focus-scope guards
|
|
20
|
+
* (`isElementInChildOfActiveScope`) and `useInteractOutside` treat a marked element — and its
|
|
21
|
+
* subtree — as part of the top layer, so the outer overlay is not dismissed when focus moves
|
|
22
|
+
* into the portal.
|
|
23
|
+
*
|
|
24
|
+
* The select popover is a plain `createPortal` rather than a react-aria `Overlay`, so nothing
|
|
25
|
+
* marks it for us. Without this, opening it inside a react-aria overlay (eg. a `MenuPopover`)
|
|
26
|
+
* closes that overlay as soon as focus enters the portal. See KZN-4210.
|
|
27
|
+
*/
|
|
28
|
+
var TOP_LAYER_ATTRIBUTE = 'data-react-aria-top-layer';
|
|
17
29
|
var Popover = function (_a) {
|
|
30
|
+
var _b;
|
|
18
31
|
var children = _a.children,
|
|
19
32
|
refs = _a.refs,
|
|
20
33
|
floatingOptions = _a.floatingOptions,
|
|
@@ -45,11 +58,11 @@ var Popover = function (_a) {
|
|
|
45
58
|
return ReactDOM.createPortal(React__default.default.createElement(reactFocusOn.FocusOn, tslib.__assign({
|
|
46
59
|
enabled: false,
|
|
47
60
|
scrollLock: false
|
|
48
|
-
}, focusOnProps), React__default.default.createElement("div", tslib.__assign({
|
|
61
|
+
}, focusOnProps), React__default.default.createElement("div", tslib.__assign((_b = {
|
|
49
62
|
ref: refs.setFloating,
|
|
50
63
|
style: floatingStyles,
|
|
51
64
|
className: classnames__default.default(Popover_module.popover, classNameOverride)
|
|
52
|
-
}, restProps), children)), portalContainer !== null && portalContainer !== void 0 ? portalContainer : document.body);
|
|
65
|
+
}, _b[TOP_LAYER_ATTRIBUTE] = 'true', _b), restProps), children)), portalContainer !== null && portalContainer !== void 0 ? portalContainer : document.body);
|
|
53
66
|
};
|
|
54
67
|
Popover.displayName = 'Popover';
|
|
55
68
|
exports.Popover = Popover;
|
|
@@ -5,7 +5,20 @@ import { useFloating, autoUpdate, offset, flip, size } from '@floating-ui/react-
|
|
|
5
5
|
import classnames from 'classnames';
|
|
6
6
|
import { FocusOn } from 'react-focus-on';
|
|
7
7
|
import modules_ef8f756f from './Popover.module.scss.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* react-aria stamps this attribute on the overlays it owns. Focus-scope guards
|
|
11
|
+
* (`isElementInChildOfActiveScope`) and `useInteractOutside` treat a marked element — and its
|
|
12
|
+
* subtree — as part of the top layer, so the outer overlay is not dismissed when focus moves
|
|
13
|
+
* into the portal.
|
|
14
|
+
*
|
|
15
|
+
* The select popover is a plain `createPortal` rather than a react-aria `Overlay`, so nothing
|
|
16
|
+
* marks it for us. Without this, opening it inside a react-aria overlay (eg. a `MenuPopover`)
|
|
17
|
+
* closes that overlay as soon as focus enters the portal. See KZN-4210.
|
|
18
|
+
*/
|
|
19
|
+
var TOP_LAYER_ATTRIBUTE = 'data-react-aria-top-layer';
|
|
8
20
|
var Popover = function (_a) {
|
|
21
|
+
var _b;
|
|
9
22
|
var children = _a.children,
|
|
10
23
|
refs = _a.refs,
|
|
11
24
|
floatingOptions = _a.floatingOptions,
|
|
@@ -36,11 +49,11 @@ var Popover = function (_a) {
|
|
|
36
49
|
return /*#__PURE__*/createPortal(/*#__PURE__*/React__default.createElement(FocusOn, __assign({
|
|
37
50
|
enabled: false,
|
|
38
51
|
scrollLock: false
|
|
39
|
-
}, focusOnProps), /*#__PURE__*/React__default.createElement("div", __assign({
|
|
52
|
+
}, focusOnProps), /*#__PURE__*/React__default.createElement("div", __assign((_b = {
|
|
40
53
|
ref: refs.setFloating,
|
|
41
54
|
style: floatingStyles,
|
|
42
55
|
className: classnames(modules_ef8f756f.popover, classNameOverride)
|
|
43
|
-
}, restProps), children)), portalContainer !== null && portalContainer !== void 0 ? portalContainer : document.body);
|
|
56
|
+
}, _b[TOP_LAYER_ATTRIBUTE] = 'true', _b), restProps), children)), portalContainer !== null && portalContainer !== void 0 ? portalContainer : document.body);
|
|
44
57
|
};
|
|
45
58
|
Popover.displayName = 'Popover';
|
|
46
59
|
export { Popover };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaizen/components",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.12",
|
|
4
4
|
"description": "Kaizen component library",
|
|
5
5
|
"author": "Geoffrey Chong <geoff.chong@cultureamp.com>",
|
|
6
6
|
"homepage": "https://cultureamp.design",
|
|
@@ -146,24 +146,24 @@
|
|
|
146
146
|
"jest-axe": "^10.0.0",
|
|
147
147
|
"lodash.isempty": "^4.4.0",
|
|
148
148
|
"normalize.css": "^8.0.1",
|
|
149
|
-
"postcss": "^8.5.
|
|
149
|
+
"postcss": "^8.5.26",
|
|
150
150
|
"postcss-cli": "^11.0.1",
|
|
151
151
|
"postcss-import": "^16.1.1",
|
|
152
152
|
"postcss-preset-env": "^11.3.2",
|
|
153
153
|
"postcss-scss": "^4.0.9",
|
|
154
|
-
"query-string": "^9.
|
|
154
|
+
"query-string": "^9.5.0",
|
|
155
155
|
"react": "^19.2.8",
|
|
156
156
|
"react-dom": "^19.2.8",
|
|
157
157
|
"react-highlight": "^0.15.0",
|
|
158
|
-
"react-intl": "^10.1.
|
|
159
|
-
"rollup": "^4.62.
|
|
158
|
+
"react-intl": "^10.1.20",
|
|
159
|
+
"rollup": "^4.62.4",
|
|
160
160
|
"sass": "1.79.6",
|
|
161
161
|
"serialize-query-params": "^2.0.4",
|
|
162
162
|
"svgo": "^4.0.2",
|
|
163
163
|
"ts-patch": "^3.3.0",
|
|
164
164
|
"tslib": "^2.8.1",
|
|
165
|
-
"tsx": "^4.23.
|
|
166
|
-
"@kaizen/design-tokens": "11.0.
|
|
165
|
+
"tsx": "^4.23.11",
|
|
166
|
+
"@kaizen/design-tokens": "11.0.17"
|
|
167
167
|
},
|
|
168
168
|
"devDependenciesComments": {
|
|
169
169
|
"sass": "Prevent deprecation warnings introduced in 1.80 as we plan to move away from sass",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { ariaHideOutside } from '@react-aria/overlays'
|
|
2
3
|
import { render, waitFor } from '@testing-library/react'
|
|
3
4
|
import { Popover, useFloating, type PopoverProps } from './'
|
|
4
5
|
|
|
@@ -51,5 +52,33 @@ describe('<Popover />', () => {
|
|
|
51
52
|
expect(getByTestId('portal-container')).not.toHaveTextContent('Hello')
|
|
52
53
|
})
|
|
53
54
|
})
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* react-aria uses `data-react-aria-top-layer` to exempt an element from dismissal and from
|
|
58
|
+
* `ariaHideOutside`. This popover is a plain portal, so it marks itself — otherwise opening
|
|
59
|
+
* it inside a react-aria overlay closes that overlay as soon as focus enters the portal.
|
|
60
|
+
*/
|
|
61
|
+
describe('react-aria top layer', () => {
|
|
62
|
+
it('marks the popover itself as part of the top layer', () => {
|
|
63
|
+
const { getByText } = render(<PopoverWrapper />)
|
|
64
|
+
|
|
65
|
+
expect(getByText('Hello')).toHaveAttribute('data-react-aria-top-layer', 'true')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('is not hidden when another overlay isolates the rest of the page', () => {
|
|
69
|
+
const otherOverlay = document.createElement('div')
|
|
70
|
+
document.body.appendChild(otherOverlay)
|
|
71
|
+
|
|
72
|
+
const { getByText } = render(<PopoverWrapper />)
|
|
73
|
+
const popover = getByText('Hello')
|
|
74
|
+
|
|
75
|
+
const revertAriaHide = ariaHideOutside([otherOverlay])
|
|
76
|
+
|
|
77
|
+
expect(popover.closest('[aria-hidden="true"]')).toBeNull()
|
|
78
|
+
|
|
79
|
+
revertAriaHide()
|
|
80
|
+
otherOverlay.remove()
|
|
81
|
+
})
|
|
82
|
+
})
|
|
54
83
|
})
|
|
55
84
|
})
|
|
@@ -16,6 +16,18 @@ import { type ReactFocusOnProps } from 'react-focus-on/dist/es5/types'
|
|
|
16
16
|
import { type OverrideClassName } from '~components/types/OverrideClassName'
|
|
17
17
|
import styles from './Popover.module.scss'
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* react-aria stamps this attribute on the overlays it owns. Focus-scope guards
|
|
21
|
+
* (`isElementInChildOfActiveScope`) and `useInteractOutside` treat a marked element — and its
|
|
22
|
+
* subtree — as part of the top layer, so the outer overlay is not dismissed when focus moves
|
|
23
|
+
* into the portal.
|
|
24
|
+
*
|
|
25
|
+
* The select popover is a plain `createPortal` rather than a react-aria `Overlay`, so nothing
|
|
26
|
+
* marks it for us. Without this, opening it inside a react-aria overlay (eg. a `MenuPopover`)
|
|
27
|
+
* closes that overlay as soon as focus enters the portal. See KZN-4210.
|
|
28
|
+
*/
|
|
29
|
+
const TOP_LAYER_ATTRIBUTE = 'data-react-aria-top-layer'
|
|
30
|
+
|
|
19
31
|
export type PopoverProps<RT extends ReferenceType = ReferenceType> = {
|
|
20
32
|
children: React.ReactNode
|
|
21
33
|
refs: UseFloatingReturn<RT>['refs']
|
|
@@ -65,6 +77,7 @@ export const Popover = <RT extends ReferenceType>({
|
|
|
65
77
|
ref={refs.setFloating}
|
|
66
78
|
style={floatingStyles}
|
|
67
79
|
className={classnames(styles.popover, classNameOverride)}
|
|
80
|
+
{...{ [TOP_LAYER_ATTRIBUTE]: 'true' }}
|
|
68
81
|
{...restProps}
|
|
69
82
|
>
|
|
70
83
|
{children}
|
|
@@ -3,6 +3,7 @@ import { type Meta, type StoryObj } from '@storybook/react'
|
|
|
3
3
|
import { expect, userEvent, waitFor, within } from '@storybook/test'
|
|
4
4
|
import { Button } from '~components/Button'
|
|
5
5
|
import { FieldMessage } from '~components/FieldMessage'
|
|
6
|
+
import { Menu, MenuItem, MenuPopover, MenuTrigger } from '~components/Menu'
|
|
6
7
|
import { ContextModal } from '~components/Modal'
|
|
7
8
|
import { RadioField, RadioGroup } from '~components/Radio'
|
|
8
9
|
import { SingleSelect } from '../SingleSelect'
|
|
@@ -339,3 +340,60 @@ export const NativeFormValidationWithSelectedVal: Story = {
|
|
|
339
340
|
})
|
|
340
341
|
},
|
|
341
342
|
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* The select popover is portalled out of the menu popover, so react-aria's dismissal guards and
|
|
346
|
+
* its `inert` isolation both have to be told to leave it alone. Without that the menu closes as
|
|
347
|
+
* soon as the select opens, or the listbox renders but cannot be focused or clicked.
|
|
348
|
+
*/
|
|
349
|
+
export const InsideMenuPopover: Story = {
|
|
350
|
+
render: (args) => (
|
|
351
|
+
<MenuTrigger>
|
|
352
|
+
<Button>Open menu</Button>
|
|
353
|
+
<MenuPopover>
|
|
354
|
+
<Menu aria-label="Actions">
|
|
355
|
+
<MenuItem>An action</MenuItem>
|
|
356
|
+
</Menu>
|
|
357
|
+
<div className="p-16">
|
|
358
|
+
<SingleSelect {...args} label="Coffee" />
|
|
359
|
+
</div>
|
|
360
|
+
</MenuPopover>
|
|
361
|
+
</MenuTrigger>
|
|
362
|
+
),
|
|
363
|
+
parameters: {
|
|
364
|
+
// Interaction coverage only — the open popovers make for an unstable visual baseline.
|
|
365
|
+
chromatic: { disable: true },
|
|
366
|
+
docs: { source: { type: 'code' } },
|
|
367
|
+
},
|
|
368
|
+
play: async ({ canvasElement, step }) => {
|
|
369
|
+
// Both popovers are portalled out of the story root.
|
|
370
|
+
const canvas = within(canvasElement.parentElement!)
|
|
371
|
+
|
|
372
|
+
await userEvent.click(canvas.getByRole('button', { name: 'Open menu' }))
|
|
373
|
+
|
|
374
|
+
const selectToggle = await canvas.findByRole('combobox', { name: /Coffee/ })
|
|
375
|
+
|
|
376
|
+
await step('Opening the select does not dismiss the menu popover', async () => {
|
|
377
|
+
await userEvent.click(selectToggle)
|
|
378
|
+
|
|
379
|
+
await waitFor(() => {
|
|
380
|
+
expect(canvas.getByRole('menuitem', { name: 'An action' })).toBeVisible()
|
|
381
|
+
})
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
await step('Options are interactive, not inert', async () => {
|
|
385
|
+
const option = await canvas.findByRole('option', { name: 'Latte' })
|
|
386
|
+
|
|
387
|
+
// The `inert` failure mode leaves the listbox visible but non-interactive, so assert the
|
|
388
|
+
// options are reachable rather than only that they rendered.
|
|
389
|
+
expect(option.closest('[inert]')).toBeNull()
|
|
390
|
+
|
|
391
|
+
await userEvent.click(option)
|
|
392
|
+
|
|
393
|
+
await waitFor(() => {
|
|
394
|
+
expect(selectToggle).toHaveTextContent('Latte')
|
|
395
|
+
expect(canvas.getByRole('menuitem', { name: 'An action' })).toBeVisible()
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
},
|
|
399
|
+
}
|