@squiz/resource-browser 1.32.1-alpha.12

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 (160) hide show
  1. package/.storybook/main.ts +23 -0
  2. package/.storybook/preview-head.html +15 -0
  3. package/.storybook/preview.ts +16 -0
  4. package/build.js +21 -0
  5. package/jest.config.ts +18 -0
  6. package/lib/Icons/Generics/ArrowDown.d.ts +15 -0
  7. package/lib/Icons/Generics/ArrowDown.js +23 -0
  8. package/lib/Icons/Generics/ArrowRight.d.ts +15 -0
  9. package/lib/Icons/Generics/ArrowRight.js +23 -0
  10. package/lib/Icons/Generics/Close.d.ts +15 -0
  11. package/lib/Icons/Generics/Close.js +23 -0
  12. package/lib/Icons/Generics/GenericIconMap.d.ts +10 -0
  13. package/lib/Icons/Generics/GenericIconMap.js +14 -0
  14. package/lib/Icons/Generics/ResourceSelect.d.ts +15 -0
  15. package/lib/Icons/Generics/ResourceSelect.js +28 -0
  16. package/lib/Icons/Generics/Root.d.ts +15 -0
  17. package/lib/Icons/Generics/Root.js +23 -0
  18. package/lib/Icons/Generics/Selected.d.ts +15 -0
  19. package/lib/Icons/Generics/Selected.js +23 -0
  20. package/lib/Icons/Generics/index.d.ts +6 -0
  21. package/lib/Icons/Generics/index.js +19 -0
  22. package/lib/Icons/Icon.d.ts +47 -0
  23. package/lib/Icons/Icon.js +44 -0
  24. package/lib/Icons/MatrixResources/Audio.d.ts +15 -0
  25. package/lib/Icons/MatrixResources/Audio.js +28 -0
  26. package/lib/Icons/MatrixResources/Excel.d.ts +15 -0
  27. package/lib/Icons/MatrixResources/Excel.js +27 -0
  28. package/lib/Icons/MatrixResources/Folder.d.ts +15 -0
  29. package/lib/Icons/MatrixResources/Folder.js +24 -0
  30. package/lib/Icons/MatrixResources/GenericFile.d.ts +15 -0
  31. package/lib/Icons/MatrixResources/GenericFile.js +28 -0
  32. package/lib/Icons/MatrixResources/Image.d.ts +15 -0
  33. package/lib/Icons/MatrixResources/Image.js +26 -0
  34. package/lib/Icons/MatrixResources/MatrixResourceMap.d.ts +15 -0
  35. package/lib/Icons/MatrixResources/MatrixResourceMap.js +19 -0
  36. package/lib/Icons/MatrixResources/Page.d.ts +15 -0
  37. package/lib/Icons/MatrixResources/Page.js +30 -0
  38. package/lib/Icons/MatrixResources/Pdf.d.ts +15 -0
  39. package/lib/Icons/MatrixResources/Pdf.js +31 -0
  40. package/lib/Icons/MatrixResources/Powerpoint.d.ts +15 -0
  41. package/lib/Icons/MatrixResources/Powerpoint.js +28 -0
  42. package/lib/Icons/MatrixResources/Site.d.ts +15 -0
  43. package/lib/Icons/MatrixResources/Site.js +30 -0
  44. package/lib/Icons/MatrixResources/Video.d.ts +15 -0
  45. package/lib/Icons/MatrixResources/Video.js +24 -0
  46. package/lib/Icons/MatrixResources/Word.d.ts +17 -0
  47. package/lib/Icons/MatrixResources/Word.js +28 -0
  48. package/lib/Icons/MatrixResources/index.d.ts +11 -0
  49. package/lib/Icons/MatrixResources/index.js +29 -0
  50. package/lib/Modal/Modal.d.ts +11 -0
  51. package/lib/Modal/Modal.js +46 -0
  52. package/lib/Modal/ModalOpeningButton.d.ts +10 -0
  53. package/lib/Modal/ModalOpeningButton.js +13 -0
  54. package/lib/Modal/ModalTrigger.d.ts +9 -0
  55. package/lib/Modal/ModalTrigger.js +24 -0
  56. package/lib/PreviewPanel/PreviewModal.d.ts +11 -0
  57. package/lib/PreviewPanel/PreviewModal.js +81 -0
  58. package/lib/PreviewPanel/PreviewPanel.d.ts +16 -0
  59. package/lib/PreviewPanel/PreviewPanel.js +87 -0
  60. package/lib/PreviewPanel/details/MatrixResource.d.ts +12 -0
  61. package/lib/PreviewPanel/details/MatrixResource.js +41 -0
  62. package/lib/ResourceBreadcrumb/ResourceBreadcrumb.d.ts +9 -0
  63. package/lib/ResourceBreadcrumb/ResourceBreadcrumb.js +20 -0
  64. package/lib/ResourceItem/ResourceItem.d.ts +19 -0
  65. package/lib/ResourceItem/ResourceItem.js +26 -0
  66. package/lib/ResourceList/ResourceList.d.ts +14 -0
  67. package/lib/ResourceList/ResourceList.js +51 -0
  68. package/lib/ResourcePickerContainer/ResourcePickerContainer.d.ts +15 -0
  69. package/lib/ResourcePickerContainer/ResourcePickerContainer.js +145 -0
  70. package/lib/Skeleton/List/SkeletonList.d.ts +6 -0
  71. package/lib/Skeleton/List/SkeletonList.js +13 -0
  72. package/lib/Skeleton/ListItem/SkeletonListItem.d.ts +2 -0
  73. package/lib/Skeleton/ListItem/SkeletonListItem.js +15 -0
  74. package/lib/SourceDropdown/SourceDropdown.d.ts +9 -0
  75. package/lib/SourceDropdown/SourceDropdown.js +106 -0
  76. package/lib/SourceList/SourceList.d.ts +14 -0
  77. package/lib/SourceList/SourceList.js +58 -0
  78. package/lib/Spinner/Spinner.d.ts +8 -0
  79. package/lib/Spinner/Spinner.js +12 -0
  80. package/lib/index.css +968 -0
  81. package/lib/index.d.ts +37 -0
  82. package/lib/index.js +15 -0
  83. package/lib/uuid.d.ts +1 -0
  84. package/lib/uuid.js +8 -0
  85. package/package.json +74 -0
  86. package/postcss.config.js +11 -0
  87. package/src/Icons/Generics/ArrowDown.tsx +27 -0
  88. package/src/Icons/Generics/ArrowRight.tsx +27 -0
  89. package/src/Icons/Generics/Close.tsx +26 -0
  90. package/src/Icons/Generics/GenericIconMap.ts +14 -0
  91. package/src/Icons/Generics/ResourceSelect.tsx +40 -0
  92. package/src/Icons/Generics/Root.tsx +24 -0
  93. package/src/Icons/Generics/Selected.tsx +27 -0
  94. package/src/Icons/Generics/index.tsx +7 -0
  95. package/src/Icons/Icon.spec.tsx +62 -0
  96. package/src/Icons/Icon.stories.tsx +105 -0
  97. package/src/Icons/Icon.tsx +61 -0
  98. package/src/Icons/MatrixResources/Audio.tsx +30 -0
  99. package/src/Icons/MatrixResources/Excel.tsx +29 -0
  100. package/src/Icons/MatrixResources/Folder.tsx +29 -0
  101. package/src/Icons/MatrixResources/GenericFile.tsx +34 -0
  102. package/src/Icons/MatrixResources/Image.tsx +36 -0
  103. package/src/Icons/MatrixResources/MatrixResourceMap.ts +19 -0
  104. package/src/Icons/MatrixResources/Page.tsx +33 -0
  105. package/src/Icons/MatrixResources/Pdf.tsx +34 -0
  106. package/src/Icons/MatrixResources/Powerpoint.tsx +34 -0
  107. package/src/Icons/MatrixResources/Site.tsx +37 -0
  108. package/src/Icons/MatrixResources/Video.tsx +27 -0
  109. package/src/Icons/MatrixResources/Word.tsx +30 -0
  110. package/src/Icons/MatrixResources/index.tsx +12 -0
  111. package/src/Modal/Modal.spec.tsx +244 -0
  112. package/src/Modal/Modal.tsx +58 -0
  113. package/src/Modal/ModalContainer.stories.tsx +33 -0
  114. package/src/Modal/ModalOpeningButton.tsx +20 -0
  115. package/src/Modal/ModalTrigger.tsx +45 -0
  116. package/src/PreviewPanel/PreviewModal.spec.tsx +164 -0
  117. package/src/PreviewPanel/PreviewModal.tsx +92 -0
  118. package/src/PreviewPanel/PreviewPanel.spec.tsx +197 -0
  119. package/src/PreviewPanel/PreviewPanel.stories.tsx +61 -0
  120. package/src/PreviewPanel/PreviewPanel.tsx +123 -0
  121. package/src/PreviewPanel/details/MatrixResource.tsx +59 -0
  122. package/src/ResourceBreadcrumb/ResourceBreadcrumb.spec.tsx +76 -0
  123. package/src/ResourceBreadcrumb/ResourceBreadcrumb.stories.tsx +24 -0
  124. package/src/ResourceBreadcrumb/ResourceBreadcrumb.tsx +39 -0
  125. package/src/ResourceBreadcrumb/sample-hierarchy.json +23 -0
  126. package/src/ResourceItem/ResourceItem.spec.tsx +69 -0
  127. package/src/ResourceItem/ResourceItem.tsx +82 -0
  128. package/src/ResourceList/ResourceList.spec.tsx +196 -0
  129. package/src/ResourceList/ResourceList.stories.tsx +40 -0
  130. package/src/ResourceList/ResourceList.tsx +74 -0
  131. package/src/ResourceList/sample-resources.json +75 -0
  132. package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +706 -0
  133. package/src/ResourcePickerContainer/ResourcePickerContainer.stories.tsx +56 -0
  134. package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +224 -0
  135. package/src/Skeleton/List/SkeletonList.spec.tsx +18 -0
  136. package/src/Skeleton/List/SkeletonList.stories.tsx +15 -0
  137. package/src/Skeleton/List/SkeletonList.tsx +16 -0
  138. package/src/Skeleton/ListItem/SkeletonListItem.stories.tsx +15 -0
  139. package/src/Skeleton/ListItem/SkeletonListItem.tsx +14 -0
  140. package/src/SourceDropdown/SourceDropdown.spec.tsx +263 -0
  141. package/src/SourceDropdown/SourceDropdown.stories.tsx +36 -0
  142. package/src/SourceDropdown/SourceDropdown.tsx +175 -0
  143. package/src/SourceDropdown/sample-sources.json +110 -0
  144. package/src/SourceList/SourceList.spec.tsx +224 -0
  145. package/src/SourceList/SourceList.stories.tsx +40 -0
  146. package/src/SourceList/SourceList.tsx +93 -0
  147. package/src/SourceList/sample-sources.json +110 -0
  148. package/src/Spinner/Spinner.spec.tsx +18 -0
  149. package/src/Spinner/Spinner.stories.tsx +26 -0
  150. package/src/Spinner/Spinner.tsx +18 -0
  151. package/src/Spinner/_spinner.scss +11 -0
  152. package/src/__mocks__/JestHelpers.ts +65 -0
  153. package/src/__mocks__/jestHelpers.spec.ts +38 -0
  154. package/src/__mocks__/styleMock.ts +1 -0
  155. package/src/index.scss +7 -0
  156. package/src/index.stories.tsx +70 -0
  157. package/src/index.tsx +71 -0
  158. package/src/uuid.ts +7 -0
  159. package/tailwind.config.cjs +84 -0
  160. package/tsconfig.json +22 -0
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { useButton, AriaButtonProps } from 'react-aria';
3
+
4
+ interface ModalOpeningButtonProps extends AriaButtonProps<'button'> {
5
+ className?: string;
6
+ children?: React.ReactNode;
7
+ isDisabled?: boolean;
8
+ title?: string;
9
+ }
10
+
11
+ export default function ModalOpeningButton({ className, children, title, ...props }: ModalOpeningButtonProps) {
12
+ const ref = React.useRef<HTMLButtonElement>(null);
13
+
14
+ const { buttonProps } = useButton(props, ref);
15
+ return (
16
+ <button {...buttonProps} ref={ref} className={className} title={title}>
17
+ {children}
18
+ </button>
19
+ );
20
+ }
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+
3
+ import { useOverlayTrigger } from 'react-aria';
4
+ import { useOverlayTriggerState } from 'react-stately';
5
+ import { DOMAttributes, FocusableElement } from '@react-types/shared';
6
+
7
+ import Modal from './Modal';
8
+ import ModalOpeningButton from './ModalOpeningButton';
9
+
10
+ function ModalTrigger({
11
+ label,
12
+ showLabel,
13
+ icon,
14
+ children,
15
+ ...props
16
+ }: {
17
+ label: string;
18
+ showLabel?: boolean;
19
+ icon?: React.ReactNode;
20
+ children: (onClose: () => void, titleProps: DOMAttributes<FocusableElement>) => React.ReactElement; // Child needs to be a functions which generates the 'content' so we can pass the onClose function and title aria props
21
+ }) {
22
+ const state = useOverlayTriggerState(props);
23
+ const { triggerProps, overlayProps } = useOverlayTrigger({ type: 'dialog' }, state);
24
+
25
+ let ariaAttr: React.AriaAttributes = {};
26
+ if (!showLabel) {
27
+ ariaAttr = { ...ariaAttr, 'aria-label': label };
28
+ }
29
+
30
+ return (
31
+ <>
32
+ <ModalOpeningButton type="button" {...triggerProps} {...ariaAttr}>
33
+ {showLabel && label}
34
+ {icon}
35
+ </ModalOpeningButton>
36
+ {state.isOpen && (
37
+ <Modal isDismissable state={state} overlayProps={overlayProps}>
38
+ {(titleProps) => children(state.close, titleProps)}
39
+ </Modal>
40
+ )}
41
+ </>
42
+ );
43
+ }
44
+
45
+ export default ModalTrigger;
@@ -0,0 +1,164 @@
1
+ /* eslint-disable @typescript-eslint/no-empty-function */
2
+ import React from 'react';
3
+ import { screen, render, waitFor } from '@testing-library/react';
4
+ import userEvent from '@testing-library/user-event';
5
+
6
+ import { useOverlayTriggerState, OverlayTriggerState } from 'react-stately';
7
+ import { DOMAttributes, FocusableElement } from '@react-types/shared';
8
+ import { useOverlayTrigger } from 'react-aria';
9
+
10
+ import PreviewModal from './PreviewModal';
11
+
12
+ function PreviewModalTestWrapper({
13
+ constructFunction,
14
+ }: {
15
+ constructFunction: (state: OverlayTriggerState, overlayProps: DOMAttributes<FocusableElement>) => JSX.Element;
16
+ }) {
17
+ const previewModalState = useOverlayTriggerState({ isOpen: true });
18
+ const { overlayProps } = useOverlayTrigger({ type: 'dialog' }, previewModalState);
19
+
20
+ return constructFunction(previewModalState, overlayProps);
21
+ }
22
+
23
+ describe('PreviewModal', () => {
24
+ it('Modal is open by default', async () => {
25
+ const onClose = jest.fn();
26
+
27
+ render(
28
+ <PreviewModalTestWrapper
29
+ constructFunction={(state, overlayProps) => {
30
+ return (
31
+ <PreviewModal state={state} overlayProps={overlayProps} onClose={onClose}>
32
+ Modal Content
33
+ </PreviewModal>
34
+ );
35
+ }}
36
+ />,
37
+ );
38
+ expect(screen.queryByRole('dialog')).toBeTruthy();
39
+ });
40
+
41
+ it('Clicking outside the modal calls onClose', async () => {
42
+ const onClose = jest.fn();
43
+ render(
44
+ <div data-testid="outside">
45
+ <PreviewModalTestWrapper
46
+ constructFunction={(state, overlayProps) => {
47
+ return (
48
+ <PreviewModal state={state} overlayProps={overlayProps} onClose={onClose}>
49
+ Modal Content
50
+ </PreviewModal>
51
+ );
52
+ }}
53
+ />
54
+ ,
55
+ <div style={{ height: '150vh' }} />
56
+ </div>,
57
+ );
58
+
59
+ const user = userEvent.setup();
60
+
61
+ // Modal Open
62
+ await waitFor(() => {
63
+ expect(screen.queryByRole('dialog')).toBeTruthy();
64
+ });
65
+
66
+ await user.click(screen.getByTestId('outside'));
67
+
68
+ // Modal Closed
69
+ await waitFor(() => {
70
+ expect(onClose).toHaveBeenCalled();
71
+ });
72
+ });
73
+
74
+ it('ESC closes modal', async () => {
75
+ const onClose = jest.fn();
76
+ render(
77
+ <div data-testid="outside">
78
+ <PreviewModalTestWrapper
79
+ constructFunction={(state, overlayProps) => {
80
+ return (
81
+ <PreviewModal state={state} overlayProps={overlayProps} onClose={onClose}>
82
+ Modal Content
83
+ </PreviewModal>
84
+ );
85
+ }}
86
+ />
87
+ <div style={{ height: '150vh' }} />
88
+ </div>,
89
+ );
90
+
91
+ const user = userEvent.setup();
92
+
93
+ // Modal Open
94
+ await waitFor(() => {
95
+ expect(screen.queryByRole('dialog')).toBeTruthy();
96
+ });
97
+
98
+ user.type(screen.getByRole('dialog'), '{escape}');
99
+
100
+ // Modal Closed
101
+ await waitFor(() => {
102
+ expect(onClose).toHaveBeenCalled();
103
+ });
104
+ });
105
+
106
+ it('Focus is trapped within modal', async () => {
107
+ const onClose = jest.fn();
108
+
109
+ render(
110
+ <div>
111
+ <button />
112
+ <PreviewModalTestWrapper
113
+ constructFunction={(state, overlayProps) => {
114
+ return (
115
+ <PreviewModal state={state} overlayProps={overlayProps} onClose={onClose}>
116
+ Modal Content
117
+ </PreviewModal>
118
+ );
119
+ }}
120
+ />
121
+ <button />
122
+ <div style={{ height: '150vh' }} />
123
+ </div>,
124
+ );
125
+
126
+ const user = userEvent.setup();
127
+
128
+ await waitFor(() => {
129
+ expect(screen.queryByRole('dialog')).toBeTruthy();
130
+ });
131
+
132
+ user.type(screen.getByRole('dialog'), '{tab}');
133
+ user.type(screen.getByRole('dialog'), '{tab}');
134
+
135
+ await waitFor(() => {
136
+ expect(screen.queryByRole('button', { name: 'Close details' })).toHaveFocus();
137
+ });
138
+ });
139
+
140
+ it('Focus should start on the dialog', async () => {
141
+ const onClose = jest.fn();
142
+
143
+ render(
144
+ <div>
145
+ <button />
146
+ <PreviewModalTestWrapper
147
+ constructFunction={(state, overlayProps) => {
148
+ return (
149
+ <PreviewModal state={state} overlayProps={overlayProps} onClose={onClose}>
150
+ Modal Content
151
+ </PreviewModal>
152
+ );
153
+ }}
154
+ />
155
+ <button />
156
+ <div style={{ height: '150vh' }} />
157
+ </div>,
158
+ );
159
+
160
+ await waitFor(() => {
161
+ expect(screen.queryByRole('dialog')).toHaveFocus();
162
+ });
163
+ });
164
+ });
@@ -0,0 +1,92 @@
1
+ import React, { ReactNode, useRef, useEffect } from 'react';
2
+ import { Overlay, useModalOverlay, useDialog, useKeyboard } from 'react-aria';
3
+ import { OverlayTriggerState } from 'react-stately';
4
+ import { DOMAttributes, FocusableElement } from '@react-types/shared';
5
+
6
+ import Icon, { IconOptions } from '../Icons/Icon';
7
+
8
+ /*
9
+ This has to be a separate element otherwise the focus trap fails. Assuming this is because it needs
10
+ to fit inside the 'Overlay' as a form of context.
11
+ */
12
+ function PreviewModalContent({ children, onClose, ...props }: { children: ReactNode; onClose: () => void }) {
13
+ const ref = useRef<HTMLDivElement>(null);
14
+ const { dialogProps, titleProps } = useDialog(props, ref);
15
+
16
+ return (
17
+ <div {...dialogProps} ref={ref} className="h-full flex flex-col">
18
+ <h3 {...titleProps} className="sr-only">
19
+ Resource Details
20
+ </h3>
21
+ <button type="button" aria-label="Close details" onClick={onClose} className="absolute top-3 right-3">
22
+ <Icon icon={'close' as IconOptions} className="" />
23
+ </button>
24
+ {children}
25
+ </div>
26
+ );
27
+ }
28
+
29
+ export type PreviewModalProps = {
30
+ state: OverlayTriggerState;
31
+ overlayProps: DOMAttributes<FocusableElement>;
32
+ children?: ReactNode;
33
+ onClose: () => void;
34
+ };
35
+
36
+ // Accept a ref for the top level modal
37
+ function PreviewModal({ state, overlayProps, children, onClose, ...props }: PreviewModalProps) {
38
+ const modalRef = useRef<HTMLDivElement>(null);
39
+
40
+ const overlayRef = useRef<HTMLDivElement>(null);
41
+ const { modalProps, underlayProps } = useModalOverlay(
42
+ { isKeyboardDismissDisabled: true, ...props },
43
+ state,
44
+ overlayRef,
45
+ );
46
+
47
+ // Need to handle the ESC escape hatch ourselves as we need to run onClose to deselect the current node
48
+ const { keyboardProps } = useKeyboard({
49
+ onKeyDown: (e) => {
50
+ if (e.key === 'Escape') {
51
+ onClose();
52
+ } else {
53
+ // Need to do this so TAB get handled up higher and cycles though the focus trap
54
+ e.continuePropagation();
55
+ }
56
+ },
57
+ });
58
+
59
+ useEffect(() => {
60
+ // Check if the click event has happened outside the modal
61
+ function handleClickOutside(event: MouseEvent) {
62
+ if (modalRef.current && !modalRef.current.contains(event?.target as Node)) {
63
+ onClose();
64
+ }
65
+ }
66
+ // Bind the event listener
67
+ document.addEventListener('mousedown', handleClickOutside);
68
+ return () => {
69
+ // Unbind the event listener on clean up
70
+ document.removeEventListener('mousedown', handleClickOutside);
71
+ };
72
+ }, [modalRef]);
73
+
74
+ return (
75
+ <Overlay>
76
+ <div
77
+ ref={modalRef}
78
+ {...underlayProps}
79
+ {...keyboardProps}
80
+ className="fixed z-50 overflow-y-scroll bottom-0 w-full h-[50vh] bg-white border-t border-gray-300"
81
+ >
82
+ <div ref={overlayRef} {...modalProps} className="h-full">
83
+ <PreviewModalContent {...overlayProps} onClose={onClose}>
84
+ {children}
85
+ </PreviewModalContent>
86
+ </div>
87
+ </div>
88
+ </Overlay>
89
+ );
90
+ }
91
+
92
+ export default PreviewModal;
@@ -0,0 +1,197 @@
1
+ /* eslint-disable @typescript-eslint/no-empty-function */
2
+ import React from 'react';
3
+ import { screen, render, waitFor } from '@testing-library/react';
4
+ import userEvent from '@testing-library/user-event';
5
+ import { Context as ResponsiveContext } from 'react-responsive';
6
+
7
+ import { useOverlayTriggerState, OverlayTriggerState } from 'react-stately';
8
+ import { DOMAttributes, FocusableElement } from '@react-types/shared';
9
+ import { useOverlayTrigger } from 'react-aria';
10
+
11
+ import PreviewPanel from './PreviewPanel';
12
+
13
+ function PreviewPanelTestWrapper({
14
+ constructFunction,
15
+ }: {
16
+ constructFunction: (
17
+ previewModalState: OverlayTriggerState,
18
+ overlayProps: DOMAttributes<FocusableElement>,
19
+ ) => JSX.Element;
20
+ }) {
21
+ const previewModalState = useOverlayTriggerState({});
22
+ const { overlayProps } = useOverlayTrigger({ type: 'dialog' }, previewModalState);
23
+
24
+ return constructFunction(previewModalState, overlayProps);
25
+ }
26
+
27
+ describe('PreviewPanel', () => {
28
+ it('Show loading when isLoading is true', async () => {
29
+ const onSelect = jest.fn();
30
+ const onClose = jest.fn();
31
+
32
+ render(
33
+ <PreviewPanelTestWrapper
34
+ constructFunction={(previewModalState, overlayProps) => {
35
+ return (
36
+ <PreviewPanel
37
+ node={{ id: '1', source: '1' }}
38
+ resourceDetail={null}
39
+ isLoading
40
+ allowedTypes={undefined}
41
+ modalState={previewModalState}
42
+ previewModalOverlayProps={overlayProps}
43
+ onSelect={onSelect}
44
+ onClose={onClose}
45
+ />
46
+ );
47
+ }}
48
+ />,
49
+ );
50
+
51
+ await waitFor(() => {
52
+ expect(screen.getAllByText('Loading info')).toBeTruthy();
53
+ });
54
+ });
55
+
56
+ it('Renders a message when no item selected', async () => {
57
+ const onSelect = jest.fn();
58
+ const onClose = jest.fn();
59
+
60
+ render(
61
+ <PreviewPanelTestWrapper
62
+ constructFunction={(previewModalState, overlayProps) => {
63
+ return (
64
+ <PreviewPanel
65
+ node={null}
66
+ resourceDetail={null}
67
+ isLoading={false}
68
+ allowedTypes={undefined}
69
+ modalState={previewModalState}
70
+ previewModalOverlayProps={overlayProps}
71
+ onSelect={onSelect}
72
+ onClose={onClose}
73
+ />
74
+ );
75
+ }}
76
+ />,
77
+ );
78
+
79
+ await waitFor(() => {
80
+ expect(screen.getAllByText('Make a selection to see more info here.')).toBeTruthy();
81
+ });
82
+ });
83
+
84
+ it('Renders tablet / desktop very above 640px', async () => {
85
+ const onSelect = jest.fn();
86
+ const onClose = jest.fn();
87
+
88
+ render(
89
+ <ResponsiveContext.Provider value={{ width: 641 }}>
90
+ <PreviewPanelTestWrapper
91
+ constructFunction={(previewModalState, overlayProps) => {
92
+ return (
93
+ <PreviewPanel
94
+ node={{ id: '1', source: '1' }}
95
+ resourceDetail={{
96
+ type: 'folder',
97
+ name: 'TestResource',
98
+ properties: new Map([
99
+ ['assetId', '12345'],
100
+ ['status', 'UnderConstruction'],
101
+ ]),
102
+ }}
103
+ isLoading={false}
104
+ allowedTypes={undefined}
105
+ modalState={previewModalState}
106
+ previewModalOverlayProps={overlayProps}
107
+ onSelect={onSelect}
108
+ onClose={onClose}
109
+ />
110
+ );
111
+ }}
112
+ />
113
+ </ResponsiveContext.Provider>,
114
+ );
115
+
116
+ await waitFor(() => {
117
+ expect(screen.queryByRole('dialog')).toBeFalsy();
118
+ expect(screen.queryByText('TestResource')).toBeTruthy();
119
+ });
120
+ });
121
+
122
+ it('Renders mobile below 640px', async () => {
123
+ const onSelect = jest.fn();
124
+ const onClose = jest.fn();
125
+
126
+ render(
127
+ <ResponsiveContext.Provider value={{ width: 640 }}>
128
+ <PreviewPanelTestWrapper
129
+ constructFunction={(previewModalState, overlayProps) => {
130
+ return (
131
+ <PreviewPanel
132
+ node={{ id: '1', source: '1' }}
133
+ resourceDetail={{
134
+ type: 'folder',
135
+ name: 'TestResource',
136
+ properties: new Map([
137
+ ['assetId', '12345'],
138
+ ['status', 'UnderConstruction'],
139
+ ]),
140
+ }}
141
+ isLoading={false}
142
+ allowedTypes={undefined}
143
+ modalState={previewModalState}
144
+ previewModalOverlayProps={overlayProps}
145
+ onSelect={onSelect}
146
+ onClose={onClose}
147
+ />
148
+ );
149
+ }}
150
+ />
151
+ </ResponsiveContext.Provider>,
152
+ );
153
+
154
+ await waitFor(() => {
155
+ expect(screen.queryByRole('dialog')).toBeTruthy();
156
+ expect(screen.queryByText('TestResource')).toBeTruthy();
157
+ });
158
+ });
159
+
160
+ it('Clicking select button return source and id of resource being shown', async () => {
161
+ const onSelect = jest.fn();
162
+ const onClose = jest.fn();
163
+
164
+ render(
165
+ <PreviewPanelTestWrapper
166
+ constructFunction={(previewModalState, overlayProps) => {
167
+ return (
168
+ <PreviewPanel
169
+ node={{ id: '1', source: '1' }}
170
+ resourceDetail={{
171
+ type: 'folder',
172
+ name: 'TestResource',
173
+ properties: new Map([
174
+ ['assetId', '12345'],
175
+ ['status', 'UnderConstruction'],
176
+ ]),
177
+ }}
178
+ isLoading={false}
179
+ allowedTypes={undefined}
180
+ modalState={previewModalState}
181
+ previewModalOverlayProps={overlayProps}
182
+ onSelect={onSelect}
183
+ onClose={onClose}
184
+ />
185
+ );
186
+ }}
187
+ />,
188
+ );
189
+
190
+ const user = userEvent.setup();
191
+ user.click(screen.getByRole('button'));
192
+
193
+ await waitFor(() => {
194
+ expect(onSelect).toHaveBeenCalledWith({ id: '1', source: '1' });
195
+ });
196
+ });
197
+ });
@@ -0,0 +1,61 @@
1
+ import React from 'react';
2
+ import { StoryFn, Meta } from '@storybook/react';
3
+ import { useOverlayTriggerState } from 'react-stately';
4
+ import { useOverlayTrigger } from 'react-aria';
5
+
6
+ import PreviewPanel from './PreviewPanel';
7
+
8
+ export default {
9
+ title: 'Preview Panel',
10
+ component: PreviewPanel,
11
+ } as Meta<typeof PreviewPanel>;
12
+
13
+ const Template: StoryFn<typeof PreviewPanel> = ({ node, resourceDetail, isLoading, allowedTypes }) => {
14
+ const previewModalState = useOverlayTriggerState({});
15
+ const { overlayProps } = useOverlayTrigger({ type: 'dialog' }, previewModalState);
16
+
17
+ return (
18
+ <PreviewPanel
19
+ node={node}
20
+ resourceDetail={resourceDetail}
21
+ modalState={previewModalState}
22
+ previewModalOverlayProps={overlayProps}
23
+ isLoading={isLoading}
24
+ allowedTypes={allowedTypes}
25
+ onSelect={({ source, id }) => alert(`Resource Selected: ${source} - ${id}`)}
26
+ onClose={() => alert(`OnClose Selected`)}
27
+ />
28
+ );
29
+ };
30
+
31
+ export const Primary = Template.bind({});
32
+ Primary.args = {
33
+ node: {
34
+ id: '1',
35
+ source: '1',
36
+ },
37
+ resourceDetail: {
38
+ type: 'page',
39
+ name: 'Products',
40
+ properties: new Map([
41
+ ['assetId', '12345'],
42
+ ['status', 'UnderConstruction'],
43
+ ]),
44
+ },
45
+ isLoading: false,
46
+ isNotSelected: false,
47
+ allowedTypes: undefined,
48
+ };
49
+
50
+ export const NoSelected = Template.bind({});
51
+ NoSelected.args = {
52
+ ...Primary.args,
53
+ node: null,
54
+ resourceDetail: null,
55
+ };
56
+
57
+ export const Loading = Template.bind({});
58
+ Loading.args = {
59
+ ...Primary.args,
60
+ isLoading: true,
61
+ };
@@ -0,0 +1,123 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { useMediaQuery } from 'react-responsive';
3
+ import { OverlayTriggerState } from 'react-stately';
4
+ import { DOMAttributes, FocusableElement } from '@react-types/shared';
5
+
6
+ import Icon, { IconOptions } from '../Icons/Icon';
7
+ import MatrixResource from './details/MatrixResource';
8
+ import { NodeIdentifier, ResourceDetail } from '../index';
9
+ import PreviewModal from './PreviewModal';
10
+ import Spinner from '../Spinner/Spinner';
11
+
12
+ export interface PreviewPanelProps {
13
+ node: NodeIdentifier | null;
14
+ resourceDetail: ResourceDetail | null;
15
+ isLoading: boolean;
16
+ modalState: OverlayTriggerState;
17
+ previewModalOverlayProps: DOMAttributes<FocusableElement>;
18
+ allowedTypes: string[] | undefined;
19
+ onSelect: (node: NodeIdentifier) => void;
20
+ onClose: () => void;
21
+ }
22
+
23
+ const PreviewPanel = function ({
24
+ node,
25
+ resourceDetail,
26
+ isLoading,
27
+ previewModalOverlayProps,
28
+ modalState,
29
+ onSelect,
30
+ onClose,
31
+ }: PreviewPanelProps) {
32
+ // Watch the media size to see if we are on mobile size
33
+ const isMobile = useMediaQuery({ query: '(max-width: 640px)' });
34
+ const previousIsMobile = useRef<boolean | null>(null);
35
+
36
+ useEffect(() => {
37
+ if (node && isMobile) {
38
+ modalState.setOpen(true);
39
+ previousIsMobile.current = true;
40
+ } else {
41
+ previousIsMobile.current = false;
42
+ }
43
+ }, []);
44
+
45
+ // If the media changes from mobile to non mobile or reverse toggle modal showing to undo the aria-hidden etc
46
+ useEffect(() => {
47
+ // Only trigger if we have a node selected, otherwise the modal will re-open itself as soon as its closed
48
+ if (node) {
49
+ if (previousIsMobile.current !== isMobile) {
50
+ previousIsMobile.current = isMobile;
51
+
52
+ if (isMobile) {
53
+ // If no mobile open the modal automatically
54
+ modalState.setOpen(true);
55
+ } else {
56
+ // If not in mobile close the modal automatically
57
+ modalState.setOpen(false);
58
+ }
59
+ }
60
+ }
61
+ }, [node, isMobile, modalState]);
62
+
63
+ let previewPanel = node && resourceDetail && (
64
+ <>
65
+ <div className="flex flex-col grow">
66
+ <MatrixResource {...resourceDetail} />
67
+ </div>
68
+ <div className="flex justify-end border-t border-gray-300">
69
+ <button
70
+ type="button"
71
+ onClick={() => onSelect(node)}
72
+ className="rounded text-sm text-white bg-blue-300 py-2 px-2.5 m-5"
73
+ >
74
+ Select
75
+ </button>
76
+ </div>
77
+ </>
78
+ );
79
+ if (isLoading) {
80
+ previewPanel = (
81
+ <div className="flex flex-col grow">
82
+ <div className="flex flex-col grow items-center mt-20 mx-20">
83
+ <Spinner />
84
+ <div className="text-sm text-gray-600 text-center mt-4">Loading info</div>
85
+ </div>
86
+ </div>
87
+ );
88
+ }
89
+
90
+ return (
91
+ <div className="sm:overflow-y-scroll sm:flex-1 sm:grow-[2] bg-white">
92
+ {/* Dialog has its own title */}
93
+ {!isMobile && <h3 className="sr-only">Resource Details</h3>}
94
+
95
+ {/* Nothing selected, show an info message */}
96
+ {node === null && (
97
+ <div className="max-sm:hidden flex flex-col h-full">
98
+ <div className="flex flex-col grow items-center mt-20 mx-20">
99
+ <Icon icon={'resource-select' as IconOptions} aria-hidden />
100
+ <div className="text-sm text-gray-600 text-center mt-4">Make a selection to see more info here.</div>
101
+ </div>
102
+ <div className="flex justify-end border-t border-gray-300">
103
+ <button disabled type="button" className="rounded text-sm text-white bg-blue-300/[.6] py-2 px-2.5 m-5">
104
+ Select
105
+ </button>
106
+ </div>
107
+ </div>
108
+ )}
109
+
110
+ {/* Resource details shows in a new modal / bottom popover on mobile size */}
111
+ {node && isMobile && modalState.isOpen && (
112
+ <PreviewModal state={modalState} overlayProps={previewModalOverlayProps} onClose={onClose}>
113
+ {previewPanel}
114
+ </PreviewModal>
115
+ )}
116
+
117
+ {/* If not mobile, just print the details out */}
118
+ {node && !isMobile && <div className="flex flex-col h-full">{previewPanel}</div>}
119
+ </div>
120
+ );
121
+ };
122
+
123
+ export default PreviewPanel;