@lumx/react 2.2.18 → 2.2.20

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.
Files changed (100) hide show
  1. package/esm/_internal/ButtonRoot.js.map +1 -1
  2. package/esm/_internal/Checkbox2.js +3 -1
  3. package/esm/_internal/Checkbox2.js.map +1 -1
  4. package/esm/_internal/ClickAwayProvider.js +90 -12
  5. package/esm/_internal/ClickAwayProvider.js.map +1 -1
  6. package/esm/_internal/DatePickerField.js +18 -11
  7. package/esm/_internal/DatePickerField.js.map +1 -1
  8. package/esm/_internal/Dialog2.js +2 -2
  9. package/esm/_internal/Dialog2.js.map +1 -1
  10. package/esm/_internal/GenericBlock.js +90 -0
  11. package/esm/_internal/GenericBlock.js.map +1 -0
  12. package/esm/_internal/Lightbox2.js +2 -2
  13. package/esm/_internal/Lightbox2.js.map +1 -1
  14. package/esm/_internal/LinkPreview.js +22 -12
  15. package/esm/_internal/LinkPreview.js.map +1 -1
  16. package/esm/_internal/Popover2.js +21 -8
  17. package/esm/_internal/Popover2.js.map +1 -1
  18. package/esm/_internal/SelectMultiple.js +16 -4
  19. package/esm/_internal/SelectMultiple.js.map +1 -1
  20. package/esm/_internal/Tooltip2.js +3 -7
  21. package/esm/_internal/Tooltip2.js.map +1 -1
  22. package/esm/_internal/UserBlock.js +9 -2
  23. package/esm/_internal/UserBlock.js.map +1 -1
  24. package/esm/_internal/alert-dialog.js +2 -2
  25. package/esm/_internal/autocomplete.js +2 -1
  26. package/esm/_internal/autocomplete.js.map +1 -1
  27. package/esm/_internal/button.js +2 -1
  28. package/esm/_internal/button.js.map +1 -1
  29. package/esm/_internal/comment-block.js +2 -1
  30. package/esm/_internal/comment-block.js.map +1 -1
  31. package/esm/_internal/date-picker.js +3 -2
  32. package/esm/_internal/date-picker.js.map +1 -1
  33. package/esm/_internal/dialog.js +2 -2
  34. package/esm/_internal/dropdown.js +2 -1
  35. package/esm/_internal/dropdown.js.map +1 -1
  36. package/esm/_internal/expansion-panel.js +1 -1
  37. package/esm/_internal/generic-block.js +12 -0
  38. package/esm/_internal/generic-block.js.map +1 -0
  39. package/esm/_internal/lightbox.js +3 -2
  40. package/esm/_internal/lightbox.js.map +1 -1
  41. package/esm/_internal/popover.js +2 -1
  42. package/esm/_internal/popover.js.map +1 -1
  43. package/esm/_internal/select.js +2 -1
  44. package/esm/_internal/select.js.map +1 -1
  45. package/esm/_internal/side-navigation.js +2 -1
  46. package/esm/_internal/side-navigation.js.map +1 -1
  47. package/esm/_internal/slideshow.js +2 -1
  48. package/esm/_internal/slideshow.js.map +1 -1
  49. package/esm/_internal/text-field.js +2 -1
  50. package/esm/_internal/text-field.js.map +1 -1
  51. package/esm/_internal/tooltip.js +2 -1
  52. package/esm/_internal/tooltip.js.map +1 -1
  53. package/esm/_internal/type.js.map +1 -1
  54. package/esm/_internal/useFocusTrap.js +65 -72
  55. package/esm/_internal/useFocusTrap.js.map +1 -1
  56. package/esm/_internal/user-block.js +1 -0
  57. package/esm/_internal/user-block.js.map +1 -1
  58. package/esm/index.js +4 -2
  59. package/esm/index.js.map +1 -1
  60. package/package.json +5 -5
  61. package/src/components/button/Button.stories.tsx +1 -0
  62. package/src/components/button/ButtonRoot.tsx +4 -4
  63. package/src/components/checkbox/Checkbox.tsx +2 -1
  64. package/src/components/checkbox/__snapshots__/Checkbox.test.tsx.snap +4 -0
  65. package/src/components/date-picker/DatePickerField.tsx +15 -16
  66. package/src/components/date-picker/types.ts +2 -2
  67. package/src/components/dialog/Dialog.stories.tsx +57 -14
  68. package/src/components/dialog/Dialog.tsx +1 -1
  69. package/src/components/dialog/__snapshots__/Dialog.test.tsx.snap +160 -91
  70. package/src/components/generic-block/GenericBlock.stories.tsx +149 -0
  71. package/src/components/generic-block/GenericBlock.test.tsx +28 -0
  72. package/src/components/generic-block/GenericBlock.tsx +120 -0
  73. package/src/components/generic-block/__snapshots__/GenericBlock.test.tsx.snap +92 -0
  74. package/src/components/generic-block/index.ts +1 -0
  75. package/src/components/lightbox/Lightbox.tsx +1 -1
  76. package/src/components/link-preview/LinkPreview.test.tsx +50 -55
  77. package/src/components/link-preview/LinkPreview.tsx +43 -16
  78. package/src/components/popover/Popover.tsx +20 -4
  79. package/src/components/select/Select.stories.tsx +2 -0
  80. package/src/components/select/Select.tsx +11 -1
  81. package/src/components/select/SelectMultiple.stories.tsx +2 -0
  82. package/src/components/select/SelectMultiple.tsx +11 -1
  83. package/src/components/select/constants.ts +2 -0
  84. package/src/components/table/__snapshots__/Table.test.tsx.snap +5 -0
  85. package/src/components/tooltip/Tooltip.tsx +2 -5
  86. package/src/components/user-block/UserBlock.stories.tsx +4 -4
  87. package/src/components/user-block/UserBlock.tsx +9 -3
  88. package/src/components/user-block/__snapshots__/UserBlock.test.tsx.snap +51 -8
  89. package/src/hooks/useBooleanState.tsx +4 -10
  90. package/src/hooks/useCallbackOnEscape.ts +21 -13
  91. package/src/hooks/useFocusTrap.ts +67 -76
  92. package/src/index.ts +1 -0
  93. package/src/stories/generated/Dialog/Demos.stories.tsx +1 -0
  94. package/src/stories/generated/GenericBlock/Demos.stories.tsx +6 -0
  95. package/src/utils/focus/getFirstAndLastFocusable.test.ts +128 -0
  96. package/src/utils/focus/getFirstAndLastFocusable.ts +27 -0
  97. package/src/utils/makeListenerTowerContext.ts +32 -0
  98. package/src/utils/type.ts +3 -0
  99. package/types.d.ts +50 -9
  100. package/src/components/link-preview/__snapshots__/LinkPreview.test.tsx.snap +0 -51
