@lumx/react 3.1.5 → 3.2.1-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.
Files changed (147) hide show
  1. package/_internal/types.d.ts +16 -5
  2. package/index.d.ts +45 -4
  3. package/index.js +632 -423
  4. package/index.js.map +1 -1
  5. package/package.json +3 -3
  6. package/src/components/alert-dialog/AlertDialog.stories.tsx +96 -165
  7. package/src/components/alert-dialog/AlertDialog.test.tsx +15 -23
  8. package/src/components/avatar/Avatar.stories.tsx +91 -62
  9. package/src/components/avatar/Avatar.test.tsx +9 -24
  10. package/src/components/badge/Badge.stories.tsx +63 -38
  11. package/src/components/button/Button.stories.tsx +147 -139
  12. package/src/components/button/IconButton.stories.tsx +45 -0
  13. package/src/components/checkbox/Checkbox.stories.tsx +37 -30
  14. package/src/components/chip/Chip.stories.tsx +77 -15
  15. package/src/components/comment-block/CommentBlock.stories.tsx +90 -25
  16. package/src/components/comment-block/CommentBlock.test.tsx +12 -17
  17. package/src/components/date-picker/DatePickerField.stories.tsx +52 -83
  18. package/src/components/dialog/Dialog.stories.tsx +131 -293
  19. package/src/components/dialog/Dialog.test.tsx +0 -32
  20. package/src/components/dropdown/Dropdown.stories.tsx +1 -186
  21. package/src/components/flag/Flag.stories.tsx +33 -18
  22. package/src/components/flag/Flag.test.tsx +1 -8
  23. package/src/components/flex-box/FlexBox.stories.tsx +151 -238
  24. package/src/components/flex-box/FlexBox.test.tsx +9 -49
  25. package/src/components/generic-block/GenericBlock.stories.jsx +1 -1
  26. package/src/components/grid-column/GridColumn.stories.tsx +46 -0
  27. package/src/components/heading/Heading.stories.tsx +57 -95
  28. package/src/components/icon/Icon.stories.tsx +67 -70
  29. package/src/components/image-block/ImageBlock.stories.tsx +103 -47
  30. package/src/components/image-block/ImageBlock.test.tsx +12 -17
  31. package/src/components/inline-list/InlineList.stories.tsx +45 -29
  32. package/src/components/input-helper/InputHelper.stories.tsx +31 -25
  33. package/src/components/input-label/InputLabel.stories.tsx +33 -10
  34. package/src/components/lightbox/Lightbox.stories.tsx +39 -77
  35. package/src/components/lightbox/Lightbox.test.tsx +12 -17
  36. package/src/components/link/Link.stories.tsx +98 -128
  37. package/src/components/link-preview/LinkPreview.stories.tsx +48 -75
  38. package/src/components/list/List.stories.tsx +59 -84
  39. package/src/components/list/List.test.tsx +8 -17
  40. package/src/components/list/ListDivider.stories.tsx +9 -4
  41. package/src/components/list/ListDivider.test.tsx +12 -17
  42. package/src/components/list/ListItem.stories.tsx +97 -59
  43. package/src/components/list/ListItem.test.tsx +12 -17
  44. package/src/components/list/ListSubheader.stories.tsx +8 -5
  45. package/src/components/list/ListSubheader.test.tsx +12 -18
  46. package/src/components/message/Message.stories.tsx +51 -22
  47. package/src/components/mosaic/Mosaic.stories.tsx +78 -74
  48. package/src/components/mosaic/Mosaic.test.tsx +0 -31
  49. package/src/components/navigation/Navigation.stories.tsx +67 -0
  50. package/src/components/navigation/Navigation.test.tsx +58 -0
  51. package/src/components/navigation/Navigation.tsx +62 -0
  52. package/src/components/navigation/NavigationItem.test.tsx +37 -0
  53. package/src/components/navigation/NavigationItem.tsx +89 -0
  54. package/src/components/navigation/NavigationSection.test.tsx +126 -0
  55. package/src/components/navigation/NavigationSection.tsx +109 -0
  56. package/src/components/navigation/context.tsx +6 -0
  57. package/src/components/navigation/index.ts +1 -0
  58. package/src/components/notification/Notifications.stories.tsx +52 -47
  59. package/src/components/popover/Popover.stories.tsx +68 -201
  60. package/src/components/popover/Popover.tsx +7 -9
  61. package/src/components/popover-dialog/PopoverDialog.stories.tsx +26 -65
  62. package/src/components/post-block/PostBlock.test.tsx +12 -17
  63. package/src/components/progress/ProgressCircular.stories.tsx +24 -12
  64. package/src/components/progress/ProgressLinear.stories.tsx +6 -2
  65. package/src/components/radio-button/RadioButton.stories.tsx +35 -24
  66. package/src/components/select/Select.stories.tsx +19 -23
  67. package/src/components/select/SelectMultiple.stories.tsx +105 -2
  68. package/src/components/select/WithSelectContext.tsx +10 -4
  69. package/src/components/skeleton/SkeletonCircle.stories.tsx +37 -21
  70. package/src/components/skeleton/SkeletonCircle.test.tsx +12 -17
  71. package/src/components/skeleton/SkeletonRectangle.stories.tsx +74 -99
  72. package/src/components/skeleton/SkeletonRectangle.test.tsx +12 -17
  73. package/src/components/skeleton/SkeletonTypography.test.tsx +12 -17
  74. package/src/components/slider/Slider.stories.tsx +41 -25
  75. package/src/components/slider/Slider.test.tsx +12 -18
  76. package/src/components/slideshow/Slideshow.stories.tsx +31 -61
  77. package/src/components/slideshow/Slideshow.test.tsx +15 -23
  78. package/src/components/slideshow/SlideshowControls.stories.tsx +4 -6
  79. package/src/components/switch/Switch.stories.tsx +35 -32
  80. package/src/components/table/Table.test.tsx +12 -17
  81. package/src/components/tabs/Tabs.stories.tsx +4 -3
  82. package/src/components/text/Text.stories.tsx +130 -0
  83. package/src/components/text-field/TextField.stories.tsx +114 -148
  84. package/src/components/thumbnail/Thumbnail.stories.tsx +106 -255
  85. package/src/components/thumbnail/Thumbnail.test.tsx +12 -35
  86. package/src/components/tooltip/Tooltip.stories.tsx +51 -136
  87. package/src/components/user-block/UserBlock.stories.tsx +67 -56
  88. package/src/components/user-block/UserBlock.test.tsx +1 -5
  89. package/src/hooks/useFocusTrap.ts +2 -2
  90. package/src/index.ts +1 -0
  91. package/src/stories/controls/color.ts +6 -0
  92. package/src/stories/controls/element.ts +6 -0
  93. package/src/stories/controls/focusPoint.ts +1 -0
  94. package/src/stories/controls/icons.ts +6 -0
  95. package/src/stories/{knobs → controls}/image.ts +6 -16
  96. package/src/stories/controls/selectArgType.ts +4 -0
  97. package/src/stories/controls/theme.ts +3 -0
  98. package/src/stories/controls/typography.ts +5 -0
  99. package/src/stories/controls/withUndefined.ts +1 -0
  100. package/src/stories/decorators/withChromaticForceScreenSize.tsx +8 -0
  101. package/src/stories/decorators/withCombinations.tsx +99 -0
  102. package/src/stories/decorators/withNestedProps.tsx +23 -0
  103. package/src/stories/{withResizableBox.tsx → decorators/withResizableBox.tsx} +6 -10
  104. package/src/stories/decorators/withValueOnChange.tsx +18 -0
  105. package/src/stories/decorators/withWrapper.tsx +19 -0
  106. package/src/stories/utils/CustomLink.tsx +8 -2
  107. package/src/stories/{knobs → utils}/lorem.ts +9 -9
  108. package/src/testing/utils/commonTestsSuiteRTL.ts +2 -3
  109. package/src/testing/utils/index.ts +0 -2
  110. package/src/untypped-modules.d.ts +0 -2
  111. package/src/utils/MaterialThemeSwitcher/MaterialThemeSwitcher.tsx +1 -1
  112. package/src/utils/ThemeContext.ts +4 -0
  113. package/src/utils/forwardRefPolymorphic.ts +9 -0
  114. package/src/utils/type.ts +28 -4
  115. package/src/components/alert-dialog/__snapshots__/AlertDialog.test.tsx.snap +0 -558
  116. package/src/components/avatar/__snapshots__/Avatar.test.tsx.snap +0 -681
  117. package/src/components/comment-block/__snapshots__/CommentBlock.test.tsx.snap +0 -92
  118. package/src/components/dialog/__snapshots__/Dialog.test.tsx.snap +0 -1133
  119. package/src/components/expansion-panel/ExpansionPanel.stories.tsx +0 -65
  120. package/src/components/flag/__snapshots__/Flag.test.tsx.snap +0 -133
  121. package/src/components/flex-box/__snapshots__/FlexBox.test.tsx.snap +0 -492
  122. package/src/components/grid-column/GridColumn.stories.jsx +0 -56
  123. package/src/components/image-block/__snapshots__/ImageBlock.test.tsx.snap +0 -64
  124. package/src/components/lightbox/__snapshots__/Lightbox.test.tsx.snap +0 -194
  125. package/src/components/list/__snapshots__/List.test.tsx.snap +0 -360
  126. package/src/components/list/__snapshots__/ListDivider.test.tsx.snap +0 -7
  127. package/src/components/list/__snapshots__/ListItem.test.tsx.snap +0 -160
  128. package/src/components/list/__snapshots__/ListSubheader.test.tsx.snap +0 -9
  129. package/src/components/mosaic/__snapshots__/Mosaic.test.tsx.snap +0 -357
  130. package/src/components/post-block/__snapshots__/PostBlock.test.tsx.snap +0 -139
  131. package/src/components/skeleton/__snapshots__/SkeletonCircle.test.tsx.snap +0 -54
  132. package/src/components/skeleton/__snapshots__/SkeletonRectangle.test.tsx.snap +0 -177
  133. package/src/components/skeleton/__snapshots__/SkeletonTypography.test.tsx.snap +0 -174
  134. package/src/components/slider/__snapshots__/Slider.test.tsx.snap +0 -122
  135. package/src/components/slideshow/__snapshots__/Slideshow.test.tsx.snap +0 -157
  136. package/src/components/table/__snapshots__/Table.test.tsx.snap +0 -263
  137. package/src/components/text/Text.stories.jsx +0 -75
  138. package/src/components/thumbnail/__snapshots__/Thumbnail.test.tsx.snap +0 -130
  139. package/src/components/user-block/__snapshots__/UserBlock.test.tsx.snap +0 -362
  140. package/src/stories/chromaticForceScreenSize.tsx +0 -7
  141. package/src/stories/knobs/buttonKnob.ts +0 -9
  142. package/src/stories/knobs/emphasisKnob.ts +0 -8
  143. package/src/stories/knobs/enumKnob.ts +0 -14
  144. package/src/stories/knobs/focusKnob.ts +0 -3
  145. package/src/stories/knobs/sizeKnob.ts +0 -5
  146. package/src/stories/knobs/thumbnailsKnob.ts +0 -9
  147. package/src/testing/utils/itShouldRenderStories.tsx +0 -103
