@lumx/react 3.6.2 → 3.6.3-alpha.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
@@ -7,8 +7,8 @@
7
7
  },
8
8
  "dependencies": {
9
9
  "@juggle/resize-observer": "^3.2.0",
10
- "@lumx/core": "^3.6.2",
11
- "@lumx/icons": "^3.6.2",
10
+ "@lumx/core": "^3.6.3-alpha.1",
11
+ "@lumx/icons": "^3.6.3-alpha.1",
12
12
  "@popperjs/core": "^2.5.4",
13
13
  "body-scroll-lock": "^3.1.5",
14
14
  "classnames": "^2.3.2",
@@ -72,8 +72,7 @@
72
72
  "typescript": "^4.1.2",
73
73
  "vite": "^4.2.1",
74
74
  "vite-tsconfig-paths": "^4.0.7",
75
- "yargs": "^15.4.1",
76
- "yarn": "^1.19.1"
75
+ "yargs": "^15.4.1"
77
76
  },
78
77
  "peerDependencies": {
79
78
  "lodash": "4.17.21",
@@ -113,5 +112,5 @@
113
112
  "build:storybook": "storybook build"
114
113
  },
115
114
  "sideEffects": false,
116
- "version": "3.6.2"
115
+ "version": "3.6.3-alpha.1"
117
116
  }
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
 
3
3
  import { Kind } from '@lumx/react';
4
- import { render, within } from '@testing-library/react';
4
+ import { render, within, screen } from '@testing-library/react';
5
5
  import { queryByClassName } from '@lumx/react/testing/utils/queries';
6
6
  import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
7
7
  import userEvent from '@testing-library/user-event';
@@ -67,6 +67,18 @@ describe(`<${Notification.displayName}>`, () => {
67
67
  expect(onClick).toHaveBeenCalled();
68
68
  });
69
69
 
70
+ it('should render outside a portal', () => {
71
+ const parentId = '123';
72
+ render(
73
+ <div data-testid={parentId}>
74
+ <Notification isOpen type="info" usePortal={false} />
75
+ </div>,
76
+ );
77
+ const parent = screen.getByTestId(parentId);
78
+ const notification = queryByClassName(parent, CLASSNAME);
79
+ expect(notification).toBeInTheDocument();
80
+ });
81
+
70
82
  // Common tests suite.
