@lumx/react 3.1.4 → 3.2.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 (144) hide show
  1. package/_internal/types.d.ts +16 -5
  2. package/index.d.ts +51 -4
  3. package/index.js +621 -417
  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 +132 -240
  19. package/src/components/dialog/Dialog.test.tsx +6 -30
  20. package/src/components/dialog/Dialog.tsx +17 -3
  21. package/src/components/dropdown/Dropdown.stories.tsx +1 -186
  22. package/src/components/flag/Flag.stories.tsx +33 -18
  23. package/src/components/flag/Flag.test.tsx +1 -8
  24. package/src/components/flex-box/FlexBox.stories.tsx +151 -238
  25. package/src/components/flex-box/FlexBox.test.tsx +9 -49
  26. package/src/components/generic-block/GenericBlock.stories.jsx +1 -1
  27. package/src/components/grid-column/GridColumn.stories.tsx +46 -0
  28. package/src/components/heading/Heading.stories.tsx +57 -95
  29. package/src/components/icon/Icon.stories.tsx +67 -70
  30. package/src/components/image-block/ImageBlock.stories.tsx +103 -47
  31. package/src/components/image-block/ImageBlock.test.tsx +12 -17
  32. package/src/components/inline-list/InlineList.stories.tsx +45 -29
  33. package/src/components/input-helper/InputHelper.stories.tsx +31 -25
  34. package/src/components/input-label/InputLabel.stories.tsx +33 -10
  35. package/src/components/lightbox/Lightbox.stories.tsx +39 -77
  36. package/src/components/lightbox/Lightbox.test.tsx +12 -17
  37. package/src/components/link/Link.stories.tsx +98 -128
  38. package/src/components/link-preview/LinkPreview.stories.tsx +48 -75
  39. package/src/components/list/List.stories.tsx +59 -84
  40. package/src/components/list/List.test.tsx +8 -17
  41. package/src/components/list/ListDivider.stories.tsx +9 -4
  42. package/src/components/list/ListDivider.test.tsx +12 -17
  43. package/src/components/list/ListItem.stories.tsx +97 -59
  44. package/src/components/list/ListItem.test.tsx +12 -17
  45. package/src/components/list/ListSubheader.stories.tsx +8 -5
  46. package/src/components/list/ListSubheader.test.tsx +12 -18
  47. package/src/components/message/Message.stories.tsx +51 -22
  48. package/src/components/mosaic/Mosaic.stories.tsx +78 -74
  49. package/src/components/mosaic/Mosaic.test.tsx +0 -31
  50. package/src/components/navigation/Navigation.stories.tsx +67 -0
  51. package/src/components/navigation/Navigation.test.tsx +58 -0
  52. package/src/components/navigation/Navigation.tsx +62 -0
  53. package/src/components/navigation/NavigationItem.test.tsx +37 -0
  54. package/src/components/navigation/NavigationItem.tsx +89 -0
  55. package/src/components/navigation/NavigationSection.test.tsx +126 -0
  56. package/src/components/navigation/NavigationSection.tsx +109 -0
  57. package/src/components/navigation/context.tsx +6 -0
  58. package/src/components/navigation/index.ts +1 -0
  59. package/src/components/notification/Notifications.stories.tsx +52 -47
  60. package/src/components/popover/Popover.stories.tsx +68 -201
  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/skeleton/SkeletonCircle.stories.tsx +37 -21
  68. package/src/components/skeleton/SkeletonCircle.test.tsx +12 -17
  69. package/src/components/skeleton/SkeletonRectangle.stories.tsx +74 -99
  70. package/src/components/skeleton/SkeletonRectangle.test.tsx +12 -17
  71. package/src/components/skeleton/SkeletonTypography.test.tsx +12 -17
  72. package/src/components/slider/Slider.stories.tsx +41 -25
  73. package/src/components/slider/Slider.test.tsx +12 -18
  74. package/src/components/slideshow/Slideshow.stories.tsx +31 -61
  75. package/src/components/slideshow/Slideshow.test.tsx +15 -23
  76. package/src/components/slideshow/SlideshowControls.stories.tsx +4 -6
  77. package/src/components/switch/Switch.stories.tsx +35 -32
  78. package/src/components/table/Table.test.tsx +12 -17
  79. package/src/components/tabs/Tabs.stories.tsx +4 -3
  80. package/src/components/text/Text.stories.tsx +130 -0
  81. package/src/components/text-field/TextField.stories.tsx +114 -148
  82. package/src/components/thumbnail/Thumbnail.stories.tsx +106 -255
  83. package/src/components/thumbnail/Thumbnail.test.tsx +12 -35
  84. package/src/components/tooltip/Tooltip.stories.tsx +51 -136
  85. package/src/components/user-block/UserBlock.stories.tsx +67 -56
  86. package/src/components/user-block/UserBlock.test.tsx +1 -5
  87. package/src/index.ts +1 -0
  88. package/src/stories/controls/color.ts +6 -0
  89. package/src/stories/controls/element.ts +6 -0
  90. package/src/stories/controls/focusPoint.ts +1 -0
  91. package/src/stories/controls/icons.ts +6 -0
  92. package/src/stories/{knobs → controls}/image.ts +6 -16
  93. package/src/stories/controls/selectArgType.ts +4 -0
  94. package/src/stories/controls/theme.ts +3 -0
  95. package/src/stories/controls/typography.ts +5 -0
  96. package/src/stories/controls/withUndefined.ts +1 -0
  97. package/src/stories/decorators/withChromaticForceScreenSize.tsx +8 -0
  98. package/src/stories/decorators/withCombinations.tsx +99 -0
  99. package/src/stories/decorators/withNestedProps.tsx +23 -0
  100. package/src/stories/{withResizableBox.tsx → decorators/withResizableBox.tsx} +6 -10
  101. package/src/stories/decorators/withValueOnChange.tsx +18 -0
  102. package/src/stories/decorators/withWrapper.tsx +19 -0
  103. package/src/stories/utils/CustomLink.tsx +8 -2
  104. package/src/stories/{knobs → utils}/lorem.ts +9 -9
  105. package/src/testing/utils/commonTestsSuiteRTL.ts +2 -3
  106. package/src/testing/utils/index.ts +0 -2
  107. package/src/untypped-modules.d.ts +0 -2
  108. package/src/utils/MaterialThemeSwitcher/MaterialThemeSwitcher.tsx +1 -1
  109. package/src/utils/ThemeContext.ts +4 -0
  110. package/src/utils/forwardRefPolymorphic.ts +9 -0
  111. package/src/utils/type.ts +28 -4
  112. package/src/components/alert-dialog/__snapshots__/AlertDialog.test.tsx.snap +0 -551
  113. package/src/components/avatar/__snapshots__/Avatar.test.tsx.snap +0 -681
  114. package/src/components/comment-block/__snapshots__/CommentBlock.test.tsx.snap +0 -92
  115. package/src/components/dialog/__snapshots__/Dialog.test.tsx.snap +0 -960
  116. package/src/components/expansion-panel/ExpansionPanel.stories.tsx +0 -65
  117. package/src/components/flag/__snapshots__/Flag.test.tsx.snap +0 -133
  118. package/src/components/flex-box/__snapshots__/FlexBox.test.tsx.snap +0 -492
  119. package/src/components/grid-column/GridColumn.stories.jsx +0 -56
  120. package/src/components/image-block/__snapshots__/ImageBlock.test.tsx.snap +0 -64
  121. package/src/components/lightbox/__snapshots__/Lightbox.test.tsx.snap +0 -194
  122. package/src/components/list/__snapshots__/List.test.tsx.snap +0 -360
  123. package/src/components/list/__snapshots__/ListDivider.test.tsx.snap +0 -7
  124. package/src/components/list/__snapshots__/ListItem.test.tsx.snap +0 -160
  125. package/src/components/list/__snapshots__/ListSubheader.test.tsx.snap +0 -9
  126. package/src/components/mosaic/__snapshots__/Mosaic.test.tsx.snap +0 -357
  127. package/src/components/post-block/__snapshots__/PostBlock.test.tsx.snap +0 -139
  128. package/src/components/skeleton/__snapshots__/SkeletonCircle.test.tsx.snap +0 -54
  129. package/src/components/skeleton/__snapshots__/SkeletonRectangle.test.tsx.snap +0 -177
  130. package/src/components/skeleton/__snapshots__/SkeletonTypography.test.tsx.snap +0 -174
  131. package/src/components/slider/__snapshots__/Slider.test.tsx.snap +0 -122
  132. package/src/components/slideshow/__snapshots__/Slideshow.test.tsx.snap +0 -157
  133. package/src/components/table/__snapshots__/Table.test.tsx.snap +0 -263
  134. package/src/components/text/Text.stories.jsx +0 -75
  135. package/src/components/thumbnail/__snapshots__/Thumbnail.test.tsx.snap +0 -130
  136. package/src/components/user-block/__snapshots__/UserBlock.test.tsx.snap +0 -362
  137. package/src/stories/chromaticForceScreenSize.tsx +0 -7
  138. package/src/stories/knobs/buttonKnob.ts +0 -9
  139. package/src/stories/knobs/emphasisKnob.ts +0 -8
  140. package/src/stories/knobs/enumKnob.ts +0 -14
  141. package/src/stories/knobs/focusKnob.ts +0 -3
  142. package/src/stories/knobs/sizeKnob.ts +0 -5
  143. package/src/stories/knobs/thumbnailsKnob.ts +0 -9
  144. 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
+ });