@@ -1,85 +1,89 @@
1
- import React, { useCallback, useRef, useState } from 'react';
2
-
3
- import { Alignment, ImageBlock, Lightbox, Slideshow, SlideshowItem, Theme } from '@lumx/react';
4
- import { boolean, number } from '@storybook/addon-knobs';
5
- import { thumbnailsKnob } from '@lumx/react/stories/knobs/thumbnailsKnob';
1
+ import { withWrapper } from '@lumx/react/stories/decorators/withWrapper';
2
+ import { IMAGES } from '@lumx/react/stories/controls/image';
6
3
  import { Mosaic } from './Mosaic';
7
4
 
8
- export default { title: 'LumX components/mosaic/Mosaic' };
5
+ export default {
6
+ title: 'LumX components/mosaic/Mosaic',
7
+ component: Mosaic,
8
+ args: Mosaic.defaultProps,
9
+ argTypes: {},
10
+ decorators: [withWrapper({ style: { width: 250 } })],
11
+ };
9
12
 
10
- export const OneThumbnail = ({ theme }: any) => (
11
- <div style={{ width: 250 }}>
12
- <Mosaic theme={theme} thumbnails={thumbnailsKnob(1)} />
13
- </div>
14
- );
13
+ export const OneThumbnail = {
14
+ args: {
15
+ thumbnails: [{ image: IMAGES.landscape1 }],
16
+ },
17
+ };
15
18
 
