@lumx/react 3.11.3 → 3.12.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/package.json CHANGED
@@ -6,8 +6,8 @@
6
6
  "url": "https://github.com/lumapps/design-system/issues"
7
7
  },
8
8
  "dependencies": {
9
- "@lumx/core": "^3.11.3",
10
- "@lumx/icons": "^3.11.3",
9
+ "@lumx/core": "^3.12.0",
10
+ "@lumx/icons": "^3.12.0",
11
11
  "@popperjs/core": "^2.5.4",
12
12
  "body-scroll-lock": "^3.1.5",
13
13
  "classnames": "^2.3.2",
@@ -110,5 +110,5 @@
110
110
  "build:storybook": "storybook build"
111
111
  },
112
112
  "sideEffects": false,
113
- "version": "3.11.3"
113
+ "version": "3.12.0"
114
114
  }
@@ -6,6 +6,7 @@ import { render, screen } from '@testing-library/react';
6
6
  import { commonTestsSuiteRTL, SetupRenderOptions } from '@lumx/react/testing/utils';
7
7
  import userEvent from '@testing-library/user-event';
8
8
  import { ThemeSentinel } from '@lumx/react/testing/utils/ThemeSentinel';
9
+ import { Heading, HeadingLevelProvider } from '@lumx/react';
9
10
 
10
11
  const CLASSNAME = Dialog.className as string;
11
12
 
@@ -33,6 +34,21 @@ describe(`<${Dialog.displayName}>`, () => {
33
34
  expect(container).toHaveAttribute('aria-modal', 'true');
34
35
  });
35
36
 