71
83
  commonTestsSuiteRTL(setup, {
72
84
  baseClassName: CLASSNAME,
@@ -33,6 +33,8 @@ export interface NotificationProps extends GenericProps, HasTheme {
33
33
  onActionClick?(): void;
34
34
  /** On click callback. */
35
35
  onClick?(): void;
36
+ /** Whether the notification should be rendered into a DOM node that exists outside the DOM hierarchy of the parent component. */
37
+ usePortal?: boolean;
36
38
  }
37
39
 
38
40
  /**
@@ -51,6 +53,7 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
51
53
  const DEFAULT_PROPS: Partial<NotificationProps> = {
52
54
  theme: Theme.light,
53
55
  zIndex: 9999,
56
+ usePortal: true,
54
57
  };
55
58
 
56
59
  /* eslint-disable react-hooks/rules-of-hooks, jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
@@ -72,6 +75,7 @@ export const Notification: Comp<NotificationProps, HTMLDivElement> = forwardRef(
72
75
  theme,
73
76
  type,
74
77
  zIndex,
78
+ usePortal,
75
79
  ...forwardedProps
76
80
  } = props;
77
81
  if (!DOCUMENT) {
@@ -90,40 +94,43 @@ export const Notification: Comp<NotificationProps, HTMLDivElement> = forwardRef(
90
94
  evt.stopPropagation();
91
95
  };
92
96
 
93
- return type && isVisible
94
- ? createPortal(
95
- // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
96
- <div
97
- ref={mergeRefs(ref, rootRef)}
98
- role="alert"
99
- {...forwardedProps}
100
- className={classNames(
101
- className,
102
- handleBasicClasses({
103
- color,
104
- hasAction,
105
- isHidden: !isOpen,
106
- prefix: CLASSNAME,
107
- }),
108
- )}
109
- onClick={onClick}
110
- style={{ zIndex }}
111
- >
112
- <div className={`${CLASSNAME}__icon`}>
113
- <Icon icon={icon} size={Size.s} />
114
- </div>
115
- <div className={`${CLASSNAME}__content`}>{content}</div>
116
- {hasAction && (
117
- <div className={`${CLASSNAME}__action`}>
118
- <Button emphasis={Emphasis.medium} theme={theme} onClick={handleCallback}>
119
- <span>{actionLabel}</span>
120
- </Button>
121
- </div>
122
- )}
123
- </div>,
124
- document.body,
125
- )
126
- : null;
97
+ if (!type || !isVisible) {
98
+ return null;
99
+ }
100
+
101
+ const notification = (
102
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
103
+ <div
104
+ ref={mergeRefs(ref, rootRef)}
105
+ role="alert"
106
+ {...forwardedProps}
107
+ className={classNames(
108
+ className,
109
+ handleBasicClasses({
110
+ color,
111
+ hasAction,
112
+ isHidden: !isOpen,
113
+ prefix: CLASSNAME,
114
+ }),
115
+ )}
116
+ onClick={onClick}
117
+ style={{ zIndex }}
118
+ >
119
+ <div className={`${CLASSNAME}__icon`}>
120
+ <Icon icon={icon} size={Size.s} />
121
+ </div>
122
+ <div className={`${CLASSNAME}__content`}>{content}</div>
123
+ {hasAction && (
124
+ <div className={`${CLASSNAME}__action`}>
125
+ <Button emphasis={Emphasis.medium} theme={theme} onClick={handleCallback}>
126
+ <span>{actionLabel}</span>
127
+ </Button>
128
+ </div>
129
+ )}
130
+ </div>
131
+ );
132
+
133
+ return usePortal ? createPortal(notification, document.body) : notification;
127
134
  });
128
135
  Notification.displayName = COMPONENT_NAME;
129
136
  Notification.className = CLASSNAME;
@@ -80,3 +80,13 @@ export const InfoWithAction = {
80
80
  isOpen: true,
81
81
  },
82
82
  };
83
+
84
+ /**
85
+ * Info notification rendered outside a React portal
86
+ */
87
+ export const InfoWithoutPortal = {
88
+ args: {
89
+ ...Info.args,
90
+ usePortal: false,
91
+ },
92
+ };
@@ -29,6 +29,12 @@ export default {
29
29
  hasArrow: { control: 'boolean' },
30
30
  placement: getSelectArgType(Placement),
31
31
  elevation: getSelectArgType<Elevation>([1, 2, 3, 4, 5]),
32
+ ref: { control: false },
33
+ parentElement: { control: false },
34
+ focusElement: { control: false },
35
+ anchorRef: { control: false },
36
+ boundaryRef: { control: false },
37
+ children: { control: false },
32
38
  },
33
39
  decorators: [
34
40
  // Force minimum chromatic screen size to make sure the dialog appears in view.
@@ -60,6 +66,22 @@ export const Simple = {
60
66
  },
61
67
  };
62
68
 
69
+ /**
70
+ * Dark theme
71
+ */
72
+ export const DarkTheme = {
73
+ ...Simple,
74
+ args: {
75
+ theme: 'dark',
76
+ children: (
77
+ <Text as="p" color="light" className="lumx-spacing-padding-big">
78
+ Popover
79
+ </Text>
80
+ ),
81
+ hasArrow: true,
82
+ },
83
+ };
84
+
63
85
  /**
64
86
  * Popover not using React.createPortal
65
87
  */
@@ -7,7 +7,7 @@ import { useCallbackOnEscape } from '@lumx/react/hooks/useCallbackOnEscape';
7
7
  import { useFocus } from '@lumx/react/hooks/useFocus';
8
8
  import { ClickAwayProvider } from '@lumx/react/utils/ClickAwayProvider';
9
9
  import { DOCUMENT } from '@lumx/react/constants';
10
- import { Comp, GenericProps } from '@lumx/react/utils/type';
10
+ import { Comp, GenericProps, HasTheme } from '@lumx/react/utils/type';
11
11
  import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
12
12
  import { mergeRefs } from '@lumx/react/utils/mergeRefs';
13
13
  import { useFocusWithin } from '@lumx/react/hooks/useFocusWithin';
@@ -21,7 +21,7 @@ import { FitAnchorWidth, Elevation, Offset, Placement } from './constants';
21
21
  /**
22
22
  * Defines the props of the component.
23
23
  */
24
- export interface PopoverProps extends GenericProps {
24
+ export interface PopoverProps extends GenericProps, HasTheme {
25
25
  /** Reference to the DOM element used to set the position of the popover. */
26
26
  anchorRef: React.RefObject<HTMLElement>;
27
27
  /** Customize the root element. (Must accept ref forwarding and props forwarding!). */
@@ -118,6 +118,7 @@ const _InnerPopover: Comp<PopoverProps, HTMLDivElement> = forwardRef((props, ref
118
118
  offset,
119
119
  placement,
120
120
  style,
121
+ theme,
121
122
  zIndex,
122
123
  ...forwardedProps
123
124
  } = props;
@@ -203,13 +204,24 @@ const _InnerPopover: Comp<PopoverProps, HTMLDivElement> = forwardRef((props, ref
203
204
  ref={mergeRefs<HTMLDivElement>(setPopperElement, ref, clickAwayRef, contentRef)}
204
205
  className={classNames(
205
206
  className,
206
- handleBasicClasses({ prefix: CLASSNAME, elevation: Math.min(elevation || 0, 5), position }),
207
+ handleBasicClasses({
208
+ prefix: CLASSNAME,
209
+ theme,
210
+ elevation: Math.min(elevation || 0, 5),
211
+ position,
212
+ }),
207
213
  )}
208
214
  style={styles.popover}
209
215
  {...attributes.popper}
210
216
  >
211
217
  <ClickAwayProvider callback={closeOnClickAway && handleClose} childrenRefs={clickAwayRefs}>
212
- {hasArrow && <div ref={setArrowElement} className={`${CLASSNAME}__arrow`} style={styles.arrow} />}
218
+ {hasArrow && (
219
+ <div ref={setArrowElement} className={`${CLASSNAME}__arrow`} style={styles.arrow}>
220
+ <svg viewBox="0 0 14 14" aria-hidden>
221
+ <path d="M8 3.49C7.62 2.82 6.66 2.82 6.27 3.48L.04 14 14.04 14 8 3.49Z" />
222
+ </svg>
223
+ </div>
224
+ )}
213
225
  {children}
214
226
  </ClickAwayProvider>
215
227
  </Component>,
@@ -54,4 +54,4 @@ export type FitAnchorWidth = ValueOf<typeof FitAnchorWidth>;
54
54
  /**
55
55
  * Arrow size (in pixel).
56
56
  */
57
- export const ARROW_SIZE = 8;
57
+ export const ARROW_SIZE = 14;
@@ -120,7 +120,13 @@ export function usePopoverStyle({
120
120
  },
121
121
  ];
122
122
  if (hasArrow && arrowElement) {
123
- modifiers.push({ name: 'arrow', options: { element: arrowElement, padding: ARROW_SIZE } });
123
+ modifiers.push({
124
+ name: 'arrow',
125
+ options: {
126
+ element: arrowElement,
127
+ padding: ARROW_SIZE / 2,
128
+ },
129
+ });
124
130
  }
125
131
  if (fitToAnchorWidth) {
126
132
  const fitWidth = typeof fitToAnchorWidth === 'string' ? fitToAnchorWidth : FitAnchorWidth.MIN_WIDTH;
@@ -1,4 +1,7 @@
1
+ import isPlainObject from 'lodash/isPlainObject';
2
+
1
3
  export const getSelectArgType = <E>(options: Array<E> | Record<string, E>) => ({
2
4
  control: { type: 'select' },
3
- options,
5
+ options: Object.values(options),
6
+ mapping: isPlainObject(options) ? options : undefined,
4
7
  });