16
- export const TwoThumbnails = ({ theme }: any) => (
17
- <div style={{ width: 250 }}>
18
- <Mosaic theme={theme} thumbnails={thumbnailsKnob(2)} />
19
- </div>
20
- );
19
+ export const OneThumbnailClickable = {
20
+ ...OneThumbnail,
21
+ argTypes: {
22
+ onImageClick: { action: true },
23
+ },
24
+ };
21
25
 
22
- export const ThreeThumbnails = ({ theme }: any) => (
23
- <div style={{ width: 250 }}>
24
- <Mosaic theme={theme} thumbnails={thumbnailsKnob(3)} />
25
- </div>
26
- );
26
+ export const TwoThumbnail = {
27
+ args: {
28
+ thumbnails: [...OneThumbnail.args.thumbnails, { image: IMAGES.landscape2 }],
29
+ },
30
+ };
27
31
 
28
- export const FourThumbnails = ({ theme }: any) => (
29
- <div style={{ width: 250 }}>
30
- <Mosaic theme={theme} thumbnails={thumbnailsKnob(4)} />
31
- </div>
32
- );
32
+ export const TwoThumbnailClickable = {
33
+ ...TwoThumbnail,
34
+ argTypes: {
35
+ onImageClick: { action: true },
36
+ },
37
+ };
33
38
 
34
- export const FiveThumbnails = ({ theme }: any) => (
35
- <div style={{ width: 250 }}>
36
- <Mosaic theme={theme} thumbnails={thumbnailsKnob(5)} />
37
- </div>
38
- );
39
+ export const ThreeThumbnail = {
40
+ args: {
41
+ thumbnails: [...TwoThumbnail.args.thumbnails, { image: IMAGES.landscape3 }],
42
+ },
43
+ };
39
44
 