37
+ it('should have reset the heading level context', () => {
38
+ setup(
39
+ // Heading inside the dialog
40
+ { children: <Heading>Title</Heading> },
41
+ {
42
+ // This level context should not affect headings inside the dialog
43
+ wrapper({ children }) {
44
+ return <HeadingLevelProvider level={3}>{children}</HeadingLevelProvider>;
45
+ },
46
+ },
47
+ );
48
+ // Heading inside should use the dialog heading level 2
49
+ expect(screen.queryByRole('heading', { name: 'Title', level: 2 })).toBeInTheDocument();
50
+ });
51
+
36
52
  describe('Events', () => {
37
53
  it('should trigger `onClose` when pressing `escape` key', async () => {
38
54
  const onClose = jest.fn();
@@ -3,7 +3,7 @@ import { createPortal } from 'react-dom';
3
3
 
4
4
  import classNames from 'classnames';
5
5
 
6
- import { Progress, ProgressVariant, Size } from '@lumx/react';
6
+ import { HeadingLevelProvider, Progress, ProgressVariant, Size } from '@lumx/react';
7
7
 
8
8
  import { DIALOG_TRANSITION_DURATION, DOCUMENT } from '@lumx/react/constants';
9
9
  import { useCallbackOnEscape } from '@lumx/react/hooks/useCallbackOnEscape';
@@ -212,67 +212,77 @@ export const Dialog = forwardRef<DialogProps, HTMLDivElement>((props, ref) => {
212
212
  >
213
213
  <div className={`${CLASSNAME}__overlay`} />
214
214
 
215
- <ThemeProvider value={undefined}>
216
- <section className={`${CLASSNAME}__container`} role="dialog" aria-modal="true" {...dialogProps}>
217
- <ClickAwayProvider
218
- callback={!shouldPreventCloseOnClickAway && onClose}
219
- childrenRefs={clickAwayRefs}
220
- parentRef={rootRef}
215
+ <HeadingLevelProvider level={2}>
216
+ <ThemeProvider value={undefined}>
217
+ <section
218
+ className={`${CLASSNAME}__container`}
219
+ role="dialog"
220
+ aria-modal="true"
221
+ {...dialogProps}
221
222
  >
222
- <div className={`${CLASSNAME}__wrapper`} ref={wrapperRef}>
223
- {(header || headerChildContent) && (
224
- <header
225
- {...headerChildProps}
226
- className={classNames(
227
- `${CLASSNAME}__header`,
228
- (forceHeaderDivider || hasTopIntersection) &&
229
- `${CLASSNAME}__header--has-divider`,
230
- headerChildProps?.className,
231
- )}
232
- >
233
- {header}
234
- {headerChildContent}
235
- </header>
236
- )}
223
+ <ClickAwayProvider
224
+ callback={!shouldPreventCloseOnClickAway && onClose}
225
+ childrenRefs={clickAwayRefs}
226
+ parentRef={rootRef}
227
+ >
228
+ <div className={`${CLASSNAME}__wrapper`} ref={wrapperRef}>
229
+ {(header || headerChildContent) && (
230
+ <header
231
+ {...headerChildProps}
232
+ className={classNames(
233
+ `${CLASSNAME}__header`,
234
+ (forceHeaderDivider || hasTopIntersection) &&
235
+ `${CLASSNAME}__header--has-divider`,
236
+ headerChildProps?.className,
237
+ )}
238
+ >
239
+ {header}
240
+ {headerChildContent}
241
+ </header>
242
+ )}
237
243
 
238
- <div ref={mergeRefs(contentRef, localContentRef)} className={`${CLASSNAME}__content`}>
239
244
  <div
240
- className={`${CLASSNAME}__sentinel ${CLASSNAME}__sentinel--top`}
241
- ref={setSentinelTop}
242
- />
243
-
244
- {content}
245
+ ref={mergeRefs(contentRef, localContentRef)}
246
+ className={`${CLASSNAME}__content`}
247
+ >
248
+ <div
249
+ className={`${CLASSNAME}__sentinel ${CLASSNAME}__sentinel--top`}
250
+ ref={setSentinelTop}
251
+ />
245
252
 
246
- <div
247
- className={`${CLASSNAME}__sentinel ${CLASSNAME}__sentinel--bottom`}
248
- ref={setSentinelBottom}
249
- />
250
- </div>
253
+ {content}
251
254
 
252
- {(footer || footerChildContent) && (
253
- <footer
254
- {...footerChildProps}
255
- className={classNames(
256
- `${CLASSNAME}__footer`,
257
- (forceFooterDivider || hasBottomIntersection) &&
258
- `${CLASSNAME}__footer--has-divider`,
259
- footerChildProps?.className,
260
- )}
261
- >
262
- {footer}
263
- {footerChildContent}
264
- </footer>
265
- )}
266
-
267
- {isLoading && (
268
- <div className={`${CLASSNAME}__progress-overlay`}>
269
- <Progress variant={ProgressVariant.circular} />
255
+ <div
256
+ className={`${CLASSNAME}__sentinel ${CLASSNAME}__sentinel--bottom`}
257
+ ref={setSentinelBottom}
258
+ />
270
259
  </div>
271
- )}
272
- </div>
273
- </ClickAwayProvider>
274
- </section>
275
- </ThemeProvider>
260
+
261
+ {(footer || footerChildContent) && (
262
+ <footer
263
+ {...footerChildProps}
264
+ className={classNames(
265
+ `${CLASSNAME}__footer`,
266
+ (forceFooterDivider || hasBottomIntersection) &&
267
+ `${CLASSNAME}__footer--has-divider`,
268
+ footerChildProps?.className,
269
+ )}
270
+ >
271
+ {footer}
272
+ {footerChildContent}
273
+ </footer>
274
+ )}
275
+
276
+ {isLoading && (
277
+ <div className={`${CLASSNAME}__progress-overlay`}>
278
+ <Progress variant={ProgressVariant.circular} />
279
+ </div>
280
+ )}
281
+ </div>
282
+ </ClickAwayProvider>
283
+ </section>
284
+ </ThemeProvider>
285
+ </HeadingLevelProvider>
276
286
  </div>,
277
287
  document.body,
278
288
  )
@@ -5,6 +5,7 @@ import { queryByClassName } from '@lumx/react/testing/utils/queries';
5
5
  import { render, screen } from '@testing-library/react';
6
6
  import { ThemeSentinel } from '@lumx/react/testing/utils/ThemeSentinel';
7
7
 
8
+ import { Heading, HeadingLevelProvider } from '@lumx/react';
8
9
  import { Lightbox, LightboxProps } from './Lightbox';
9
10
 
10
11
  const CLASSNAME = Lightbox.className as string;
@@ -22,6 +23,23 @@ const setup = (props: Partial<LightboxProps> = {}, { wrapper }: SetupRenderOptio
22
23
  };
23
24
 
24
25
  describe(`<${Lightbox.displayName}>`, () => {
26
+ it('should have reset the heading level context', () => {
27
+ setup(
28
+ {
29
+ // Heading inside the lightbox
30
+ children: <Heading>Title</Heading>,
31
+ },
32
+ {
33
+ // This level context should not affect headings inside the lightbox
34
+ wrapper({ children }) {
35
+ return <HeadingLevelProvider level={3}>{children}</HeadingLevelProvider>;
36
+ },
37
+ },
38
+ );
39
+ // Heading inside should use the lightbox heading level 2
40
+ expect(screen.queryByRole('heading', { name: 'Title', level: 2 })).toBeInTheDocument();
41
+ });
42
+
25
43
  // Common tests suite.
26
44
  commonTestsSuiteRTL(setup, {
27
45
  baseClassName: CLASSNAME,
@@ -4,7 +4,7 @@ import classNames from 'classnames';
4
4
  import { createPortal } from 'react-dom';
5
5
 
6
6
  import { mdiClose } from '@lumx/icons';
7
- import { IconButton, IconButtonProps } from '@lumx/react';
7
+ import { HeadingLevelProvider, IconButton, IconButtonProps } from '@lumx/react';
8
8
  import { DIALOG_TRANSITION_DURATION, DOCUMENT } from '@lumx/react/constants';
9
9
  import { GenericProps, HasTheme } from '@lumx/react/utils/type';
10
10
  import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
@@ -162,13 +162,15 @@ export const Lightbox = forwardRef<LightboxProps, HTMLDivElement>((props, ref) =
162
162
  />
163
163
  </div>
164
164
  )}
165
- <ThemeProvider value={undefined}>
166
- <ClickAwayProvider callback={!preventAutoClose && onClose} childrenRefs={clickAwayRefs}>
167
- <div ref={childrenRef} className={`${CLASSNAME}__wrapper`} role="presentation">
168
- {children}
169
- </div>
170
- </ClickAwayProvider>
171
- </ThemeProvider>
165
+ <HeadingLevelProvider level={2}>
166
+ <ThemeProvider value={undefined}>
167
+ <ClickAwayProvider callback={!preventAutoClose && onClose} childrenRefs={clickAwayRefs}>
168
+ <div ref={childrenRef} className={`${CLASSNAME}__wrapper`} role="presentation">
169
+ {children}
170
+ </div>
171
+ </ClickAwayProvider>
172
+ </ThemeProvider>
173
+ </HeadingLevelProvider>
172
174
  </div>,
173
175
  document.body,
174
176
  );
@@ -40,7 +40,7 @@ export const WithButtonTrigger = (props: any) => {
40
40
  /**
41
41
  * Example PopoverDialog using an icon button as a trigger
42
42
  */
43
- export const WithIconButtonTrigger = (props: any) => {
43
+ export const WithIconButtonTrigger = ({ children, ...props }: any) => {
44
44
  const anchorRef = React.useRef(null);
45
45
  const [isOpen, close, open] = useBooleanState(false);
46
46
 
@@ -57,6 +57,7 @@ export const WithIconButtonTrigger = (props: any) => {
57
57
  {...props}
58
58
  >
59
59
  <Button onClick={close}>Close</Button>
60
+ {children}
60
61
  </PopoverDialog>
61
62
  </>
62
63
  );
@@ -2,8 +2,9 @@ import React from 'react';
2
2
  import { render, screen, within } from '@testing-library/react';
3
3
  import userEvent from '@testing-library/user-event';
4
4
 
5
- import { PopoverDialog } from './PopoverDialog';
5
+ import { Heading, HeadingLevelProvider } from '@lumx/react';
6
6
  import { WithButtonTrigger, WithIconButtonTrigger } from './PopoverDialog.stories';
7
+ import { PopoverDialog } from './PopoverDialog';
7
8
 
8
9
  jest.mock('@lumx/react/utils/browser/isFocusVisible');
9
10
 
@@ -117,4 +118,23 @@ describe(`<${PopoverDialog.displayName}>`, () => {
117
118
  // Focus restored to the trigger element
118
119
  expect(triggerElement).toHaveFocus();
119
120
  });
121
+
122
+ it('should have reset the heading level context', async () => {
123
+ render(
124
+ // This level context should not affect headings inside the popover dialog
125
+
126
+ <HeadingLevelProvider level={3}>
127
+ <WithIconButtonTrigger>
128
+ {/* Heading inside the popover dialog */}
129
+ <Heading>Title</Heading>
130
+ </WithIconButtonTrigger>
131
+ </HeadingLevelProvider>,
132
+ );
133
+
134
+ // Open popover
135
+ await userEvent.click(screen.getByRole('button', { name: 'Open popover' }));
136
+
137
+ // Heading inside should use the popover dialog heading level 2
138
+ expect(screen.getByRole('heading', { name: 'Title', level: 2 })).toBeInTheDocument();
139
+ });
120
140
  });
@@ -6,6 +6,7 @@ import { HasAriaLabelOrLabelledBy } from '@lumx/react/utils/type';
6
6
  import { getRootClassName } from '@lumx/react/utils/className';
7
7
  import { forwardRef } from '@lumx/react/utils/react/forwardRef';
8
8
 
9
+ import { HeadingLevelProvider } from '@lumx/react/components/heading';
9
10
  import { Popover, PopoverProps } from '../popover/Popover';
10
11
 
11
12
  /**
@@ -65,7 +66,7 @@ export const PopoverDialog = forwardRef<PopoverDialogProps, HTMLDivElement>((pro
65
66
  closeOnEscape
66
67
  withFocusTrap
67
68
  >
68
- {children}
69
+ <HeadingLevelProvider level={2}>{children}</HeadingLevelProvider>
69
70
  </Popover>
70
71
  );
71
72
  });
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
 
3
- import { mdiStar } from '@lumx/icons';
4
- import { Badge, ColorPalette, Icon, Orientation, Size } from '@lumx/react';
3
+ import { mdiMenuDown, mdiStar } from '@lumx/icons';
4
+ import { Badge, ColorPalette, Icon, IconButton, Orientation, Size, Text } from '@lumx/react';
5
5
  import { CustomLink } from '@lumx/react/stories/utils/CustomLink';
6
6
 
7
7
  import { AVATAR_IMAGES } from '@lumx/react/stories/controls/image';
@@ -31,11 +31,38 @@ export const AvatarAndName = {
31
31
  args: { ...AvatarOnly.args, name: 'Emmitt O. Lum' },
32
32
  };
33
33
 
34
+ /** Avatar and children */
35
+ export const AvatarAndCustomName = {
36
+ args: { ...AvatarOnly.args, name: <Text as="span">Emmitt O. Lum</Text> },
37
+ };
38
+
34
39
  /** Avatar, name and secondary fields */
35
40
  export const AvatarAndNameAndSecondaryFields = {
36
41
  args: { ...AvatarAndName.args, fields: ['Creative developer', 'Denpasar'] },
37
42
  };
38
43
 
44
+ /** With Right component */
45
+ export const WithAfter = {
46
+ args: {
47
+ ...AvatarAndName.args,
48
+ fields: ['Creative developer', 'Denpasar'],
49
+ after: <IconButton label="View" icon={mdiMenuDown} emphasis="low" />,
50
+ },
51
+ };
52
+
53
+ /** With after component */
54
+ export const WithAdditionalFields = {
55
+ args: {
56
+ ...AvatarAndName.args,
57
+ fields: ['Creative developer', 'Denpasar'],
58
+ additionalFields: (
59
+ <Text as="span" typography="body1">
60
+ Works at the Toronto office
61
+ </Text>
62
+ ),
63
+ },
64
+ };
65
+
39
66
  /** Size variants */
40
67
  export const SizesAndOrientations = {
41
68
  ...AvatarAndNameAndSecondaryFields,
@@ -1,9 +1,9 @@
1
1
  import React from 'react';
2
2
 
3
3
  import { commonTestsSuiteRTL, SetupRenderOptions } from '@lumx/react/testing/utils';
4
- import { render, within } from '@testing-library/react';
4
+ import { render, within, screen } from '@testing-library/react';
5
5
  import { getByClassName, queryByClassName } from '@lumx/react/testing/utils/queries';
6
- import { Thumbnail } from '@lumx/react';
6
+ import { Text, Thumbnail } from '@lumx/react';
7
7
  import userEvent from '@testing-library/user-event';
8
8
 
9
9
  import { UserBlock, UserBlockProps } from './UserBlock';
@@ -22,8 +22,9 @@ const setup = (propsOverride: Partial<UserBlockProps> = {}, { wrapper }: SetupRe
22
22
  const avatar = queryByClassName(userBlock, `${CLASSNAME}__avatar`);
23
23
  const thumbnail = avatar && queryByClassName(avatar, Thumbnail.className as string);
24
24
  const fields = queryByClassName(userBlock, `${CLASSNAME}__fields`);
25
+ const after = queryByClassName(userBlock, `${CLASSNAME}__after`);
25
26
 
26
- return { props, userBlock, name, avatar, thumbnail, fields };
27
+ return { props, userBlock, name, avatar, thumbnail, fields, after };
27
28
  };
28
29
 
29
30
  describe(`<${UserBlock.displayName}>`, () => {
@@ -78,6 +79,17 @@ describe(`<${UserBlock.displayName}>`, () => {
78
79
  expect(within(fields as any).getByText('Field 1')).toBeInTheDocument();
79
80
  expect(within(fields as any).getByText('Field 2')).toBeInTheDocument();
80
81
  });
82
+
83
+ it('should render additional fields', () => {
84
+ setup({ additionalFields: <Text as="span">Works in Toronto</Text> });
85
+ expect(screen.queryByText(/works in toronto/i)).toBeInTheDocument();
86
+ });
87
+
88
+ it('should render after', () => {
89
+ const { after } = setup({ after: <Text as="span">After</Text> });
90
+ expect(after).toBeInTheDocument();
91
+ expect(screen.queryByText(/after/i)).toBeInTheDocument();
92
+ });
81
93
  });
82
94
 
83
95
  // Common tests suite.
@@ -32,7 +32,7 @@ export interface UserBlockProps extends GenericProps, HasTheme {
32
32
  /** Multiple action toolbar content. */
33
33
  multipleActions?: ReactNode;
34
34
  /** User name. */
35
- name?: string;
35
+ name?: React.ReactNode;
36
36
  /** Props to pass to the name block. */
37
37
  nameProps?: GenericProps;
38
38
  /** Orientation. */
@@ -47,6 +47,10 @@ export interface UserBlockProps extends GenericProps, HasTheme {
47
47
  onMouseEnter?(): void;
48
48
  /** On mouse leave callback. */
49
49
  onMouseLeave?(): void;
50
+ /** Display additional fields below the original name and fields */
51
+ additionalFields?: React.ReactNode;
52
+ /** Display an additional element after the entire component. (to the right if orientation is horizontal, at the bottom if orientation is vertical) */
53
+ after?: React.ReactNode;
50
54
  }
51
55
 
52
56
  /**
@@ -92,6 +96,9 @@ export const UserBlock = forwardRef<UserBlockProps, HTMLDivElement>((props, ref)
92
96
  simpleAction,
93
97
  size,
94
98
  theme = defaultTheme,
99
+ children,
100
+ additionalFields,
101
+ after,
95
102
  ...forwardedProps
96
103
  } = props;
97
104
  let componentSize = size;
@@ -131,7 +138,9 @@ export const UserBlock = forwardRef<UserBlockProps, HTMLDivElement>((props, ref)
131
138
  return <NameComponent {...nProps}>{name}</NameComponent>;
132
139
  }, [avatarProps, isClickable, linkAs, linkProps, name, nameProps, onClick]);
133
140
 
134
- const fieldsBlock: ReactNode = fields && componentSize !== Size.s && componentSize !== Size.xs && (
141
+ const shouldDisplayFields = componentSize !== Size.s && componentSize !== Size.xs;
142
+
143
+ const fieldsBlock: ReactNode = fields && shouldDisplayFields && (
135
144
  <div className={`${CLASSNAME}__fields`}>
136
145
  {fields.map((field: string, idx: number) => (
137
146
  <span key={idx} className={`${CLASSNAME}__field`}>
@@ -164,16 +173,18 @@ export const UserBlock = forwardRef<UserBlockProps, HTMLDivElement>((props, ref)
164
173
  theme={theme}
165
174
  />
166
175
  )}
167
- {(fields || name) && (
176
+ {(fields || name || children || additionalFields) && (
168
177
  <div className={`${CLASSNAME}__wrapper`}>
169
- {nameBlock}
178
+ {children || nameBlock}
170
179
  {fieldsBlock}
180
+ {shouldDisplayFields ? additionalFields : null}
171
181
  </div>
172
182
  )}
173
183
  {shouldDisplayActions && simpleAction && <div className={`${CLASSNAME}__action`}>{simpleAction}</div>}
174
184
  {shouldDisplayActions && multipleActions && (
175
185
  <div className={`${CLASSNAME}__actions`}>{multipleActions}</div>
176
186
  )}
187
+ {after ? <div className={`${CLASSNAME}__after`}>{after}</div> : null}
177
188
  </div>
178
189
  );
179
190
  });