@lumx/react 3.0.5 → 3.0.6-alpha.0
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/_internal/types.d.ts +18 -2
- package/index.d.ts +48 -15
- package/index.js +562 -492
- package/index.js.map +1 -1
- package/package.json +4 -4
- package/src/components/autocomplete/Autocomplete.tsx +7 -4
- package/src/components/badge/Badge.tsx +2 -2
- package/src/components/button/Button.stories.tsx +27 -1
- package/src/components/button/Button.tsx +3 -3
- package/src/components/button/ButtonRoot.tsx +2 -2
- package/src/components/chip/Chip.tsx +2 -2
- package/src/components/dropdown/Dropdown.tsx +5 -2
- package/src/components/icon/Icon.tsx +2 -2
- package/src/components/index.ts +0 -1
- package/src/components/link/Link.tsx +2 -2
- package/src/components/popover/Popover.stories.tsx +70 -0
- package/src/components/popover/Popover.tsx +34 -9
- package/src/components/popover-dialog/PopoverDialog.stories.tsx +75 -0
- package/src/components/popover-dialog/PopoverDialog.test.tsx +65 -0
- package/src/components/popover-dialog/PopoverDialog.tsx +65 -0
- package/src/components/popover-dialog/index.tsx +1 -0
- package/src/index.ts +1 -0
- package/src/stories/generated/PopoverDialog/Demos.stories.tsx +6 -0
- package/src/utils/type.ts +20 -0
package/package.json
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@juggle/resize-observer": "^3.2.0",
|
|
10
|
-
"@lumx/core": "^3.0.
|
|
11
|
-
"@lumx/icons": "^3.0.
|
|
10
|
+
"@lumx/core": "^3.0.6-alpha.0",
|
|
11
|
+
"@lumx/icons": "^3.0.6-alpha.0",
|
|
12
12
|
"@popperjs/core": "^2.5.4",
|
|
13
13
|
"body-scroll-lock": "^3.1.5",
|
|
14
14
|
"classnames": "^2.2.6",
|
|
@@ -114,6 +114,6 @@
|
|
|
114
114
|
"build:storybook": "cd storybook && ./build"
|
|
115
115
|
},
|
|
116
116
|
"sideEffects": false,
|
|
117
|
-
"version": "3.0.
|
|
118
|
-
"gitHead": "
|
|
117
|
+
"version": "3.0.6-alpha.0",
|
|
118
|
+
"gitHead": "22ae75c6b11ab36e3f9e194af19a5fdef291ce3c"
|
|
119
119
|
}
|
|
@@ -2,7 +2,7 @@ import React, { forwardRef, ReactNode, SyntheticEvent, useRef } from 'react';
|
|
|
2
2
|
|
|
3
3
|
import classNames from 'classnames';
|
|
4
4
|
|
|
5
|
-
import { Dropdown, IconButtonProps, Offset, Placement, TextField, TextFieldProps } from '@lumx/react';
|
|
5
|
+
import { Dropdown, DropdownProps, IconButtonProps, Offset, Placement, TextField, TextFieldProps } from '@lumx/react';
|
|
6
6
|
|
|
7
7
|
import { Comp, GenericProps, HasTheme } from '@lumx/react/utils/type';
|
|
8
8
|
import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
|
|
@@ -41,10 +41,13 @@ export interface AutocompleteProps extends GenericProps, HasTheme {
|
|
|
41
41
|
*/
|
|
42
42
|
placement?: Placement;
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
44
|
+
* Manage dropdown width:
|
|
45
|
+
* - `maxWidth`: dropdown not bigger than anchor
|
|
46
|
+
* - `minWidth` or `true`: dropdown not smaller than anchor
|
|
47
|
+
* - `width`: dropdown equal to the anchor.
|
|
45
48
|
* @see {@link DropdownProps#fitToAnchorWidth}
|
|
46
49
|
*/
|
|
47
|
-
fitToAnchorWidth?:
|
|
50
|
+
fitToAnchorWidth?: DropdownProps['fitToAnchorWidth'];
|
|
48
51
|
/**
|
|
49
52
|
* The error related to the component.
|
|
50
53
|
* @see {@link TextFieldProps#error}
|
|
@@ -160,7 +163,7 @@ export interface AutocompleteProps extends GenericProps, HasTheme {
|
|
|
160
163
|
* Only the props not managed by the Autocomplete can be set.
|
|
161
164
|
* @see {@link TextFieldProps}
|
|
162
165
|
*/
|
|
163
|
-
textFieldProps?: TextFieldProps
|
|
166
|
+
textFieldProps?: Partial<TextFieldProps>;
|
|
164
167
|
}
|
|
165
168
|
|
|
166
169
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ColorPalette } from '@lumx/react';
|
|
2
2
|
import { Comp, GenericProps } from '@lumx/react/utils/type';
|
|
3
3
|
import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
|
|
4
4
|
import classNames from 'classnames';
|
|
@@ -11,7 +11,7 @@ export interface BadgeProps extends GenericProps {
|
|
|
11
11
|
/** Badge content. */
|
|
12
12
|
children?: ReactNode;
|
|
13
13
|
/** Color variant. */
|
|
14
|
-
color?:
|
|
14
|
+
color?: ColorPalette;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { Fragment } from 'react';
|
|
2
2
|
import { mdiSend } from '@lumx/icons';
|
|
3
|
-
import { Button, ColorPalette, IconButton } from '@lumx/react';
|
|
3
|
+
import { Button, ColorPalette, IconButton, Text } from '@lumx/react';
|
|
4
4
|
import { squareImageKnob } from '@lumx/react/stories/knobs/image';
|
|
5
5
|
import { buttonSize } from '@lumx/react/stories/knobs/buttonKnob';
|
|
6
6
|
import { emphasis } from '@lumx/react/stories/knobs/emphasisKnob';
|
|
@@ -30,6 +30,32 @@ export const SimpleButton = ({ theme }: any) => {
|
|
|
30
30
|
);
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
export const SimpleButtonWithTruncatedText = ({ theme }: any) => {
|
|
34
|
+
const buttonText =
|
|
35
|
+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Potenti nullam ac tortor vitae. Lorem ipsum dolor sit amet. Diam sollicitudin tempor id eu nisl nunc mi ipsum. Elementum facilisis leo vel fringilla est ullamcorper eget nulla. Mollis aliquam ut porttitor leo a diam sollicitudin tempor. Ultrices tincidunt arcu non sodales neque sodales.';
|
|
36
|
+
return (
|
|
37
|
+
<Button
|
|
38
|
+
aria-pressed={boolean('isSelected', Boolean(DEFAULT_PROPS.isSelected))}
|
|
39
|
+
emphasis={emphasis('Emphasis', DEFAULT_PROPS.emphasis)}
|
|
40
|
+
theme={theme}
|
|
41
|
+
rightIcon={select('Right icon', { none: undefined, mdiSend }, undefined)}
|
|
42
|
+
leftIcon={select('Left icon', { none: undefined, mdiSend }, undefined)}
|
|
43
|
+
size={buttonSize()}
|
|
44
|
+
isSelected={boolean('isSelected', Boolean(DEFAULT_PROPS.isSelected))}
|
|
45
|
+
isDisabled={boolean('isDisabled', Boolean(DEFAULT_PROPS.isDisabled))}
|
|
46
|
+
color={select('color', ColorPalette, DEFAULT_PROPS.color)}
|
|
47
|
+
href={text('Button link', '')}
|
|
48
|
+
hasBackground={boolean('hasBackground', Boolean(DEFAULT_PROPS.hasBackground))}
|
|
49
|
+
fullWidth
|
|
50
|
+
title={buttonText}
|
|
51
|
+
>
|
|
52
|
+
<Text as="span" truncate>
|
|
53
|
+
{text('Button content', buttonText)}
|
|
54
|
+
</Text>
|
|
55
|
+
</Button>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
33
59
|
export const WithHref = () => <Button href="https://google.com">Button with redirection</Button>;
|
|
34
60
|
|
|
35
61
|
export const Disabled = () => <Button isDisabled>Disabled button</Button>;
|
|
@@ -3,8 +3,8 @@ import React, { forwardRef, ReactNode } from 'react';
|
|
|
3
3
|
import classNames from 'classnames';
|
|
4
4
|
import isEmpty from 'lodash/isEmpty';
|
|
5
5
|
|
|
6
|
-
import { Emphasis, Icon, Size, Theme } from '@lumx/react';
|
|
7
|
-
import { Comp } from '@lumx/react/utils/type';
|
|
6
|
+
import { Emphasis, Icon, Size, Theme, Text } from '@lumx/react';
|
|
7
|
+
import { Comp, isComponent } from '@lumx/react/utils/type';
|
|
8
8
|
import { getBasicClass, getRootClassName } from '@lumx/react/utils/className';
|
|
9
9
|
import { BaseButtonProps, ButtonRoot } from './ButtonRoot';
|
|
10
10
|
|
|
@@ -71,7 +71,7 @@ export const Button: Comp<ButtonProps, HTMLButtonElement | HTMLAnchorElement> =
|
|
|
71
71
|
variant="button"
|
|
72
72
|
>
|
|
73
73
|
{leftIcon && !isEmpty(leftIcon) && <Icon icon={leftIcon} />}
|
|
74
|
-
{children && <span>{children}</span>}
|
|
74
|
+
{children && (isComponent(Text)(children) ? children : <span>{children}</span>)}
|
|
75
75
|
{rightIcon && !isEmpty(rightIcon) && <Icon icon={rightIcon} />}
|
|
76
76
|
</ButtonRoot>
|
|
77
77
|
);
|
|
@@ -4,7 +4,7 @@ import isEmpty from 'lodash/isEmpty';
|
|
|
4
4
|
|
|
5
5
|
import classNames from 'classnames';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { ColorPalette, Emphasis, Size, Theme } from '@lumx/react';
|
|
8
8
|
import { CSS_PREFIX } from '@lumx/react/constants';
|
|
9
9
|
import { Comp, GenericProps, HasTheme } from '@lumx/react/utils/type';
|
|
10
10
|
import { handleBasicClasses } from '@lumx/react/utils/className';
|
|
@@ -22,7 +22,7 @@ export interface BaseButtonProps
|
|
|
22
22
|
Pick<AriaAttributes, 'aria-expanded' | 'aria-haspopup' | 'aria-pressed' | 'aria-label'>,
|
|
23
23
|
HasTheme {
|
|
24
24
|
/** Color variant. */
|
|
25
|
-
color?:
|
|
25
|
+
color?: ColorPalette;
|
|
26
26
|
/** Emphasis variant. */
|
|
27
27
|
emphasis?: Emphasis;
|
|
28
28
|
/** Whether or not the button has a background color in low emphasis. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ColorPalette, Size, Theme } from '@lumx/react';
|
|
2
2
|
import { useStopPropagation } from '@lumx/react/hooks/useStopPropagation';
|
|
3
3
|
|
|
4
4
|
import { Comp, GenericProps, HasTheme } from '@lumx/react/utils/type';
|
|
@@ -24,7 +24,7 @@ export interface ChipProps extends GenericProps, HasTheme {
|
|
|
24
24
|
/** A component to be rendered before the content. */
|
|
25
25
|
before?: ReactNode;
|
|
26
26
|
/** Color variant. */
|
|
27
|
-
color?:
|
|
27
|
+
color?: ColorPalette;
|
|
28
28
|
/** Whether the component is clickable or not. */
|
|
29
29
|
isClickable?: boolean;
|
|
30
30
|
/** Whether the component is disabled or not. */
|
|
@@ -34,10 +34,13 @@ export interface DropdownProps extends GenericProps {
|
|
|
34
34
|
*/
|
|
35
35
|
closeOnEscape?: boolean;
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
37
|
+
* Manage dropdown width:
|
|
38
|
+
* - `maxWidth`: dropdown not bigger than anchor
|
|
39
|
+
* - `minWidth` or `true`: dropdown not smaller than anchor
|
|
40
|
+
* - `width`: dropdown equal to the anchor.
|
|
38
41
|
* @see {@link PopoverProps#fitToAnchorWidth}
|
|
39
42
|
*/
|
|
40
|
-
fitToAnchorWidth?:
|
|
43
|
+
fitToAnchorWidth?: PopoverProps['fitToAnchorWidth'];
|
|
41
44
|
/**
|
|
42
45
|
* Whether the dropdown should shrink to fit within the viewport height or not.
|
|
43
46
|
* @see {@link PopoverProps#fitWithinViewportHeight}
|
|
@@ -2,7 +2,7 @@ import React, { forwardRef } from 'react';
|
|
|
2
2
|
|
|
3
3
|
import classNames from 'classnames';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { ColorPalette, ColorVariant, Size, Theme } from '@lumx/react';
|
|
6
6
|
import { Comp, GenericProps, HasTheme } from '@lumx/react/utils/type';
|
|
7
7
|
import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
|
|
8
8
|
import { mdiAlertCircle } from '@lumx/icons';
|
|
@@ -14,7 +14,7 @@ export type IconSizes = Extract<Size, 'xxs' | 'xs' | 's' | 'm' | 'l' | 'xl' | 'x
|
|
|
14
14
|
*/
|
|
15
15
|
export interface IconProps extends GenericProps, HasTheme {
|
|
16
16
|
/** Color variant. */
|
|
17
|
-
color?:
|
|
17
|
+
color?: ColorPalette;
|
|
18
18
|
/** Lightened or darkened variant of the selected icon color. */
|
|
19
19
|
colorVariant?: ColorVariant;
|
|
20
20
|
/** Whether the icon has a shape. */
|
package/src/components/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import isEmpty from 'lodash/isEmpty';
|
|
|
4
4
|
|
|
5
5
|
import classNames from 'classnames';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { ColorPalette, ColorVariant, Icon, Size, Typography } from '@lumx/react';
|
|
8
8
|
import { Comp, GenericProps } from '@lumx/react/utils/type';
|
|
9
9
|
import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
|
|
10
10
|
import { renderLink } from '@lumx/react/utils/renderLink';
|
|
@@ -16,7 +16,7 @@ type HTMLAnchorProps = React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAn
|
|
|
16
16
|
*/
|
|
17
17
|
export interface LinkProps extends GenericProps {
|
|
18
18
|
/** Color variant. */
|
|
19
|
-
color?:
|
|
19
|
+
color?: ColorPalette;
|
|
20
20
|
/** Lightened or darkened variant of the selected icon color. */
|
|
21
21
|
colorVariant?: ColorVariant;
|
|
22
22
|
/** Link href. */
|
|
@@ -314,3 +314,73 @@ export const NestedWithoutPortal = () => {
|
|
|
314
314
|
</div>
|
|
315
315
|
);
|
|
316
316
|
};
|
|
317
|
+
|
|
318
|
+
export const FitToAnchorWidth = ({ theme }: any) => {
|
|
319
|
+
const demoPopperStyle = {
|
|
320
|
+
alignItems: 'center',
|
|
321
|
+
display: 'flex',
|
|
322
|
+
height: 100,
|
|
323
|
+
justifyContent: 'center',
|
|
324
|
+
width: 200,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const container = {
|
|
328
|
+
alignItems: 'center',
|
|
329
|
+
display: 'flex',
|
|
330
|
+
justifyContent: 'center',
|
|
331
|
+
flexDirection: 'column',
|
|
332
|
+
gap: 150,
|
|
333
|
+
marginTop: 150,
|
|
334
|
+
} as const;
|
|
335
|
+
|
|
336
|
+
const maxWidthAnchorRef = useRef(null);
|
|
337
|
+
const widthSmallAnchorRef = useRef(null);
|
|
338
|
+
const widthLargeAnchorRef = useRef(null);
|
|
339
|
+
const minWidthAnchorRef = useRef(null);
|
|
340
|
+
const defaultWidthAnchorRef = useRef(null);
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
<div style={container}>
|
|
344
|
+
<div>
|
|
345
|
+
<Chip ref={maxWidthAnchorRef} theme={theme} size={Size.s}>
|
|
346
|
+
Anchor
|
|
347
|
+
</Chip>
|
|
348
|
+
</div>
|
|
349
|
+
<Popover theme={theme} anchorRef={maxWidthAnchorRef} fitToAnchorWidth="maxWidth" isOpen placement="top">
|
|
350
|
+
<div style={demoPopperStyle}>Popover maxWidth</div>
|
|
351
|
+
</Popover>
|
|
352
|
+
<div>
|
|
353
|
+
<Chip ref={widthSmallAnchorRef} theme={theme} size={Size.s}>
|
|
354
|
+
Anchor
|
|
355
|
+
</Chip>
|
|
356
|
+
</div>
|
|
357
|
+
<Popover theme={theme} anchorRef={widthSmallAnchorRef} fitToAnchorWidth="width" isOpen placement="top">
|
|
358
|
+
<div style={demoPopperStyle}>Popover width small anchor</div>
|
|
359
|
+
</Popover>
|
|
360
|
+
<div>
|
|
361
|
+
<Chip ref={widthLargeAnchorRef} theme={theme} size={Size.s}>
|
|
362
|
+
VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLargeAnchor
|
|
363
|
+
</Chip>
|
|
364
|
+
</div>
|
|
365
|
+
<Popover theme={theme} anchorRef={widthLargeAnchorRef} fitToAnchorWidth="width" isOpen placement="top">
|
|
366
|
+
<div style={demoPopperStyle}>Popover width large anchor</div>
|
|
367
|
+
</Popover>
|
|
368
|
+
<div>
|
|
369
|
+
<Chip ref={minWidthAnchorRef} theme={theme} size={Size.s}>
|
|
370
|
+
VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLargeAnchor
|
|
371
|
+
</Chip>
|
|
372
|
+
</div>
|
|
373
|
+
<Popover theme={theme} anchorRef={minWidthAnchorRef} fitToAnchorWidth="minWidth" isOpen placement="top">
|
|
374
|
+
<div style={demoPopperStyle}>Popover minWidth</div>
|
|
375
|
+
</Popover>
|
|
376
|
+
<div>
|
|
377
|
+
<Chip ref={defaultWidthAnchorRef} theme={theme} size={Size.s}>
|
|
378
|
+
VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLargeAnchor
|
|
379
|
+
</Chip>
|
|
380
|
+
</div>
|
|
381
|
+
<Popover theme={theme} anchorRef={defaultWidthAnchorRef} isOpen placement="top">
|
|
382
|
+
<div style={demoPopperStyle}>Popover default</div>
|
|
383
|
+
</Popover>
|
|
384
|
+
</div>
|
|
385
|
+
);
|
|
386
|
+
};
|
|
@@ -4,6 +4,7 @@ import { createPortal } from 'react-dom';
|
|
|
4
4
|
import { usePopper } from 'react-popper';
|
|
5
5
|
|
|
6
6
|
import classNames from 'classnames';
|
|
7
|
+
import memoize from 'lodash/memoize';
|
|
7
8
|
|
|
8
9
|
import { DOCUMENT, WINDOW } from '@lumx/react/constants';
|
|
9
10
|
import { useCallbackOnEscape } from '@lumx/react/hooks/useCallbackOnEscape';
|
|
@@ -15,6 +16,7 @@ import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/classNam
|
|
|
15
16
|
import { mergeRefs } from '@lumx/react/utils/mergeRefs';
|
|
16
17
|
import { useFocusWithin } from '@lumx/react/hooks/useFocusWithin';
|
|
17
18
|
import { getFirstAndLastFocusable } from '@lumx/react/utils/focus/getFirstAndLastFocusable';
|
|
19
|
+
import { useFocusTrap } from '@lumx/react/hooks/useFocusTrap';
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Different possible placements for the popover.
|
|
@@ -62,6 +64,13 @@ export type Elevation = 1 | 2 | 3 | 4 | 5;
|
|
|
62
64
|
*/
|
|
63
65
|
const ARROW_SIZE = 8;
|
|
64
66
|
|
|
67
|
+
const AnchorWidthOptions = {
|
|
68
|
+
MAX_WIDTH: 'maxWidth',
|
|
69
|
+
MIN_WIDTH: 'minWidth',
|
|
70
|
+
WIDTH: 'width',
|
|
71
|
+
} as const;
|
|
72
|
+
type AnchorWidthOption = ValueOf<typeof AnchorWidthOptions>;
|
|
73
|
+
|
|
65
74
|
/**
|
|
66
75
|
* Defines the props of the component.
|
|
67
76
|
*/
|
|
@@ -78,8 +87,13 @@ export interface PopoverProps extends GenericProps {
|
|
|
78
87
|
closeOnEscape?: boolean;
|
|
79
88
|
/** Shadow elevation. */
|
|
80
89
|
elevation?: Elevation;
|
|
81
|
-
/**
|
|
82
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Manage popover width:
|
|
92
|
+
* - `maxWidth`: popover not bigger than anchor
|
|
93
|
+
* - `minWidth` or `true`: popover not smaller than anchor
|
|
94
|
+
* - `width`: popover equal to the anchor.
|
|
95
|
+
*/
|
|
96
|
+
fitToAnchorWidth?: AnchorWidthOption | boolean;
|
|
83
97
|
/** Shrink popover if even after flipping there is not enough space. */
|
|
84
98
|
fitWithinViewportHeight?: boolean;
|
|
85
99
|
/** Element to focus when opening the popover. */
|
|
@@ -102,6 +116,8 @@ export interface PopoverProps extends GenericProps {
|
|
|
102
116
|
zIndex?: number;
|
|
103
117
|
/** On close callback (on click away or Escape pressed). */
|
|
104
118
|
onClose?(): void;
|
|
119
|
+
/** Whether the popover should trap the focus within itself. Default to false. */
|
|
120
|
+
withFocusTrap?: boolean;
|
|
105
121
|
}
|
|
106
122
|
|
|
107
123
|
/**
|
|
@@ -127,20 +143,20 @@ const DEFAULT_PROPS: Partial<PopoverProps> = {
|
|
|
127
143
|
/**
|
|
128
144
|
* Popper js modifier to fit popover min width to the anchor width.
|
|
129
145
|
*/
|
|
130
|
-
const sameWidth = {
|
|
146
|
+
const sameWidth = memoize((anchorWidthOption: AnchorWidthOption) => ({
|
|
131
147
|
name: 'sameWidth',
|
|
132
148
|
enabled: true,
|
|
133
149
|
phase: 'beforeWrite',
|
|
134
150
|
requires: ['computeStyles'],
|
|
135
151
|
fn({ state }: any) {
|
|
136
152
|
// eslint-disable-next-line no-param-reassign
|
|
137
|
-
state.styles.popper
|
|
153
|
+
state.styles.popper[anchorWidthOption] = `${state.rects.reference.width}px`;
|
|
138
154
|
},
|
|
139
155
|
effect({ state }: any) {
|
|
140
156
|
// eslint-disable-next-line no-param-reassign
|
|
141
|
-
state.elements.popper.style
|
|
157
|
+
state.elements.popper.style[anchorWidthOption] = `${state.elements.reference.offsetWidth}px`;
|
|
142
158
|
},
|
|
143
|
-
};
|
|
159
|
+
}));
|
|
144
160
|
|
|
145
161
|
/**
|
|
146
162
|
* Popper js modifier to compute max size of the popover.
|
|
@@ -222,6 +238,7 @@ export const Popover: Comp<PopoverProps, HTMLDivElement> = forwardRef((props, re
|
|
|
222
238
|
usePortal,
|
|
223
239
|
zIndex,
|
|
224
240
|
focusAnchorOnClose = true,
|
|
241
|
+
withFocusTrap,
|
|
225
242
|
...forwardedProps
|
|
226
243
|
} = props;
|
|
227
244
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
@@ -230,6 +247,8 @@ export const Popover: Comp<PopoverProps, HTMLDivElement> = forwardRef((props, re
|
|
|
230
247
|
const [arrowElement, setArrowElement] = useState<null | HTMLElement>(null);
|
|
231
248
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
232
249
|
const clickAwayRef = useRef<HTMLDivElement>(null);
|
|
250
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
251
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
233
252
|
|
|
234
253
|
/**
|
|
235
254
|
* Track whether the focus is currently set in the
|
|
@@ -290,7 +309,9 @@ export const Popover: Comp<PopoverProps, HTMLDivElement> = forwardRef((props, re
|
|
|
290
309
|
}
|
|
291
310
|
|
|
292
311
|
if (fitToAnchorWidth) {
|
|
293
|
-
|
|
312
|
+
const anchorWidthOption =
|
|
313
|
+
typeof fitToAnchorWidth === 'string' ? fitToAnchorWidth : AnchorWidthOptions.MIN_WIDTH;
|
|
314
|
+
modifiers.push(sameWidth(anchorWidthOption));
|
|
294
315
|
}
|
|
295
316
|
if (fitWithinViewportHeight) {
|
|
296
317
|
modifiers.push({ ...maxSize, options: { boundary: boundaryRef?.current } }, applyMaxHeight);
|
|
@@ -325,8 +346,12 @@ export const Popover: Comp<PopoverProps, HTMLDivElement> = forwardRef((props, re
|
|
|
325
346
|
|
|
326
347
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
327
348
|
useCallbackOnEscape(handleClose, isOpen && closeOnEscape);
|
|
349
|
+
|
|
350
|
+
/** Only set focus within if the focus trap is disabled as they interfere with one another. */
|
|
351
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
352
|
+
useFocus(focusElement?.current, !withFocusTrap && isOpen && (state?.rects?.popper?.y ?? -1) >= 0);
|
|
328
353
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
329
|
-
|
|
354
|
+
useFocusTrap(withFocusTrap && isOpen && contentRef?.current, focusElement?.current);
|
|
330
355
|
|
|
331
356
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
332
357
|
const clickAwayRefs = useRef([clickAwayRef, anchorRef]);
|
|
@@ -335,7 +360,7 @@ export const Popover: Comp<PopoverProps, HTMLDivElement> = forwardRef((props, re
|
|
|
335
360
|
? renderPopover(
|
|
336
361
|
<div
|
|
337
362
|
{...forwardedProps}
|
|
338
|
-
ref={mergeRefs<HTMLDivElement>(setPopperElement, ref, clickAwayRef)}
|
|
363
|
+
ref={mergeRefs<HTMLDivElement>(setPopperElement, ref, clickAwayRef, contentRef)}
|
|
339
364
|
className={classNames(
|
|
340
365
|
className,
|
|
341
366
|
handleBasicClasses({ prefix: CLASSNAME, elevation: Math.min(elevation || 0, 5), position }),
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { mdiMenu } from '@lumx/icons/index';
|
|
2
|
+
import { mdiSettings } from '@lumx/icons/v4-to-v5-aliases';
|
|
3
|
+
import React, { useRef, useState } from 'react';
|
|
4
|
+
import { PopoverDialog, PopoverDialogProps } from '.';
|
|
5
|
+
import { Emphasis, Orientation, Size, Typography } from '..';
|
|
6
|
+
import { Button, IconButton } from '../button';
|
|
7
|
+
import { FlexBox } from '../flex-box';
|
|
8
|
+
import { Heading } from '../heading';
|
|
9
|
+
import { List, ListItem } from '../list';
|
|
10
|
+
import { Placement } from '../popover/Popover';
|
|
11
|
+
import { Toolbar } from '../toolbar';
|
|
12
|
+
|
|
13
|
+
const WithButton = (Story: any, context: any) => {
|
|
14
|
+
const anchorRef = useRef(null);
|
|
15
|
+
const [isOpen, setIsOpen] = useState<boolean>(context?.args?.isOpen || false);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<Button ref={anchorRef} onClick={() => setIsOpen((current) => !current)}>
|
|
20
|
+
Open popover
|
|
21
|
+
</Button>
|
|
22
|
+
<Story anchorRef={anchorRef} isOpen={isOpen} onClose={() => setIsOpen(false)} />
|
|
23
|
+
</>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const dialogHeaderId = 'dialog-header';
|
|
28
|
+
|
|
29
|
+
const DemoPopoverContent = () => (
|
|
30
|
+
<FlexBox orientation={Orientation.vertical}>
|
|
31
|
+
<Toolbar
|
|
32
|
+
label={
|
|
33
|
+
<Heading id="dialogHeaderId" typography={Typography.headline}>
|
|
34
|
+
Title
|
|
35
|
+
</Heading>
|
|
36
|
+
}
|
|
37
|
+
after={<IconButton label="Settings" icon={mdiSettings} emphasis={Emphasis.low} />}
|
|
38
|
+
/>
|
|
39
|
+
<List>
|
|
40
|
+
<ListItem size={Size.huge} after={<IconButton label="Menu" icon={mdiMenu} size={Size.s} />}>
|
|
41
|
+
List Item With Actions
|
|
42
|
+
</ListItem>
|
|
43
|
+
<ListItem
|
|
44
|
+
size={Size.huge}
|
|
45
|
+
linkProps={{
|
|
46
|
+
href: 'http://google.com',
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
Clickable list item
|
|
50
|
+
</ListItem>
|
|
51
|
+
</List>
|
|
52
|
+
</FlexBox>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
export default {
|
|
56
|
+
title: 'LumX components/popover-dialog/PopoverDialog',
|
|
57
|
+
component: PopoverDialog,
|
|
58
|
+
decorators: [WithButton],
|
|
59
|
+
args: {
|
|
60
|
+
children: <DemoPopoverContent />,
|
|
61
|
+
'aria-labelledby': dialogHeaderId,
|
|
62
|
+
placement: Placement.BOTTOM,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const Template = (args: PopoverDialogProps, context: any) => {
|
|
67
|
+
const { anchorRef, isOpen, onClose } = context;
|
|
68
|
+
return (
|
|
69
|
+
<PopoverDialog {...args} anchorRef={anchorRef} isOpen={isOpen} onClose={onClose}>
|
|
70
|
+
{args.children}
|
|
71
|
+
</PopoverDialog>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const Default = Template.bind({});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React, { useRef, useState } from 'react';
|
|
2
|
+
import { render, screen, within } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
|
|
5
|
+
import { PopoverDialog } from './PopoverDialog';
|
|
6
|
+
|
|
7
|
+
const DialogWithButton = (forwardedProps: any) => {
|
|
8
|
+
const anchorRef = useRef(null);
|
|
9
|
+
const [isOpen, setIsOpen] = useState<boolean>(false);
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<button type="button" ref={anchorRef} onClick={() => setIsOpen((current) => !current)}>
|
|
14
|
+
Open popover
|
|
15
|
+
</button>
|
|
16
|
+
|
|
17
|
+
<PopoverDialog {...forwardedProps} anchorRef={anchorRef} isOpen={isOpen} onClose={() => setIsOpen(false)}>
|
|
18
|
+
<button type="button">Button 1</button>
|
|
19
|
+
<button type="button">Button 2</button>
|
|
20
|
+
</PopoverDialog>
|
|
21
|
+
{/* This should never have focus while popover is opened */}
|
|
22
|
+
<button type="button">External button</button>
|
|
23
|
+
</>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe(`<${PopoverDialog.displayName}>`, () => {
|
|
28
|
+
it('should behave like a dialog', async () => {
|
|
29
|
+
const label = 'Test Label';
|
|
30
|
+
|
|
31
|
+
render(<DialogWithButton label={label} />);
|
|
32
|
+
|
|
33
|
+
/** Open the popover */
|
|
34
|
+
const triggerElement = screen.getByRole('button', { name: 'Open popover' });
|
|
35
|
+
await userEvent.click(triggerElement);
|
|
36
|
+
|
|
37
|
+
const dialog = await screen.findByRole('dialog', { name: label });
|
|
38
|
+
const withinDialog = within(dialog);
|
|
39
|
+
|
|
40
|
+
/** Get buttons within dialog */
|
|
41
|
+
const dialogButtons = withinDialog.getAllByRole('button');
|
|
42
|
+
|
|
43
|
+
// First button should have focus by default on opening
|
|
44
|
+
expect(dialogButtons[0]).toHaveFocus();
|
|
45
|
+
|
|
46
|
+
// Tab to next button
|
|
47
|
+
await userEvent.tab();
|
|
48
|
+
|
|
49
|
+
// Second button should have focus
|
|
50
|
+
expect(dialogButtons[1]).toHaveFocus();
|
|
51
|
+
|
|
52
|
+
// Tab to next button
|
|
53
|
+
await userEvent.tab();
|
|
54
|
+
|
|
55
|
+
// As there is no more button, focus should loop back to first button.
|
|
56
|
+
expect(dialogButtons[0]).toHaveFocus();
|
|
57
|
+
|
|
58
|
+
// Close the popover
|
|
59
|
+
await userEvent.keyboard('{escape}');
|
|
60
|
+
|
|
61
|
+
expect(screen.queryByRole('dialog', { name: label })).not.toBeInTheDocument();
|
|
62
|
+
/** Anchor should retrieve the focus */
|
|
63
|
+
expect(triggerElement).toHaveFocus();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
|
|
4
|
+
import { Comp, HasAriaLabelOrLabelledBy } from '@lumx/react/utils/type';
|
|
5
|
+
import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
|
|
6
|
+
|
|
7
|
+
import { Popover, PopoverProps } from '../popover/Popover';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* PopoverDialog props.
|
|
11
|
+
* The PopoverDialog has the same props as the Popover but requires an accessible label.
|
|
12
|
+
*/
|
|
13
|
+
export type PopoverDialogProps = PopoverProps & HasAriaLabelOrLabelledBy;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Component display name.
|
|
17
|
+
*/
|
|
18
|
+
const COMPONENT_NAME = 'PopoverDialog';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Component default class name and class prefix.
|
|
22
|
+
*/
|
|
23
|
+
const CLASSNAME = getRootClassName(COMPONENT_NAME);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Component default props.
|
|
27
|
+
*/
|
|
28
|
+
const DEFAULT_PROPS: Partial<PopoverDialogProps> = {};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* PopoverDialog component.
|
|
32
|
+
* Defines a popover that acts like a dialog
|
|
33
|
+
* * Has a dialog aria role
|
|
34
|
+
* * Sets a focus trap within the popover
|
|
35
|
+
* * Closes on click away and escape.
|
|
36
|
+
*/
|
|
37
|
+
export const PopoverDialog: Comp<PopoverDialogProps, HTMLDivElement> = forwardRef((props, ref) => {
|
|
38
|
+
const { children, isOpen, focusElement, label, className, ...forwardedProps } = props;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Popover
|
|
42
|
+
{...forwardedProps}
|
|
43
|
+
ref={ref}
|
|
44
|
+
className={classNames(className, handleBasicClasses({ prefix: CLASSNAME }))}
|
|
45
|
+
role="dialog"
|
|
46
|
+
aria-modal="true"
|
|
47
|
+
/**
|
|
48
|
+
* If a label is set, set as aria-label.
|
|
49
|
+
* If it is undefined, the label can be set using the `aria-label` and `aria-labelledby` props
|
|
50
|
+
*/
|
|
51
|
+
aria-label={label}
|
|
52
|
+
isOpen={isOpen}
|
|
53
|
+
focusElement={focusElement}
|
|
54
|
+
closeOnClickAway
|
|
55
|
+
closeOnEscape
|
|
56
|
+
withFocusTrap
|
|
57
|
+
>
|
|
58
|
+
{children}
|
|
59
|
+
</Popover>
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
PopoverDialog.displayName = COMPONENT_NAME;
|
|
64
|
+
PopoverDialog.className = CLASSNAME;
|
|
65
|
+
PopoverDialog.defaultProps = DEFAULT_PROPS;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './PopoverDialog';
|
package/src/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ export * from './components/message';
|
|
|
35
35
|
export * from './components/mosaic';
|
|
36
36
|
export * from './components/notification';
|
|
37
37
|
export * from './components/popover';
|
|
38
|
+
export * from './components/popover-dialog';
|
|
38
39
|
export * from './components/post-block';
|
|
39
40
|
export * from './components/progress';
|
|
40
41
|
export * from './components/progress-tracker';
|