@@ -0,0 +1,120 @@
1
+ import React, { forwardRef, ReactNode } from 'react';
2
+ import classNames from 'classnames';
3
+ import { Comp, getRootClassName } from '@lumx/react/utils';
4
+ import { Orientation, Size, FlexBox, FlexBoxProps, Alignment, HorizontalAlignment } from '@lumx/react';
5
+
6
+ export interface GenericBlockProps extends FlexBoxProps {
7
+ /** Component to use as visual element. */
8
+ figure?: ReactNode;
9
+ /** Actions to set after the main content. */
10
+ actions?: ReactNode;
11
+ /** Main content to display */
12
+ children: ReactNode;
13
+ /** Orientation of the 3 sections */
14
+ orientation?: FlexBoxProps['orientation'];
15
+ /** Horizontal alignment. */
16
+ hAlign?: FlexBoxProps['hAlign'];
17
+ /** Vertical alignment. */
18
+ vAlign?: FlexBoxProps['vAlign'];
19
+ /**
20
+ * The props to forward to the content.
21
+ * By default, the content will have the same alignment as wrapper.
22
+ */
23
+ contentProps?: Omit<FlexBoxProps, 'children'>;
24
+ /** props to forward to the actions element. */
25
+ actionsProps?: Omit<FlexBoxProps, 'children'>;
26
+ /** props to forward to the figure element. */
27
+ figureProps?: Omit<FlexBoxProps, 'children'>;
28
+ }
29
+
30
+ /**
31
+ * Component display name.
32
+ */
33
+ const COMPONENT_NAME = 'GenericBlock';
34
+
35
+ /**
36
+ * Component default class name and class prefix.
37
+ */
38
+ const CLASSNAME = getRootClassName(COMPONENT_NAME);
39
+
40
+ /**
41
+ * Component default props.
42
+ */
43
+ const DEFAULT_PROPS: Partial<GenericBlockProps> = {
44
+ gap: Size.regular,
45
+ orientation: Orientation.vertical,
46
+ hAlign: Alignment.center,
47
+ vAlign: Alignment.center,
48
+ };
49
+
50
+ /**
51
+ * The GenericBlock is a layout component made of 3 sections that can be
52
+ * displayed either horizontally of vertically with the same gap between each section.
53
+ *
54
+ * The sections are:
55
+ * * (Optional) `Figure` => A visual element to display before the main content.
56
+ * * (Required) `Content` => The main content displayed
57
+ * * (Optional) `Actions` => One or more actions to set after the element.
58
+ *
59
+ * @see https://www.figma.com/file/lzzrQmsfaXRaOyRfoEogPZ/DS%3A-playground?node-id=1%3A4076
60
+ */
61
+ export const GenericBlock: Comp<GenericBlockProps, HTMLDivElement> = forwardRef((props, ref) => {
62
+ const {
63
+ className,
64
+ figure,
65
+ figureProps,
66
+ children,
67
+ actions,
68
+ actionsProps,
69
+ gap,
70
+ orientation,
71
+ contentProps,
72
+ ...forwardedProps
73
+ } = props;
74
+
75
+ let actionsVAlign: HorizontalAlignment = Alignment.center;
76
+ if (orientation === Orientation.horizontal) {
77
+ actionsVAlign = Alignment.right;
78
+ }
79
+ let contentVAlign: HorizontalAlignment = Alignment.center;
80
+ if (orientation === Orientation.horizontal) {
81
+ contentVAlign = Alignment.left;
82
+ }
83
+
84
+ return (
85
+ <FlexBox
86
+ ref={ref}
87
+ className={classNames(className, CLASSNAME)}
88
+ gap={gap}
89
+ orientation={orientation}
90
+ {...forwardedProps}
91
+ >
92
+ <FlexBox {...figureProps} className={classNames(figureProps?.className, `${CLASSNAME}__figure`)}>
93
+ {figure}
94
+ </FlexBox>
95
+
96
+ {children && (
97
+ <FlexBox
98
+ orientation={Orientation.vertical}
99
+ fillSpace
100
+ vAlign={contentVAlign}
101
+ {...contentProps}
102
+ className={classNames(contentProps?.className, `${CLASSNAME}__content`)}
103
+ >
104
+ {children}
105
+ </FlexBox>
106
+ )}
107
+
108
+ <FlexBox
109
+ vAlign={actionsVAlign}
110
+ {...actionsProps}
111
+ className={classNames(actionsProps?.className, `${CLASSNAME}__actions`)}
112
+ >
113
+ {actions}
114
+ </FlexBox>
115
+ </FlexBox>
116
+ );
117
+ });
118
+ GenericBlock.displayName = COMPONENT_NAME;
119
+ GenericBlock.className = CLASSNAME;
120
+ GenericBlock.defaultProps = DEFAULT_PROPS;
@@ -0,0 +1,92 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<GenericBlock> Snapshots and structure should render story 'Default' 1`] = `
4
+ Array [
5
+ <FlexBox
6
+ className="lumx-generic-block"
7
+ gap="regular"
8
+ hAlign="center"
9
+ orientation="vertical"
10
+ vAlign="center"
11
+ >
12
+ <FlexBox
13
+ className="lumx-generic-block__figure"
14
+ >
15
+ <Avatar
16
+ alt=""
17
+ image="/demo-assets/persona.png"
18
+ size="m"
19
+ theme="light"
20
+ />
21
+ </FlexBox>
22
+ <FlexBox
23
+ className="lumx-generic-block__content"
24
+ fillSpace={true}
25
+ orientation="vertical"
26
+ vAlign="center"
27
+ >
28
+ <h2>
29
+ Content title
30
+ </h2>
31
+ <p>
32
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec rhoncus libero aliquet pharetra luctus. Fusce nisl turpis, posuere ac tellus at, euismod vulputate libero...
33
+ </p>
34
+ </FlexBox>
35
+ <FlexBox
36
+ className="lumx-generic-block__actions"
37
+ vAlign="center"
38
+ >
39
+ <Button
40
+ emphasis="high"
41
+ size="m"
42
+ theme="light"
43
+ >
44
+ Actions
45
+ </Button>
46
+ </FlexBox>
47
+ </FlexBox>,
48
+ <FlexBox
49
+ className="lumx-generic-block"
50
+ gap="regular"
51
+ hAlign="center"
52
+ orientation="horizontal"
53
+ vAlign="center"
54
+ >
55
+ <FlexBox
56
+ className="lumx-generic-block__figure"
57
+ >
58
+ <Avatar
59
+ alt=""
60
+ image="/demo-assets/persona.png"
61
+ size="m"
62
+ theme="light"
63
+ />
64
+ </FlexBox>
65
+ <FlexBox
66
+ className="lumx-generic-block__content"
67
+ fillSpace={true}
68
+ orientation="vertical"
69
+ vAlign="left"
70
+ >
71
+ <h2>
72
+ Content title
73
+ </h2>
74
+ <p>
75
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec rhoncus libero aliquet pharetra luctus. Fusce nisl turpis, posuere ac tellus at, euismod vulputate libero...
76
+ </p>
77
+ </FlexBox>
78
+ <FlexBox
79
+ className="lumx-generic-block__actions"
80
+ vAlign="right"
81
+ >
82
+ <Button
83
+ emphasis="high"
84
+ size="m"
85
+ theme="light"
86
+ >
87
+ Actions
88
+ </Button>
89
+ </FlexBox>
90
+ </FlexBox>,
91
+ ]
92
+ `;
@@ -0,0 +1 @@
1
+ export * from './GenericBlock';
@@ -87,7 +87,7 @@ export const Lightbox: Comp<LightboxProps, HTMLDivElement> = forwardRef((props,
87
87
 
88
88
  // Handle focus trap.
89
89
  // eslint-disable-next-line react-hooks/rules-of-hooks
90
- useFocusTrap(wrapperRef.current, childrenRef.current?.firstChild);
90
+ useFocusTrap(isOpen && wrapperRef.current, childrenRef.current?.firstChild);
91
91
 
92
92
  // eslint-disable-next-line react-hooks/rules-of-hooks
93
93
  const previousOpen = useRef(isOpen);
@@ -5,7 +5,7 @@ import 'jest-enzyme';
5
5
 
6
6
  import { commonTestsSuite, Wrapper } from '@lumx/react/testing/utils';
7
7
  import { getBasicClass } from '@lumx/react/utils';
8
- import { Thumbnail } from '@lumx/react';
8
+ import { Link, Thumbnail } from '@lumx/react';
9
9
 
10
10
  import { Size, Theme } from '..';
11
11
  import { LinkPreview, LinkPreviewProps } from './LinkPreview';
@@ -25,81 +25,76 @@ const setup = (propsOverride: SetupProps = {}, shallowRendering = true) => {
25
25
 
26
26
  return {
27
27
  thumbnail: wrapper.find(Thumbnail),
28
+ title: wrapper.find(`.${CLASSNAME}__title`),
29
+ description: wrapper.find(`.${CLASSNAME}__description`),
30
+ link: wrapper.find(`.${CLASSNAME}__link`).find(Link),
28
31
  props,
29
32
  wrapper,
30
33
  };
31
34
  };
32
35
 
33
36
  describe(`<${LinkPreview.displayName}>`, () => {
34
- // 1. Test render via snapshot (default states of component).
35
- describe('Snapshots and structure', () => {
36
- // Here is an example of a basic rendering check, with snapshot.
37
+ it('should render with default props', () => {
38
+ const { wrapper, thumbnail, title, link, description } = setup();
39
+ expect(wrapper).toHaveClassName(CLASSNAME);
37
40
 
38
- it('should render correctly', () => {
39
- const { wrapper } = setup();
40
- expect(wrapper).toMatchSnapshot();
41
-
42
- expect(wrapper).toExist();
43
- expect(wrapper).toHaveClassName(CLASSNAME);
41
+ ['size', 'theme'].forEach((type) => {
42
+ expect(wrapper).toHaveClassName(getBasicClass({ prefix: CLASSNAME, type, value: DEFAULT_PROPS[type] }));
44
43
  });
44
+ expect(thumbnail).not.toExist();
45
+ expect(title).not.toExist();
46
+ expect(link).toExist();
47
+ expect(link).toHaveProp('tabIndex', undefined);
48
+ expect(description).not.toExist();
49
+ });
45
50
 
46
- it('should render correctly', () => {
47
- const { wrapper } = setup({ size: Size.big });
48
- expect(wrapper).toMatchSnapshot();
51
+ it('should render with only the title', () => {
52
+ const { title, link } = setup({ title: 'Title' });
49
53
 
50
- expect(wrapper).toExist();
51
- expect(wrapper).toHaveClassName(CLASSNAME);
52
- });
54
+ expect(title).toExist();
55
+ expect(link).toExist();
56
+ expect(link).toHaveProp('tabIndex', '-1');
53
57
  });
54
58
 
55
- // 2. Test defaultProps value and important props custom values.
56
- describe('Props', () => {
57
- it('should use default props', () => {
58
- const { wrapper, thumbnail } = setup();
59
- ['size', 'theme'].forEach((prop: string) => {
60
- expect(wrapper).toHaveClassName(
61
- getBasicClass({ prefix: CLASSNAME, type: prop, value: DEFAULT_PROPS[prop] }),
62
- );
63
- });
64
- expect(thumbnail).not.toExist();
59
+ it('should render with complete props', () => {
60
+ const { wrapper, thumbnail, title, link, description, props } = setup({
61
+ size: Size.big,
62
+ theme: Theme.dark,
63
+ thumbnailProps: { image: 'https://example.com/thumbnail.jpg', alt: '' },
64
+ link: 'https://example.com',
65
+ linkProps: { 'data-custom-attr': 'true' },
66
+ title: 'Title',
67
+ description: 'Description',
65
68
  });
66
69
 
67
- it('should pass className prop to the wrapper', () => {
68
- const expectedClassName = 'must-be-set';
69
- const { wrapper } = setup({ className: expectedClassName });
70
-
71
- expect(wrapper).toHaveClassName(expectedClassName);
72
- });
70
+ const validateLink = (linkElement: any) => {
71
+ expect(linkElement).toHaveProp('href', props.link);
72
+ // Props forwarding
73
+ expect(linkElement).toHaveProp('data-custom-attr', 'true');
74
+ };
73
75
 
74
- it('should set --theme-dark class variant on wrapper if theme = Theme.dark', () => {
75
- const { wrapper } = setup({ theme: Theme.dark });
76
+ expect(wrapper).toExist();
76
77
 
77
- expect(wrapper).toHaveClassName(getBasicClass({ prefix: CLASSNAME, type: 'theme', value: Theme.dark }));
78
- });
79
- });
78
+ // Thumbnail
79
+ expect(thumbnail).toExist();
80
+ validateLink((thumbnail as any).dive());
80
81
 
81
- // 3. Test events.
82
- describe('Events', () => {
83
- const expectedUrl = 'https://expected.url';
84
- const { thumbnail } = setup({
85
- link: expectedUrl,
86
- thumbnailProps: { image: 'https://expected.url/image.png', alt: 'Alt' },
87
- });
88
- window.open = jest.fn();
82
+ // Title
83
+ expect(title).toHaveText(props.title);
84
+ validateLink(title.find(Link));
89
85
 
90
- thumbnail.simulate('click');
86
+ // Link
87
+ expect(link).toHaveText(props.link);
88
+ validateLink(link.find(Link));
91
89
 
92
- expect(window.open).toHaveBeenCalledWith(expectedUrl, '_blank');
93
- });
90
+ // Description
91
+ expect(description).toHaveText(props.description);
94
92
 
95
- // 4. Test conditions (i.e. things that display or not in the UI based on props).
96
- describe('Conditions', () => {
97
- // Nothing to do here.
98
- });
93
+ // Size prop applied
94
+ expect(wrapper).toHaveClassName(getBasicClass({ prefix: CLASSNAME, type: 'size', value: props.size }));
99
95
 
100
- // 5. Test state.
101
- describe('State', () => {
102
- // Nothing to do here.
96
+ // Dark theme applied
97
+ expect(wrapper).toHaveClassName(getBasicClass({ prefix: CLASSNAME, type: 'theme', value: Theme.dark }));
103
98
  });
104
99
 
105
100
  // Common tests suite.
@@ -1,4 +1,4 @@
1
- import React, { forwardRef, useCallback } from 'react';
1
+ import React, { forwardRef } from 'react';
2
2
 
3
3
  import classNames from 'classnames';
4
4
 
@@ -14,16 +14,18 @@ import {
14
14
  ThumbnailProps,
15
15
  } from '@lumx/react';
16
16
 
17
- import { Comp, GenericProps, getRootClassName, handleBasicClasses } from '@lumx/react/utils';
17
+ import { Comp, GenericProps, getRootClassName, handleBasicClasses, HeadingElement } from '@lumx/react/utils';
18
18
 
19
19
  /**
20
20
  * Defines the props of the component.
21
21
  */
22
22
  export interface LinkPreviewProps extends GenericProps {
23
- /** Description (either a string, or sanitized html). */
24
- description?: string | { __html: string };
23
+ /** Description. */
24
+ description?: string;
25
25
  /** Link URL. */
26
26
  link: string;
27
+ /** Custom react component for the link (can be used to inject react router Link). */
28
+ linkAs?: 'a' | any;
27
29
  /** Props to pass to the link (minus those already set by the LinkPreview props). */
28
30
  linkProps?: Omit<LinkProps, 'color' | 'colorVariant' | 'href' | 'target'>;
29
31
  /** Size variant. */
@@ -34,6 +36,8 @@ export interface LinkPreviewProps extends GenericProps {
34
36
  thumbnailProps?: ThumbnailProps;
35
37
  /** Title. */
36
38
  title?: string;
39
+ /** Customize the title heading tag. */
40
+ titleHeading?: HeadingElement;
37
41
  }
38
42
 
39
43
  /**
@@ -49,10 +53,11 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
49
53
  /**
50
54
  * Component default props.
51
55
  */
52
- const DEFAULT_PROPS: Partial<LinkPreviewProps> = {
56
+ const DEFAULT_PROPS = {
53
57
  size: Size.regular,
54
58
  theme: Theme.light,
55
- };
59
+ titleHeading: 'h2',
60
+ } as const;
56
61
 
57
62
  /**
58
63
  * LinkPreview component.
@@ -62,13 +67,24 @@ const DEFAULT_PROPS: Partial<LinkPreviewProps> = {
62
67
  * @return React element.
63
68
  */
64
69
  export const LinkPreview: Comp<LinkPreviewProps, HTMLDivElement> = forwardRef((props, ref) => {
65
- const { className, description, link, linkProps, size, theme, thumbnailProps, title, ...forwardedProps } = props;
66
-
67
- //TODO: a11y
68
- const goToUrl = useCallback(() => window.open(link, '_blank'), [link]);
70
+ const {
71
+ className,
72
+ description,
73
+ link,
74
+ linkAs,
75
+ linkProps,
76
+ size,
77
+ theme,
78
+ thumbnailProps,
79
+ title,
80
+ titleHeading,
81
+ ...forwardedProps
82
+ } = props;
83
+ // Use title heading as title wrapper (see DEFAULT_PROPS for the default value).
84
+ const TitleHeading = titleHeading as HeadingElement;
69
85
 
70
86
  return (
71
- <div
87
+ <article
72
88
  ref={ref}
73
89
  {...forwardedProps}
74
90
  className={classNames(
@@ -85,8 +101,14 @@ export const LinkPreview: Comp<LinkPreviewProps, HTMLDivElement> = forwardRef((p
85
101
  <div className={`${CLASSNAME}__thumbnail`}>
86
102
  <Thumbnail
87
103
  {...thumbnailProps}
88
- onClick={goToUrl}
89
- role="link"
104
+ linkAs={linkAs}
105
+ linkProps={{
106
+ ...linkProps,
107
+ href: link,
108
+ target: '_blank',
109
+ // Avoid redundant links in focus order
110
+ tabIndex: -1,
111
+ }}
90
112
  aspectRatio={AspectRatio.free}
91
113
  fillHeight
92
114
  />
@@ -95,9 +117,10 @@ export const LinkPreview: Comp<LinkPreviewProps, HTMLDivElement> = forwardRef((p
95
117
 
96
118
  <div className={`${CLASSNAME}__container`}>
97
119
  {title && (
98
- <div className={`${CLASSNAME}__title`}>
120
+ <TitleHeading className={`${CLASSNAME}__title`}>
99
121
  <Link
100
122
  {...linkProps}
123
+ linkAs={linkAs}
101
124
  target="_blank"
102
125
  href={link}
103
126
  color={theme === Theme.light ? ColorPalette.dark : ColorPalette.light}
@@ -105,25 +128,29 @@ export const LinkPreview: Comp<LinkPreviewProps, HTMLDivElement> = forwardRef((p
105
128
  >
106
129
  {title}
107
130
  </Link>
108
- </div>
131
+ </TitleHeading>
109
132
  )}
133
+
110
134
  {description && <p className={`${CLASSNAME}__description`}>{description}</p>}
111
135
 
112
136
  <div className={`${CLASSNAME}__link`}>
113
137
  <Link
114
138
  {...linkProps}
139
+ linkAs={linkAs}
115
140
  className={classNames(`${CLASSNAME}__link`, linkProps?.className)}
116
141
  target="_blank"
117
142
  href={link}
118
143
  color={theme === Theme.light ? ColorPalette.primary : ColorPalette.light}
119
144
  colorVariant={ColorVariant.N}
145
+ // Avoid redundant links in focus order
146
+ tabIndex={title ? '-1' : undefined}
120
147
  >
121
148
  {link}
122
149
  </Link>
123
150
  </div>
124
151
  </div>
125
152
  </div>
126
- </div>
153
+ </article>
127
154
  );
128
155
  });
129
156
 
@@ -1,5 +1,5 @@
1
1
  import { detectOverflow } from '@popperjs/core';
2
- import React, { forwardRef, ReactNode, RefObject, useEffect, useMemo, useRef, useState } from 'react';
2
+ import React, { forwardRef, ReactNode, RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
4
  import { usePopper } from 'react-popper';
5
5
 
@@ -13,6 +13,7 @@ import { ClickAwayProvider } from '@lumx/react/utils/ClickAwayProvider';
13
13
  import { Comp, GenericProps, getRootClassName, handleBasicClasses, ValueOf } from '@lumx/react/utils';
14
14
  import { mergeRefs } from '@lumx/react/utils/mergeRefs';
15
15
  import { useFocusWithin } from '@lumx/react/hooks/useFocusWithin';
16
+ import { getFirstAndLastFocusable } from '@lumx/react/utils/focus/getFirstAndLastFocusable';
16
17
 
17
18
  /**
18
19
  * Different possible placements for the popover.
@@ -90,6 +91,8 @@ export interface PopoverProps extends GenericProps {
90
91
  isOpen: boolean;
91
92
  /** Offset placement relative to anchor. */
92
93
  offset?: Offset;
94
+ /** Reference to the parent element that triggered the popover (will get back focus on close or else fallback on the anchor element). */
95
+ parentElement?: RefObject<HTMLElement>;
93
96
  /** Placement relative to anchor. */
94
97
  placement?: Placement;
95
98
  /** Whether the popover should be rendered into a DOM node that exists outside the DOM hierarchy of the parent component. */
@@ -212,6 +215,7 @@ export const Popover: Comp<PopoverProps, HTMLDivElement> = forwardRef((props, re
212
215
  isOpen,
213
216
  offset,
214
217
  onClose,
218
+ parentElement,
215
219
  placement,
216
220
  style,
217
221
  usePortal,
@@ -245,7 +249,8 @@ export const Popover: Comp<PopoverProps, HTMLDivElement> = forwardRef((props, re
245
249
  });
246
250
 
247
251
  /** Action on close */
248
- const handleClose = () => {
252
+ // eslint-disable-next-line react-hooks/rules-of-hooks
253
+ const handleClose = useCallback(() => {
249
254
  if (!onClose) {
250
255
  return;
251
256
  }
@@ -256,11 +261,22 @@ export const Popover: Comp<PopoverProps, HTMLDivElement> = forwardRef((props, re
256
261
  * unless specifically requested not to.
257
262
  */
258
263
  if (isFocusedWithin.current && focusAnchorOnClose) {
259
- anchorRef.current?.focus();
264
+ if (parentElement?.current) {
265
+ parentElement?.current.focus();
266
+ }
267
+
268
+ const firstFocusable = anchorRef?.current && getFirstAndLastFocusable(anchorRef?.current).first;
269
+ if (firstFocusable) {
270
+ // Focus the first focusable element in anchor.
271
+ firstFocusable.focus();
272
+ } else {
273
+ // Fallback on the anchor element.
274
+ anchorRef?.current?.focus();
275
+ }
260
276
  }
261
277
 
262
278
  onClose();
263
- };
279
+ }, [anchorRef, focusAnchorOnClose, onClose, parentElement]);
264
280
 
265
281
  const modifiers: any = [];
266
282
  const actualOffset: [number, number] = [offset?.along ?? 0, (offset?.away ?? 0) + (hasArrow ? ARROW_SIZE : 0)];
@@ -1,3 +1,4 @@
1
+ import { mdiBullhornOutline } from '@lumx/icons/';
1
2
  import { List, ListItem, Select, Size, TextField } from '@lumx/react';
2
3
  import { useBooleanState } from '@lumx/react/hooks/useBooleanState';
3
4
  import { text } from '@storybook/addon-knobs';
@@ -32,6 +33,7 @@ export const SimpleSelect = ({ theme }: any) => {
32
33
  theme={theme}
33
34
  onInputClick={toggleSelect}
34
35
  onDropdownClose={closeSelect}
36
+ icon={mdiBullhornOutline}
35
37
  >
36
38
  <List isClickable>
37
39
  {CHOICES.length > 0
@@ -5,7 +5,7 @@ import lodashIsEmpty from 'lodash/isEmpty';
5
5
 
6
6
  import { mdiAlertCircle, mdiCheckCircle, mdiCloseCircle, mdiMenuDown } from '@lumx/icons';
7
7
 
8
- import { Emphasis, Size } from '@lumx/react/components';
8
+ import { Emphasis, Size, Theme } from '@lumx/react/components';
9
9
  import { IconButton } from '@lumx/react/components/button/IconButton';
10
10
  import { Chip } from '@lumx/react/components/chip/Chip';
11
11
  import { Icon } from '@lumx/react/components/icon/Icon';
@@ -46,6 +46,7 @@ const SelectField: React.FC<SelectProps> = ({
46
46
  handleKeyboardNav,
47
47
  hasError,
48
48
  hasInputClear,
49
+ icon,
49
50
  id,
50
51
  isDisabled,
51
52
  isEmpty,
@@ -89,6 +90,15 @@ const SelectField: React.FC<SelectProps> = ({
89
90
  aria-disabled={isDisabled || undefined}
90
91
  {...forwardedProps}
91
92
  >
93
+ {icon && (
94
+ <Icon
95
+ className={`${CLASSNAME}__input-icon`}
96
+ color={theme === Theme.dark ? 'light' : undefined}
97
+ icon={icon}
98
+ size={Size.xs}
99
+ />
100
+ )}
101
+
92
102
  <div
93
103
  className={classNames([
94
104
  `${CLASSNAME}__input-native`,
@@ -1,4 +1,5 @@
1
1
  /* istanbul ignore file */
2
+ import { mdiTram } from '@lumx/icons/';
2
3
  import { Chip, List, ListItem, SelectMultiple, Size } from '@lumx/react';
3
4
  import { useBooleanState } from '@lumx/react/hooks/useBooleanState';
4
5
  import noop from 'lodash/noop';
@@ -40,6 +41,7 @@ export const DefaultSelectMultiple = ({ theme }: any) => {
40
41
  theme={theme}
41
42
  onInputClick={toggleSelect}
42
43
  onDropdownClose={closeSelect}
44
+ icon={mdiTram}
43
45
  >
44
46
  <List isClickable>
45
47
  {CHOICES.length > 0
@@ -4,7 +4,7 @@ import classNames from 'classnames';
4
4
 
5
5
  import { mdiAlertCircle, mdiCheckCircle, mdiClose, mdiCloseCircle, mdiMenuDown } from '@lumx/icons';
6
6
 
7
- import { Size } from '@lumx/react/components';
7
+ import { Size, Theme } from '@lumx/react/components';
8
8
  import { Chip } from '@lumx/react/components/chip/Chip';
9
9
  import { Icon } from '@lumx/react/components/icon/Icon';
10
10
  import { InputLabel } from '@lumx/react/components/input-label/InputLabel';
@@ -59,6 +59,7 @@ export const SelectMultipleField: React.FC<SelectMultipleProps> = ({
59
59
  anchorRef,
60
60
  handleKeyboardNav,
61
61
  hasError,
62
+ icon,
62
63
  id,
63
64
  isDisabled,
64
65
  isEmpty,
@@ -102,6 +103,15 @@ export const SelectMultipleField: React.FC<SelectMultipleProps> = ({
102
103
  aria-disabled={isDisabled || undefined}
103
104
  {...forwardedProps}
104
105
  >
106
+ {icon && (
107
+ <Icon
108
+ className={`${CLASSNAME}__input-icon`}
109
+ color={theme === Theme.dark ? 'light' : undefined}
110
+ icon={icon}
111
+ size={Size.xs}
112
+ />
113
+ )}
114
+
105
115
  <div className={`${CLASSNAME}__chips`}>
106
116
  {!isEmpty &&
107
117
  value.map((val, index) => selectedChipRender?.(val, index, onClear, isDisabled, theme))}