@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,56 @@
1
+ import React from 'react';
2
+ import { StoryFn, Meta } from '@storybook/react';
3
+
4
+ import { NodeIdentifier } from '../index';
5
+ import ResourcePickerContainer from './ResourcePickerContainer';
6
+ import sampleSources from '../SourceList/sample-sources.json';
7
+ import sampleResources from '../ResourceList/sample-resources.json';
8
+
9
+ export default {
10
+ title: 'Resource Picker',
11
+ component: ResourcePickerContainer,
12
+ } as Meta<typeof ResourcePickerContainer>;
13
+
14
+ const Template: StoryFn<typeof ResourcePickerContainer> = ({ title }) => {
15
+ return (
16
+ <ResourcePickerContainer
17
+ title={title}
18
+ titleAriaProps={{}}
19
+ allowedTypes={undefined}
20
+ onRequestSources={() => {
21
+ return new Promise((resolve) => {
22
+ setTimeout(resolve, 500, sampleSources);
23
+ });
24
+ }}
25
+ onRequestChildren={() => {
26
+ return new Promise((resolve) => {
27
+ setTimeout(resolve, 500, sampleResources);
28
+ });
29
+ }}
30
+ onRequestResource={() => {
31
+ return new Promise((resolve) => {
32
+ setTimeout(resolve, 500, {
33
+ type: 'page',
34
+ name: 'Products',
35
+ properties: new Map([
36
+ ['assetId', '12345'],
37
+ ['status', 'UnderConstruction'],
38
+ ]),
39
+ });
40
+ });
41
+ }}
42
+ onChange={(nodeId: NodeIdentifier) => {
43
+ alert(`Resource Browser has selected ${nodeId.source} ${nodeId.id}`);
44
+ }}
45
+ onClose={() => {
46
+ alert('Resource Browser closed');
47
+ }}
48
+ />
49
+ );
50
+ };
51
+
52
+ export const Primary = Template.bind({});
53
+
54
+ Primary.args = {
55
+ title: 'Asset Picker',
56
+ };
@@ -0,0 +1,224 @@
1
+ import React, { useState, useCallback, useEffect } from 'react';
2
+ import { DOMAttributes, FocusableElement } from '@react-types/shared';
3
+ import { useOverlayTriggerState } from 'react-stately';
4
+
5
+ import SourceList from '../SourceList/SourceList';
6
+ import ResourceList from '../ResourceList/ResourceList';
7
+ import ResourceBreadcrumb from '../ResourceBreadcrumb/ResourceBreadcrumb';
8
+ import PreviewPanel from '../PreviewPanel/PreviewPanel';
9
+ import SourceDropdown from '../SourceDropdown/SourceDropdown';
10
+
11
+ import { NodeIdentifier, Source, Resource, ResourceDetail, Hierarchy } from '../index';
12
+
13
+ interface ResourcePickerContainerProps {
14
+ title: string;
15
+ titleAriaProps: DOMAttributes<FocusableElement>;
16
+ allowedTypes: string[] | undefined;
17
+ onRequestSources: () => Promise<Source[]>;
18
+ onRequestChildren(id: NodeIdentifier): Promise<Resource[]>;
19
+ onRequestResource(id: NodeIdentifier): Promise<ResourceDetail | null>;
20
+ onChange(resource: NodeIdentifier | null): void;
21
+ onClose: () => void;
22
+ }
23
+
24
+ function ResourcePickerContainer({
25
+ title,
26
+ titleAriaProps,
27
+ allowedTypes,
28
+ onRequestSources,
29
+ onRequestChildren,
30
+ onRequestResource,
31
+ onChange,
32
+ onClose,
33
+ }: ResourcePickerContainerProps) {
34
+ const previewModalState = useOverlayTriggerState({});
35
+
36
+ const [isSourceLoading, setIsSourceLoading] = useState<boolean>(false);
37
+ const [isMainLoading, setIsMainLoading] = useState<boolean>(false);
38
+ const [isSecondaryLoading, setIsSecondaryLoading] = useState<boolean>(false);
39
+
40
+ const [currentNode, setCurrentNode] = useState<NodeIdentifier | null>(null);
41
+ const [selectedId, setSelectedId] = useState<NodeIdentifier | null>(null);
42
+ const [previewModalOverlayProps, setPreviewModalOverlayProps] = useState<DOMAttributes<FocusableElement>>({});
43
+ const [hierarchy, setHierarchy] = useState<Array<Hierarchy>>([]);
44
+
45
+ const [sources, setSources] = useState<Array<Source>>([]);
46
+ const [resources, setResources] = useState<Array<Resource>>([]);
47
+ const [selectedNodeDetails, setSelectedNodeDetails] = useState<ResourceDetail | null>(null);
48
+
49
+ const adjustHierarchy = (node: NodeIdentifier, resetHierarchy: boolean) => {
50
+ const isInHierarchy = hierarchy.find((hNode) => hNode.id === node);
51
+ let newHierarchy: Array<Hierarchy> = [];
52
+
53
+ // If the node is already in the hierarchy we need to 'jump' back to it
54
+ if (isInHierarchy) {
55
+ let reachedNode = false;
56
+ // Read though the hierarchy and add any nodes before and including the current to the array
57
+ hierarchy.forEach((hNode) => {
58
+ if (reachedNode === false) {
59
+ newHierarchy.push(hNode);
60
+ if (hNode.id === node) {
61
+ reachedNode = true;
62
+ }
63
+ }
64
+ });
65
+ } else {
66
+ let label: string = resources.find((resource) => resource.id === node)?.label || '';
67
+
68
+ // Might be a source
69
+ if (!label) {
70
+ const source = sources.find((source) => source.id === node.source);
71
+ if (source) {
72
+ label = source.nodes.find((resource) => resource.id === node)?.label || '';
73
+ }
74
+ }
75
+
76
+ // If we are jumping to a complete other spot and the container knows it, it can request a complete reset
77
+ if (!resetHierarchy) {
78
+ newHierarchy = hierarchy.slice();
79
+ }
80
+
81
+ newHierarchy.push({
82
+ id: node,
83
+ label,
84
+ });
85
+ }
86
+
87
+ setHierarchy(newHierarchy);
88
+ };
89
+
90
+ const handleResourceSelected = useCallback((node: NodeIdentifier, overlayProps: DOMAttributes<FocusableElement>) => {
91
+ setPreviewModalOverlayProps(overlayProps);
92
+ setIsSecondaryLoading(true);
93
+ setSelectedId(node);
94
+
95
+ setSelectedNodeDetails(null);
96
+
97
+ onRequestResource(node).then((detail: ResourceDetail | null) => {
98
+ setSelectedNodeDetails(detail);
99
+ setIsSecondaryLoading(false);
100
+ });
101
+ }, []);
102
+
103
+ const handleResourceDrillDown = useCallback(
104
+ (node: NodeIdentifier, resetHierarchy?: boolean) => {
105
+ setIsMainLoading(true);
106
+ setCurrentNode(node);
107
+ adjustHierarchy(node, resetHierarchy || false);
108
+
109
+ setSelectedId(null);
110
+ setSelectedNodeDetails(null);
111
+ setResources([]);
112
+
113
+ onRequestChildren(node).then((resources: Array<Resource>) => {
114
+ setResources(resources);
115
+ setIsMainLoading(false);
116
+ });
117
+ },
118
+ [resources, sources],
119
+ );
120
+
121
+ const handleReturnToRoot = useCallback(() => {
122
+ setCurrentNode(null);
123
+ setSelectedId(null);
124
+ setResources([]);
125
+ setHierarchy([]);
126
+
127
+ setSelectedNodeDetails(null);
128
+ }, []);
129
+
130
+ const handleDetailSelect = useCallback((node: NodeIdentifier) => {
131
+ onChange(node);
132
+ onClose();
133
+ }, []);
134
+
135
+ const handleDetailClose = useCallback(() => {
136
+ setSelectedId(null);
137
+ }, []);
138
+
139
+ // On load of component fetch the list of sources
140
+ useEffect(() => {
141
+ setIsSourceLoading(true);
142
+ onRequestSources().then((sources: Array<Source>) => {
143
+ setSources(sources);
144
+ setIsSourceLoading(false);
145
+ });
146
+ }, []);
147
+
148
+ return (
149
+ <div className="relative flex flex-col h-full">
150
+ <div className="flex items-center p-6">
151
+ <h2 {...titleAriaProps} className="text-xl leading-6 text-gray-800 font-semibold mr-6">
152
+ {title}
153
+ </h2>
154
+ <div className="px-3 border-l border-grey-300 w-300px">
155
+ <SourceDropdown
156
+ sources={sources}
157
+ currentSource={hierarchy[0]?.id}
158
+ isLoading={isSourceLoading}
159
+ onSourceSelect={handleResourceDrillDown}
160
+ onRootSelect={handleReturnToRoot}
161
+ />
162
+ </div>
163
+ <button
164
+ type="button"
165
+ aria-label={`Close ${title} dialog`}
166
+ onClick={onClose}
167
+ className="absolute top-2 right-2 p-2.5 rounded hover:bg-blue-100 focus:bg-blue-100"
168
+ >
169
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
170
+ <path
171
+ d="M13.3 0.710017C13.1131 0.522765 12.8595 0.417532 12.595 0.417532C12.3305 0.417532 12.0768 0.522765 11.89 0.710017L6.99997 5.59002L2.10997 0.700017C1.92314 0.512765 1.66949 0.407532 1.40497 0.407532C1.14045 0.407532 0.886802 0.512765 0.699971 0.700017C0.309971 1.09002 0.309971 1.72002 0.699971 2.11002L5.58997 7.00002L0.699971 11.89C0.309971 12.28 0.309971 12.91 0.699971 13.3C1.08997 13.69 1.71997 13.69 2.10997 13.3L6.99997 8.41002L11.89 13.3C12.28 13.69 12.91 13.69 13.3 13.3C13.69 12.91 13.69 12.28 13.3 11.89L8.40997 7.00002L13.3 2.11002C13.68 1.73002 13.68 1.09002 13.3 0.710017Z"
172
+ fill="currentColor"
173
+ />
174
+ </svg>
175
+ </button>
176
+ </div>
177
+ <div className="flex border-t border-grey-300 h-[calc(100%-92px)]">
178
+ <div className="overflow-y-scroll flex-1 grow-[3] border-r border-gray-300">
179
+ <h3 className="sr-only">Resource List</h3>
180
+ {currentNode === null && (
181
+ <SourceList
182
+ sources={sources}
183
+ previewModalState={previewModalState}
184
+ isLoading={isSourceLoading}
185
+ onSourceSelect={handleResourceSelected}
186
+ onSourceDrillDown={handleResourceDrillDown}
187
+ allowedTypes={allowedTypes}
188
+ />
189
+ )}
190
+
191
+ {currentNode && (
192
+ <>
193
+ <ResourceBreadcrumb
194
+ hierarchy={hierarchy}
195
+ onBreadcrumbSelect={handleResourceDrillDown}
196
+ onReturnToRoot={handleReturnToRoot}
197
+ />
198
+ <ResourceList
199
+ previewModalState={previewModalState}
200
+ resources={resources}
201
+ isLoading={isMainLoading}
202
+ onResourceSelect={handleResourceSelected}
203
+ onResourceDrillDown={handleResourceDrillDown}
204
+ allowedTypes={allowedTypes}
205
+ />
206
+ </>
207
+ )}
208
+ </div>
209
+ <PreviewPanel
210
+ node={selectedId}
211
+ resourceDetail={selectedNodeDetails}
212
+ modalState={previewModalState}
213
+ isLoading={isSecondaryLoading}
214
+ previewModalOverlayProps={previewModalOverlayProps}
215
+ allowedTypes={allowedTypes}
216
+ onSelect={handleDetailSelect}
217
+ onClose={handleDetailClose}
218
+ />
219
+ </div>
220
+ </div>
221
+ );
222
+ }
223
+
224
+ export default ResourcePickerContainer;
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { screen, render } from '@testing-library/react';
3
+
4
+ import { SkeletonList } from './SkeletonList';
5
+
6
+ describe('SkeletonList', () => {
7
+ it('Should render the skeleton list', async () => {
8
+ render(<SkeletonList itemCount={17} />);
9
+
10
+ expect(screen.getByLabelText('Skeleton loader list')).toBeInTheDocument();
11
+ });
12
+
13
+ it('Should list out the amount of items we set as the count', async () => {
14
+ render(<SkeletonList itemCount={5} />);
15
+
16
+ expect(screen.getAllByRole('listitem')).toHaveLength(5);
17
+ });
18
+ });
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import { StoryFn, Meta } from '@storybook/react';
3
+
4
+ import { SkeletonList, SkeletonListProps } from './SkeletonList';
5
+
6
+ export default {
7
+ title: 'Global/Skeleton/List',
8
+ component: SkeletonList,
9
+ } as Meta<typeof SkeletonList>;
10
+
11
+ const Template: StoryFn<SkeletonListProps> = (args: SkeletonListProps) => <SkeletonList {...args} />;
12
+
13
+ export const Default = Template.bind({});
14
+
15
+ Default.args = {};
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { SkeletonListItem } from '../ListItem/SkeletonListItem';
3
+ import clsx from 'clsx';
4
+
5
+ export type SkeletonListProps = {
6
+ itemCount: number;
7
+ className?: string;
8
+ };
9
+
10
+ export const SkeletonList = ({ itemCount = 8, className }: SkeletonListProps) => (
11
+ <ul className={clsx(`flex flex-col px-7 my-4`, className)} aria-label="Skeleton loader list">
12
+ {[...Array(itemCount)].map((_item, index: number) => {
13
+ return <SkeletonListItem key={index} />;
14
+ })}
15
+ </ul>
16
+ );
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import { StoryFn, Meta } from '@storybook/react';
3
+
4
+ import { SkeletonListItem } from './SkeletonListItem';
5
+
6
+ export default {
7
+ title: 'Global/Skeleton/List item',
8
+ component: SkeletonListItem,
9
+ } as Meta<typeof SkeletonListItem>;
10
+
11
+ const Template: StoryFn<typeof SkeletonListItem> = () => <SkeletonListItem />;
12
+
13
+ export const Default = Template.bind({});
14
+
15
+ Default.args = {};
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+
3
+ export const SkeletonListItem = () => (
4
+ <li className="flex items-center p-1 first:mt-0 bg-white border border-b-0 border-grey-200 first:rounded-t-lg last:rounded-b-lg last:border-b">
5
+ <div className="grid grid-cols-[24px_1fr_45px] w-full flex items-center p-4 rounded">
6
+ <span className="w-6 h-6 bg-gray-200 rounded-full" />
7
+ <div className="w-full d-flex flex-col mx-4">
8
+ <div className="mb-1 w-3/4 h-2 bg-gray-200 rounded" />
9
+ <div className="w-1/2 h-2 bg-gray-200 rounded" />
10
+ </div>
11
+ <div className="ml-auto mx-4 w-12 h-2 bg-gray-200 rounded" />
12
+ </div>
13
+ </li>
14
+ );
@@ -0,0 +1,263 @@
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 SourceDropdown from './SourceDropdown';
7
+
8
+ const sources = [
9
+ {
10
+ id: '1',
11
+ name: 'Source 1',
12
+ nodes: [
13
+ {
14
+ id: {
15
+ id: '1',
16
+ source: '1',
17
+ },
18
+ type: 'site',
19
+ selected: false,
20
+ label: 'Node 1',
21
+ childCount: 21,
22
+ },
23
+ {
24
+ id: {
25
+ id: '2',
26
+ source: '1',
27
+ },
28
+ type: 'site',
29
+ selected: false,
30
+ label: 'Node 2',
31
+ childCount: 13,
32
+ },
33
+ ],
34
+ },
35
+ {
36
+ id: '2',
37
+ name: 'Source 2',
38
+ nodes: [
39
+ {
40
+ id: {
41
+ id: '1',
42
+ source: '2',
43
+ },
44
+ type: 'site',
45
+ selected: false,
46
+ label: 'Node 3',
47
+ childCount: 15,
48
+ },
49
+ {
50
+ id: {
51
+ id: '2',
52
+ source: '2',
53
+ },
54
+ type: 'site',
55
+ selected: false,
56
+ label: 'Node 4',
57
+ childCount: 10,
58
+ },
59
+ ],
60
+ },
61
+ ];
62
+
63
+ describe('SourceDropdown', () => {
64
+ it('Render generic label if no source provided', async () => {
65
+ render(
66
+ <SourceDropdown
67
+ sources={sources}
68
+ currentSource={null}
69
+ isLoading={false}
70
+ onRootSelect={() => {}}
71
+ onSourceSelect={() => {}}
72
+ />,
73
+ );
74
+
75
+ await waitFor(() => {
76
+ expect(screen.getByRole('button', { name: 'Source quick select' })).toHaveTextContent(
77
+ 'view All available sources',
78
+ );
79
+ });
80
+ });
81
+
82
+ it('Show loading state if isLoading true', async () => {
83
+ render(
84
+ <SourceDropdown
85
+ sources={sources}
86
+ currentSource={null}
87
+ isLoading={true}
88
+ onRootSelect={() => {}}
89
+ onSourceSelect={() => {}}
90
+ />,
91
+ );
92
+
93
+ await waitFor(() => {
94
+ expect(screen.getByLabelText('Loading sources')).toBeTruthy();
95
+ });
96
+ });
97
+
98
+ it('Render title of currentSource if provided', async () => {
99
+ render(
100
+ <SourceDropdown
101
+ sources={sources}
102
+ currentSource={{
103
+ id: '2',
104
+ source: '2',
105
+ }}
106
+ isLoading={false}
107
+ onRootSelect={() => {}}
108
+ onSourceSelect={() => {}}
109
+ />,
110
+ );
111
+
112
+ await waitFor(() => {
113
+ expect(screen.getByRole('button', { name: 'Source quick select' })).toHaveTextContent('current source Node 4');
114
+ });
115
+ });
116
+
117
+ it('Sources are rendered when dropdown clicked', async () => {
118
+ render(
119
+ <SourceDropdown
120
+ sources={sources}
121
+ currentSource={null}
122
+ isLoading={false}
123
+ onRootSelect={() => {}}
124
+ onSourceSelect={() => {}}
125
+ />,
126
+ );
127
+
128
+ const user = userEvent.setup();
129
+ user.click(screen.getByRole('button', { name: 'Source quick select' }));
130
+
131
+ await waitFor(() => {
132
+ expect(screen.getByRole('button', { name: 'All available sources' })).toBeTruthy();
133
+ expect(screen.getByRole('button', { name: 'site Node 1' })).toBeTruthy();
134
+ expect(screen.getByRole('button', { name: 'site Node 2' })).toBeTruthy();
135
+ expect(screen.getByRole('button', { name: 'site Node 3' })).toBeTruthy();
136
+ expect(screen.getByRole('button', { name: 'site Node 4' })).toBeTruthy();
137
+ });
138
+ });
139
+
140
+ it('Source menu closes on focus loss', async () => {
141
+ render(
142
+ <div>
143
+ <SourceDropdown
144
+ sources={sources}
145
+ currentSource={null}
146
+ isLoading={false}
147
+ onRootSelect={() => {}}
148
+ onSourceSelect={() => {}}
149
+ />
150
+ <input />
151
+ </div>,
152
+ );
153
+
154
+ const user = userEvent.setup();
155
+ const buttonDropdown = screen.getByRole('button', { name: 'Source quick select' });
156
+ user.click(buttonDropdown);
157
+
158
+ await waitFor(() => {
159
+ expect(screen.getByRole('button', { name: 'All available sources' })).toBeTruthy();
160
+ expect(screen.getByRole('button', { name: 'site Node 1' })).toBeTruthy();
161
+ expect(screen.getByRole('button', { name: 'site Node 2' })).toBeTruthy();
162
+ expect(screen.getByRole('button', { name: 'site Node 3' })).toBeTruthy();
163
+ expect(screen.getByRole('button', { name: 'site Node 4' })).toBeTruthy();
164
+ });
165
+
166
+ user.type(buttonDropdown, '{tab}');
167
+ user.type(buttonDropdown, '{tab}');
168
+ user.type(buttonDropdown, '{tab}');
169
+ user.type(buttonDropdown, '{tab}');
170
+ user.type(buttonDropdown, '{tab}');
171
+
172
+ await waitFor(() => {
173
+ expect(screen.queryByRole('button', { name: 'All available sources' })).toBeFalsy();
174
+ expect(screen.queryByRole('button', { name: 'site Node 1' })).toBeFalsy();
175
+ expect(screen.queryByRole('button', { name: 'site Node 2' })).toBeFalsy();
176
+ expect(screen.queryByRole('button', { name: 'site Node 3' })).toBeFalsy();
177
+ expect(screen.queryByRole('button', { name: 'site Node 4' })).toBeFalsy();
178
+ });
179
+ });
180
+
181
+ it('Source menu closes on {esc}', async () => {
182
+ render(
183
+ <div>
184
+ <SourceDropdown
185
+ sources={sources}
186
+ currentSource={null}
187
+ isLoading={false}
188
+ onRootSelect={() => {}}
189
+ onSourceSelect={() => {}}
190
+ />
191
+ <input />
192
+ </div>,
193
+ );
194
+
195
+ const user = userEvent.setup();
196
+ const buttonDropdown = screen.getByRole('button', { name: 'Source quick select' });
197
+ user.click(buttonDropdown);
198
+
199
+ await waitFor(() => {
200
+ expect(screen.getByRole('button', { name: 'All available sources' })).toBeTruthy();
201
+ expect(screen.getByRole('button', { name: 'site Node 1' })).toBeTruthy();
202
+ expect(screen.getByRole('button', { name: 'site Node 2' })).toBeTruthy();
203
+ expect(screen.getByRole('button', { name: 'site Node 3' })).toBeTruthy();
204
+ expect(screen.getByRole('button', { name: 'site Node 4' })).toBeTruthy();
205
+ });
206
+
207
+ user.type(buttonDropdown, '{escape}');
208
+
209
+ await waitFor(() => {
210
+ expect(screen.queryByRole('button', { name: 'All available sources' })).toBeFalsy();
211
+ expect(screen.queryByRole('button', { name: 'site Node 1' })).toBeFalsy();
212
+ expect(screen.queryByRole('button', { name: 'site Node 2' })).toBeFalsy();
213
+ expect(screen.queryByRole('button', { name: 'site Node 3' })).toBeFalsy();
214
+ expect(screen.queryByRole('button', { name: 'site Node 4' })).toBeFalsy();
215
+ });
216
+ });
217
+
218
+ it('Selecting all available sources calls onRootSelect ', async () => {
219
+ const onRootSelect = jest.fn();
220
+
221
+ render(
222
+ <SourceDropdown
223
+ sources={sources}
224
+ currentSource={null}
225
+ isLoading={false}
226
+ onRootSelect={onRootSelect}
227
+ onSourceSelect={() => {}}
228
+ />,
229
+ );
230
+
231
+ const user = userEvent.setup();
232
+ await user.click(screen.getByRole('button', { name: 'Source quick select' }));
233
+ await user.click(screen.getByRole('button', { name: 'All available sources' }));
234
+
235
+ await waitFor(() => {
236
+ expect(onRootSelect).toHaveBeenCalled();
237
+ expect(screen.queryByRole('button', { name: 'All available sources' })).toBeFalsy();
238
+ });
239
+ });
240
+
241
+ it('Selecting node calls onSourceSelect ', async () => {
242
+ const onSourceSelect = jest.fn();
243
+
244
+ render(
245
+ <SourceDropdown
246
+ sources={sources}
247
+ currentSource={null}
248
+ isLoading={false}
249
+ onRootSelect={() => {}}
250
+ onSourceSelect={onSourceSelect}
251
+ />,
252
+ );
253
+
254
+ const user = userEvent.setup();
255
+ await user.click(screen.getByRole('button', { name: 'Source quick select' }));
256
+ await user.click(screen.getByRole('button', { name: 'site Node 1' }));
257
+
258
+ await waitFor(() => {
259
+ expect(onSourceSelect).toHaveBeenCalledWith({ id: '1', source: '1' }, true);
260
+ expect(screen.queryByRole('button', { name: 'All available sources' })).toBeFalsy();
261
+ });
262
+ });
263
+ });
@@ -0,0 +1,36 @@
1
+ import React from 'react';
2
+ import { StoryFn, Meta } from '@storybook/react';
3
+
4
+ import SourceDropdown from './SourceDropdown';
5
+ import sampleSources from './sample-sources.json';
6
+
7
+ export default {
8
+ title: 'Source Dropdown',
9
+ component: SourceDropdown,
10
+ } as Meta<typeof SourceDropdown>;
11
+
12
+ const Template: StoryFn<typeof SourceDropdown> = ({ sources, currentSource, isLoading }) => (
13
+ <SourceDropdown
14
+ sources={sources}
15
+ currentSource={currentSource}
16
+ isLoading={isLoading}
17
+ onSourceSelect={({ source, id }) => alert(`Source Select: ${source} - ${id}`)}
18
+ onRootSelect={() => alert(`Root Select`)}
19
+ />
20
+ );
21
+
22
+ export const Primary = Template.bind({});
23
+ Primary.args = {
24
+ sources: sampleSources,
25
+ currentSource: {
26
+ id: '1',
27
+ source: '1',
28
+ },
29
+ isLoading: false,
30
+ };
31
+
32
+ export const Loading = Template.bind({});
33
+ Loading.args = {
34
+ ...Primary.args,
35
+ isLoading: true,
36
+ };