40
- export const SixThumbnails = ({ theme }: any) => {
41
- const enableSlideShow = boolean('Enable slideshow', true);
42
- const thumbnails = thumbnailsKnob(number('Number of thumbnails', 6, { min: 1, max: 6 }));
43
- const [activeIndex, setActiveIndex] = useState<number>();
44
- const lightBoxParent = useRef(null);
45
- const closeLightBox = useCallback(() => {
46
- setActiveIndex(undefined);
47
- }, [setActiveIndex]);
45
+ export const ThreeThumbnailClickable = {
46
+ ...ThreeThumbnail,
47
+ argTypes: {
48
+ onImageClick: { action: true },
49
+ },
50
+ };
48
51
 
49
- return (
50
- <div ref={lightBoxParent} style={{ width: 250 }}>
51
- <Mosaic theme={theme} onImageClick={enableSlideShow ? setActiveIndex : undefined} thumbnails={thumbnails} />
52
+ export const FourThumbnail = {
53
+ args: {
54
+ thumbnails: [...ThreeThumbnail.args.thumbnails, { image: IMAGES.portrait1 }],
55
+ },
56
+ };
57
+
58
+ export const FourThumbnailClickable = {
59
+ ...FourThumbnail,
60
+ argTypes: {
61
+ onImageClick: { action: true },
62
+ },
63
+ };
64
+
65
+ export const FiveThumbnail = {
66
+ args: {
67
+ thumbnails: [...FourThumbnail.args.thumbnails, { image: IMAGES.portrait2 }],
68
+ },
69
+ };
70
+
71
+ export const FiveThumbnailClickable = {
72
+ ...FiveThumbnail,
73
+ argTypes: {
74
+ onImageClick: { action: true },
75
+ },
76
+ };
77
+
78
+ export const SixThumbnail = {
79
+ args: {
80
+ thumbnails: [...FiveThumbnail.args.thumbnails, { image: IMAGES.portrait3 }],
81
+ },
82
+ };
52
83
 
53
- {enableSlideShow && (
54
- <Lightbox
55
- isOpen={activeIndex !== undefined}
56
- parentElement={lightBoxParent}
57
- onClose={closeLightBox}
58
- closeButtonProps={{ label: 'Close' }}
59
- >
60
- <Slideshow
61
- activeIndex={activeIndex}
62
- slideshowControlsProps={{
63
- nextButtonProps: { label: 'Next' },
64
- previousButtonProps: { label: 'Previous' },
65
- }}
66
- fillHeight
67
- theme={Theme.dark}
68
- >
69
- {thumbnails.map((thumbnail, index) => (
70
- <SlideshowItem key={`${thumbnail.alt}-${thumbnail.image}-${index}`}>
71
- <ImageBlock
72
- alt={thumbnail.alt}
73
- image={thumbnail.image}
74
- theme={Theme.dark}
75
- align={Alignment.center}
76
- fillHeight
77
- />
78
- </SlideshowItem>
79
- ))}
80
- </Slideshow>
81
- </Lightbox>
82
- )}
83
- </div>
84
- );
84
+ export const SixThumbnailClickable = {
85
+ ...SixThumbnail,
86
+ argTypes: {
87
+ onImageClick: { action: true },
88
+ },
85
89
  };
@@ -6,7 +6,6 @@ import 'jest-enzyme';
6
6
 
7
7
  import React, { ReactElement } from 'react';
8
8
  import { Theme } from '..';
9
- import * as stories from './Mosaic.stories';
10
9
 
11
10
  const CLASSNAME = Mosaic.className as string;
12
11
 
@@ -17,9 +16,6 @@ jest.mock('@lumx/react/hooks/useIntersectionObserver', () => ({
17
16
 
18
17
  type SetupProps = Partial<MosaicProps>;
19
18
 
20
- /**
21
- * Mounts the component and returns common DOM elements / data needed in multiple tests further down.
22
- */
23
19
  const setup = (propsOverride: SetupProps = {}, shallowRendering = true) => {
24
20
  const props: any = { ...propsOverride };
25
21
  const renderer: (el: ReactElement) => Wrapper = shallowRendering ? shallow : mount;
@@ -33,22 +29,6 @@ const setup = (propsOverride: SetupProps = {}, shallowRendering = true) => {
33
29
  };
34
30
 
35
31
  describe(`<${Mosaic.displayName}>`, () => {
36
- // 1. Test render via snapshot.
37
- describe('Snapshots and structure', () => {
38
- // Do snapshot render test on every stories.
39
- for (const [storyName, Story] of Object.entries(stories)) {
40
- if (typeof Story !== 'function') {
41
- continue;
42
- }
43
-
44
- it(`should render story ${storyName}`, () => {
45
- const wrapper = shallow(<Story />);
46
- expect(wrapper.find('Mosaic').dive()).toMatchSnapshot();
47
- });
48
- }
49
- });
50
-
51
- // 2. Test defaultProps value and important props custom values.
52
32
  describe('Props', () => {
53
33
  it('should pass theme prop to Thumbnails', () => {
54
34
  const expectedTheme = Theme.dark;
@@ -67,7 +47,6 @@ describe(`<${Mosaic.displayName}>`, () => {
67
47
  });
68
48
  });
69
49
 
70
- // 3. Test events.
71
50
  describe('Events', () => {
72
51
  it('should keep Thumbnail onClick', () => {
73
52
  const onClick = jest.fn();
@@ -105,16 +84,6 @@ describe(`<${Mosaic.displayName}>`, () => {
105
84
  });
106
85
  });
107
86
 
108
- // 4. Test conditions (i.e. things that display or not in the UI based on props).
109
- describe('Conditions', () => {
110
- // Nothing to do here.
111
- });
112
-
113
- // 5. Test state.
114
- describe('State', () => {
115
- // Nothing to do here.
116
- });
117
-
118
87
  // Common tests suite.
119
88
  commonTestsSuite(setup, { className: 'wrapper' }, { className: CLASSNAME });
120
89
  });
@@ -0,0 +1,67 @@
1
+ import React from 'react';
2
+
3
+ import {
4
+ mdiHome,
5
+ mdiMessageTextOutline,
6
+ mdiFolderGoogleDrive,
7
+ mdiTextBox,
8
+ mdiLink,
9
+ mdiGoogleCirclesExtended,
10
+ mdiFolder,
11
+ } from '@lumx/icons';
12
+ import { Navigation, Orientation } from '@lumx/react';
13
+ import { CustomLink } from '@lumx/react/stories/utils/CustomLink';
14
+
15
+ export default { title: 'LumX components/navigation/Navigation' };
16
+
17
+ export const Default = ({ theme, onClick, orientation }: any) => {
18
+ return (
19
+ <Navigation theme={theme} aria-label="navigation" orientation={orientation}>
20
+ <Navigation.Item isCurrentPage label="Homepage" icon={mdiHome} href="#" />
21
+ <Navigation.Item
22
+ label="Some very very very very very very very very very very very very very very very very very very very very very very very very very very very long text"
23
+ href="#"
24
+ />
25
+ <Navigation.Item
26
+ label="Custom link element"
27
+ icon={mdiMessageTextOutline}
28
+ as={CustomLink}
29
+ // `to` prop is required in CustomLink
30
+ to="#"
31
+ />
32
+ <Navigation.Item as="button" label="Button element" icon={mdiFolderGoogleDrive} onClick={onClick} />
33
+ <Navigation.Section label="Section 1" icon={mdiFolder}>
34
+ <Navigation.Item label="A content" href="#content" />
35
+ <Navigation.Item label="A button" icon={mdiLink} href="https://www.google.com" />
36
+ <Navigation.Item
37
+ label="Some very very very very very very very very very very very very very very very very very very very very very very very very very very very long text"
38
+ icon={mdiTextBox}
39
+ href="#content"
40
+ />
41
+ <Navigation.Item label="A community" icon={mdiGoogleCirclesExtended} href="#community" />
42
+ <Navigation.Section label="Section 1.1" icon={mdiFolder}>
43
+ <Navigation.Item label="A content" icon={mdiTextBox} href="#content" />
44
+ <Navigation.Item
45
+ label="Some very very very very very very very very very very very very very very very very very very very very very very very very very very very long text"
46
+ icon={mdiTextBox}
47
+ href="#content"
48
+ />
49
+ <Navigation.Item label="A link" icon={mdiLink} href="https://www.google.com" />
50
+ <Navigation.Item label="A community" icon={mdiGoogleCirclesExtended} href="#community" />
51
+ </Navigation.Section>
52
+ </Navigation.Section>
53
+ <Navigation.Section label="Section 2" icon={mdiFolder}>
54
+ <Navigation.Item label="A content" icon={mdiTextBox} href="#content" />
55
+ <Navigation.Item label="A link" icon={mdiLink} href="https://www.google.com" />
56
+ <Navigation.Item label="A community" icon={mdiGoogleCirclesExtended} href="#community" />
57
+ </Navigation.Section>
58
+ </Navigation>
59
+ );
60
+ };
61
+ Default.argTypes = { onClick: { action: true } };
62
+
63
+ export const VerticalWithSection: any = Default.bind({});
64
+ VerticalWithSection.args = { orientation: Orientation.vertical };
65
+
66
+ export const HorizontalWithSection: any = Default.bind({});
67
+ HorizontalWithSection.args = { orientation: Orientation.horizontal };
@@ -0,0 +1,58 @@
1
+ import React from 'react';
2
+
3
+ import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
4
+ import { render } from '@testing-library/react';
5
+ import { getByClassName } from '@lumx/react/testing/utils/queries';
6
+ import { Navigation, NavigationProps } from '.';
7
+ import { Orientation } from '..';
8
+
9
+ const CLASSNAME = Navigation.className as string;
10
+
11
+ type SetupProps = Partial<NavigationProps>;
12
+
13
+ /**
14
+ * Mounts the component and returns common DOM elements / data needed in multiple tests further down.
15
+ */
16
+
17
+ const setup = (propsOverride: SetupProps = {}) => {
18
+ const props = { 'aria-label': 'navigation', ...propsOverride } as any;
19
+ const { container } = render(
20
+ <Navigation {...props}>
21
+ <Navigation.Item label="A link" href="" />
22
+ <Navigation.Item label="A link" as="button" />
23
+ <Navigation.Item label="A link" href="" />
24
+ </Navigation>,
25
+ );
26
+
27
+ return {
28
+ container,
29
+ element: getByClassName(container, CLASSNAME),
30
+ props,
31
+ };
32
+ };
33
+
34
+ describe(`<${Navigation.displayName}>`, () => {
35
+ it('should render default', () => {
36
+ const { element } = setup();
37
+ expect(element).toBeInTheDocument();
38
+ expect(element).toHaveClass(CLASSNAME);
39
+ });
40
+
41
+ it('should render vertically by default', () => {
42
+ const { element } = setup();
43
+ expect(element).toHaveClass(`${CLASSNAME}--orientation-vertical`);
44
+ });
45
+
46
+ it('should render vertically when orientation is set to vertical', () => {
47
+ const { element } = setup({ orientation: Orientation.vertical });
48
+ expect(element).toHaveClass(`${CLASSNAME}--orientation-vertical`);
49
+ });
50
+
51
+ it('should render horizontally when orientation is set to horizontal', () => {
52
+ const { element } = setup({ orientation: Orientation.horizontal });
53
+ expect(element).toHaveClass(`${CLASSNAME}--orientation-horizontal`);
54
+ });
55
+
56
+ // Common tests suite.
57
+ commonTestsSuiteRTL(setup, { baseClassName: CLASSNAME, forwardClassName: 'element', forwardAttributes: 'element' });
58
+ });
@@ -0,0 +1,62 @@
1
+ import React, { forwardRef, ReactNode } from 'react';
2
+ import classNames from 'classnames';
3
+ import { HasAriaLabelOrLabelledBy, HasClassName, HasTheme } from '@lumx/react/utils/type';
4
+ import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
5
+ import { Orientation, Theme } from '@lumx/react';
6
+ import { ThemeContext } from '@lumx/react/utils/ThemeContext';
7
+ import { NavigationSection } from './NavigationSection';
8
+ import { NavigationItem } from './NavigationItem';
9
+ import { NavigationContext } from './context';
10
+
11
+ export type NavigationProps = React.ComponentProps<'nav'> &
12
+ HasClassName &
13
+ HasTheme & {
14
+ /** Content of the navigation. These components should be of type NavigationItem to be rendered */
15
+ children?: ReactNode;
16
+ orientation?: Orientation;
17
+ } & HasAriaLabelOrLabelledBy;
18
+
19
+ /**
20
+ * Component display name.
21
+ */
22
+ const COMPONENT_NAME = 'Navigation';
23
+
24
+ /**
25
+ * Component default class name and class prefix.
26
+ */
27
+ const CLASSNAME = getRootClassName(COMPONENT_NAME);
28
+
29
+ export const Navigation = Object.assign(
30
+ // eslint-disable-next-line react/display-name
31
+ forwardRef<HTMLElement, NavigationProps>((props, ref) => {
32
+ const { children, className, theme, orientation, ...forwardedProps } = props;
33
+ return (
34
+ <ThemeContext.Provider value={theme}>
35
+ <nav
36
+ className={classNames(
37
+ className,
38
+ handleBasicClasses({
39
+ prefix: CLASSNAME,
40
+ theme,
41
+ orientation,
42
+ }),
43
+ )}
44
+ ref={ref}
45
+ {...forwardedProps}
46
+ >
47
+ <NavigationContext.Provider value={{ orientation }}>
48
+ <ul className={`${CLASSNAME}__list`}>{children}</ul>
49
+ </NavigationContext.Provider>
50
+ </nav>
51
+ </ThemeContext.Provider>
52
+ );
53
+ }),
54
+ {
55
+ displayName: COMPONENT_NAME,
56
+ className: CLASSNAME,
57
+ defaultProps: { theme: Theme.light, orientation: Orientation.vertical },
58
+ // Sub components
59
+ Section: NavigationSection,
60
+ Item: NavigationItem,
61
+ },
62
+ );
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+
3
+ import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
4
+ import { render } from '@testing-library/react';
5
+ import { getByClassName } from '@lumx/react/testing/utils/queries';
6
+ import { NavigationItem, NavigationItemProps } from './NavigationItem';
7
+
8
+ const CLASSNAME = NavigationItem.className as string;
9
+
10
+ type SetupProps = Partial<NavigationItemProps>;
11
+
12
+ /**
13
+ * Mounts the component and returns common DOM elements / data needed in multiple tests further down.
14
+ */
15
+
16
+ const setup = (propsOverride: SetupProps = {}) => {
17
+ const props = { ...propsOverride };
18
+ const { container } = render(<NavigationItem label="A link" href="" {...props} />);
19
+
20
+ return {
21
+ container,
22
+ element: getByClassName(container, CLASSNAME),
23
+ link: getByClassName(container, `${CLASSNAME}__link`),
24
+ props,
25
+ };
26
+ };
27
+
28
+ describe(`<${NavigationItem.displayName}>`, () => {
29
+ it('should render default', () => {
30
+ const { element } = setup();
31
+ expect(element).toBeInTheDocument();
32
+ expect(element).toHaveClass(CLASSNAME);
33
+ });
34
+
35
+ // Common tests suite.
36
+ commonTestsSuiteRTL(setup, { baseClassName: CLASSNAME, forwardClassName: 'element', forwardAttributes: 'link' });
37
+ });
@@ -0,0 +1,89 @@
1
+ import React, { ElementType, ReactNode, useState, useContext } from 'react';
2
+ import { Icon, Placement, Size, Tooltip, Text } from '@lumx/react';
3
+ import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
4
+ import { ComponentRef, HasClassName, HasPolymorphicAs, HasTheme } from '@lumx/react/utils/type';
5
+ import classNames from 'classnames';
6
+ import { forwardRefPolymorphic } from '@lumx/react/utils/forwardRefPolymorphic';
7
+ import { ThemeContext } from '@lumx/react/utils/ThemeContext';
8
+
9
+ type BaseNavigationItemProps = {
10
+ /* Icon (SVG path). */
11
+ icon?: string;
12
+ /** Label content. */
13
+ label: ReactNode;
14
+ /** Mark as the current page link */
15
+ isCurrentPage?: boolean;
16
+ };
17
+
18
+ /** Make `href` required when `as` is `a` */
19
+ type RequiredLinkHref<E> = E extends 'a' ? { href: string } : Record<string, unknown>;
20
+
21
+ /**
22
+ * Navigation item props
23
+ */
24
+ export type NavigationItemProps<E extends ElementType = 'a'> = HasPolymorphicAs<E> &
25
+ HasTheme &
26
+ HasClassName &
27
+ BaseNavigationItemProps &
28
+ RequiredLinkHref<E>;
29
+
30
+ /**
31
+ * Component display name.
32
+ */
33
+ const COMPONENT_NAME = 'NavigationItem';
34
+
35
+ /**
36
+ * Component default class name and class prefix.
37
+ */
38
+ export const CLASSNAME = getRootClassName(COMPONENT_NAME);
39
+
40
+ export const NavigationItem = Object.assign(
41
+ forwardRefPolymorphic(<E extends ElementType = 'a'>(props: NavigationItemProps<E>, ref: ComponentRef<E>) => {
42
+ const { className, icon, label, isCurrentPage, as: Element = 'a', ...forwardedProps } = props;
43
+ const theme = useContext(ThemeContext);
44
+ const [labelElement, setLabelElement] = useState<HTMLSpanElement | null>(null);
45
+ const tooltipLabel =
46
+ typeof label === 'string' && labelElement && labelElement.offsetWidth < labelElement.scrollWidth
47
+ ? label
48
+ : null;
49
+
50
+ const buttonProps = Element === 'button' ? { type: 'button' } : {};
51
+
52
+ return (
53
+ <li
54
+ className={classNames(
55
+ className,
56
+ handleBasicClasses({
57
+ prefix: CLASSNAME,
58
+ theme,
59
+ }),
60
+ )}
61
+ >
62
+ <Tooltip label={tooltipLabel} placement={Placement.TOP}>
63
+ <Element
64
+ className={handleBasicClasses({
65
+ prefix: `${CLASSNAME}__link`,
66
+ isSelected: isCurrentPage,
67
+ })}
68
+ ref={ref}
69
+ aria-current={isCurrentPage ? 'page' : undefined}
70
+ {...buttonProps}
71
+ {...forwardedProps}
72
+ >
73
+ {icon ? (
74
+ <Icon className={`${CLASSNAME}__icon`} icon={icon} size={Size.xs} theme={theme} />
75
+ ) : null}
76
+
77
+ <Text as="span" truncate className={`${CLASSNAME}__label`} ref={setLabelElement}>
78
+ {label}
79
+ </Text>
80
+ </Element>
81
+ </Tooltip>
82
+ </li>
83
+ );
84
+ }),
85
+ {
86
+ displayName: COMPONENT_NAME,
87
+ className: CLASSNAME,
88
+ },
89
+ );
@@ -0,0 +1,126 @@
1
+ import React from 'react';
2
+
3
+ import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
4
+ import { render, screen } from '@testing-library/react';
5
+ import { getByClassName, queryByClassName } from '@lumx/react/testing/utils/queries';
6
+ import userEvent from '@testing-library/user-event';
7
+ import { NavigationItem } from './NavigationItem';
8
+ import { NavigationSection, NavigationSectionProps } from './NavigationSection';
9
+ import { NavigationContext } from './context';
10
+ import { Orientation } from '..';
11
+
12
+ const CLASSNAME = NavigationSection.className as string;
13
+
14
+ type SetupProps = Partial<NavigationSectionProps>;
15
+
16
+ /**
17
+ * Mounts the component and returns common DOM elements / data needed in multiple tests further down.
18
+ */
19
+
20
+ const setup = (propsOverride: SetupProps = {}, orientation: Orientation = Orientation.vertical) => {
21
+ const props = { ...propsOverride };
22
+ const { container } = render(
23
+ <NavigationContext.Provider value={{ orientation }}>
24
+ <NavigationSection label="Section 1" {...props}>
25
+ <NavigationItem label="A content" href="" />
26
+ <NavigationItem label="A link" href="" />
27
+ <NavigationItem label="A community" href="" />
28
+ </NavigationSection>
29
+ </NavigationContext.Provider>,
30
+ );
31
+
32
+ return {
33
+ container,
34
+ element: getByClassName(container, CLASSNAME),
35
+ query: {
36
+ button: () =>
37
+ screen.getByRole('button', {
38
+ name: /section 1/i,
39
+ }),
40
+ content: () => queryByClassName(container, `${CLASSNAME}__drawer`),
41
+ popover: () => queryByClassName(container, `${CLASSNAME}__drawer--popover`),
42
+ },
43
+ props,
44
+ };
45
+ };
46
+
47
+ describe(`<${NavigationSection.displayName}>`, () => {
48
+ it('should render default', () => {
49
+ const { element } = setup();
50
+ expect(element).toBeInTheDocument();
51
+ expect(element).toHaveClass(CLASSNAME);
52
+ });
53
+
54
+ it('should be closed by default in vertical mode', () => {
55
+ const { element, query } = setup();
56
+ expect(element).toBeInTheDocument();
57
+ expect(element).toHaveClass(CLASSNAME);
58
+ // Section is visible
59
+ expect(query.button()).toBeInTheDocument();
60
+ expect(query.button()).toHaveAttribute('aria-expanded', 'false');
61
+ // Content is not visible
62
+ expect(query.content()).not.toBeInTheDocument();
63
+ });
64
+
65
+ it('should be closed by default in horizontal mode', () => {
66
+ const { element, query } = setup({}, Orientation.horizontal);
67
+ expect(element).toBeInTheDocument();
68
+ expect(element).toHaveClass(CLASSNAME);
69
+ // Section is visible
70
+ expect(query.button()).toBeInTheDocument();
71
+ expect(query.button()).toHaveAttribute('aria-expanded', 'false');
72
+ // Content is not visible
73
+ expect(query.popover()).not.toBeInTheDocument();
74
+ });
75
+
76
+ it('should toggle on click in vertical mode', async () => {
77
+ const { query } = setup();
78
+ // Content is not visible
79
+ expect(query.content()).not.toBeInTheDocument();
80
+ // click to open
81
+ await userEvent.click(query.button() as any);
82
+ expect(query.button()).toBeInTheDocument();
83
+ expect(query.button()).toHaveAttribute('aria-expanded', 'true');
84
+ expect(query.button()).toHaveAttribute('aria-controls');
85
+ expect(query.content()).toBeInTheDocument();
86
+ expect(query.button().getAttribute('aria-controls')).toBe(query.content()?.getAttribute('id'));
87
+ // click to close
88
+ await userEvent.click(query.button() as any);
89
+ expect(query.button()).toBeInTheDocument();
90
+ expect(query.button()).toHaveAttribute('aria-expanded', 'false');
91
+ expect(query.content()).not.toBeInTheDocument();
92
+ });
93
+
94
+ it('should be in a popover and toggle on click in horizontal mode', async () => {
95
+ const { query } = setup({}, Orientation.horizontal);
96
+ // Content is not visible
97
+ expect(query.popover()).not.toBeInTheDocument();
98
+ // click to open
99
+ await userEvent.click(query.button() as any);
100
+ expect(query.button()).toBeInTheDocument();
101
+ expect(query.button()).toHaveAttribute('aria-expanded', 'true');
102
+ expect(query.button()).toHaveAttribute('aria-controls');
103
+ expect(query.popover()).toBeInTheDocument();
104
+ expect(query.button().getAttribute('aria-controls')).toBe(query.popover()?.getAttribute('id'));
105
+ // click to close
106
+ await userEvent.click(query.button() as any);
107
+ expect(query.button()).toBeInTheDocument();
108
+ expect(query.button()).toHaveAttribute('aria-expanded', 'false');
109
+ expect(query.popover()).not.toBeInTheDocument();
110
+ });
111
+
112
+ it('should also toggle on click away in horizontal mode', async () => {
113
+ const { query } = setup({}, Orientation.horizontal);
114
+ // Content is not visible
115
+ expect(query.popover()).not.toBeInTheDocument();
116
+ // click to open
117
+ await userEvent.click(query.button() as any);
118
+ expect(query.popover()).toBeInTheDocument();
119
+ // click away to close
120
+ await userEvent.click(document.body);
121
+ expect(query.popover()).not.toBeInTheDocument();
122
+ });
123
+
124
+ // Common tests suite.
125
+ commonTestsSuiteRTL(setup, { baseClassName: CLASSNAME, forwardClassName: 'element', forwardAttributes: 'element' });
126
+ });