@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,175 @@
1
+ import React, { useState, useRef } from 'react';
2
+ import { useFocusWithin, useKeyboard } from '@react-aria/interactions';
3
+
4
+ import type { Source, Resource, NodeIdentifier } from '../index';
5
+ import Spinner from '../Spinner/Spinner';
6
+ import Icon, { IconOptions } from '../Icons/Icon';
7
+
8
+ import uuid from '../uuid';
9
+
10
+ export default function SourceDropdown({
11
+ sources,
12
+ currentSource,
13
+ isLoading,
14
+ onRootSelect,
15
+ onSourceSelect,
16
+ }: {
17
+ sources: Array<Source>;
18
+ currentSource: NodeIdentifier | null;
19
+ isLoading: boolean;
20
+ onRootSelect: () => void;
21
+ onSourceSelect: (node: NodeIdentifier, resetHierarchy: boolean) => void;
22
+ }) {
23
+ const [uniqueId] = useState(uuid());
24
+
25
+ const buttonRef = useRef<HTMLButtonElement>(null);
26
+ const [isOpen, setIsOpen] = useState(false);
27
+
28
+ // Watch the focus and blur on the menu and close if focus leaves the control
29
+ const { focusWithinProps } = useFocusWithin({
30
+ onBlurWithin: () => {
31
+ setIsOpen(false);
32
+ },
33
+ });
34
+
35
+ // Listen for Esc key within this element
36
+ const { keyboardProps } = useKeyboard({
37
+ onKeyDown: (e) => {
38
+ if (isOpen && e.key === 'Escape') {
39
+ setIsOpen(false);
40
+ buttonRef.current?.focus(); // Restore focus to the element which opened the menu
41
+ }
42
+ },
43
+ });
44
+
45
+ const handleSourceClick = (id: NodeIdentifier) => {
46
+ setIsOpen(false);
47
+ buttonRef.current?.focus();
48
+ onSourceSelect(id, true);
49
+ };
50
+
51
+ const handleRootSelect = () => {
52
+ setIsOpen(false);
53
+ buttonRef.current?.focus();
54
+ onRootSelect();
55
+ };
56
+
57
+ let currentResource: Resource | undefined = undefined;
58
+
59
+ for (let i = 0; i < sources.length; i++) {
60
+ const source = sources[i];
61
+ if (currentSource?.source === source.id) {
62
+ currentResource = source.nodes.find((node) => {
63
+ if (node.id.id === currentSource?.id) {
64
+ return node;
65
+ }
66
+ });
67
+ }
68
+ }
69
+
70
+ return (
71
+ <div {...focusWithinProps} {...keyboardProps} className="relative w-72 border border-2 rounded border-gray-300">
72
+ <button
73
+ ref={buttonRef}
74
+ type="button"
75
+ aria-label="Source quick select"
76
+ aria-expanded={isOpen}
77
+ aria-controls={`${uniqueId}-button-menu`}
78
+ onClick={() => setIsOpen(!isOpen)}
79
+ className="relative flex items-center text-sm font-semibold p-2 w-full"
80
+ >
81
+ {currentResource && (
82
+ <>
83
+ <span className="sr-only">current source </span>
84
+ <Icon icon={currentResource.type as IconOptions} resourceSource="matrix" aria-hidden className="mr-2.5" />
85
+ <div className="truncate max-w-[200px]">{currentResource.label}</div>
86
+ </>
87
+ )}
88
+
89
+ {!currentResource && (
90
+ <>
91
+ <span className="sr-only">view </span>
92
+ <Icon icon={'root' as IconOptions} aria-hidden className="mr-2.5" />
93
+ All available sources
94
+ </>
95
+ )}
96
+
97
+ <Icon icon={'arrow-down' as IconOptions} aria-hidden className="absolute right-3" />
98
+ </button>
99
+ <ul
100
+ id={`${uniqueId}-button-menu`}
101
+ aria-hidden={!isOpen}
102
+ className={`absolute z-50 top-[calc(100%+5px)] -left-0.5 w-[calc(100%+4px)] bg-gray-100 border border-2 rounded border-gray-300 p-2 ${
103
+ !isOpen ? 'hidden' : ''
104
+ }`}
105
+ >
106
+ <li
107
+ key="return-root"
108
+ className="flex items-center text-sm font-semibold mb-2 bg-white border rounded border-grey-200"
109
+ >
110
+ <button
111
+ type="button"
112
+ onClick={handleRootSelect}
113
+ className={`relative grow flex items-center p-2.5 hover:bg-gray-100 focus:bg-gray-100`}
114
+ >
115
+ <Icon icon={'root' as IconOptions} aria-hidden className="mr-2.5" />
116
+ All available sources
117
+ </button>
118
+ </li>
119
+ {isLoading && (
120
+ <li className="mt-6">
121
+ <Spinner size="lg" label="Loading sources" />
122
+ </li>
123
+ )}
124
+ {!isLoading &&
125
+ sources.map(({ id: sourceId, name, nodes }, index) => {
126
+ return (
127
+ <li
128
+ key={sourceId}
129
+ className={`flex flex-col text-sm font-semibold text-grey-800 ${index > 0 ? 'mt-3' : ''}`}
130
+ >
131
+ <div className="relative flex justify-center before:w-full before:h-px before:bg-gray-300 before:absolute before:top-2/4 before:z-0">
132
+ <span className="z-10 bg-gray-100 px-2.5">{name}</span>
133
+ </div>
134
+ {nodes?.length > 0 && (
135
+ <ul aria-label={`${name} nodes`} className="flex flex-col mt-2">
136
+ {nodes.map(({ type, id: nodeId, selected, label }) => {
137
+ return (
138
+ <li
139
+ key={`${sourceId}-${nodeId.id}`}
140
+ className="flex items-center bg-white border border-b-0 last:border-b border-grey-200 first:rounded-t last:rounded-b"
141
+ >
142
+ <button
143
+ type="button"
144
+ onClick={() => handleSourceClick(nodeId)}
145
+ className={`relative grow flex items-center p-2.5 hover:bg-gray-100 focus:bg-gray-100 ${
146
+ selected ? 'bg-blue-100 text-blue-400' : ''
147
+ }`}
148
+ >
149
+ <Icon
150
+ icon={type as IconOptions}
151
+ resourceSource="matrix"
152
+ aria-label={type}
153
+ className="shrink-0 mr-2.5"
154
+ />
155
+ <span className="text-left mr-7">{label}</span>
156
+ {nodeId === currentResource?.id && (
157
+ <Icon
158
+ icon={'selected' as IconOptions}
159
+ aria-label="selected"
160
+ className="absolute right-4"
161
+ />
162
+ )}
163
+ </button>
164
+ </li>
165
+ );
166
+ })}
167
+ </ul>
168
+ )}
169
+ </li>
170
+ );
171
+ })}
172
+ </ul>
173
+ </div>
174
+ );
175
+ }
@@ -0,0 +1,110 @@
1
+ [
2
+ {
3
+ "id": "1",
4
+ "name": "Acme corporate system",
5
+ "nodes": [
6
+ {
7
+ "id": {
8
+ "id": "1",
9
+ "source": "1"
10
+ },
11
+ "type": "site",
12
+ "selected": false,
13
+ "label": "HandyHomes Website very long wrapping name",
14
+ "childCount": 21
15
+ },
16
+ {
17
+ "id": {
18
+ "id": "2",
19
+ "source": "1"
20
+ },
21
+ "type": "site",
22
+ "selected": false,
23
+ "label": "Another Website",
24
+ "childCount": 13
25
+ }
26
+ ]
27
+ },
28
+ {
29
+ "id": "2",
30
+ "name": "Acme internal system",
31
+ "nodes": [
32
+ {
33
+ "id": {
34
+ "id": "1",
35
+ "source": "2"
36
+ },
37
+ "type": "site",
38
+ "selected": false,
39
+ "label": "Intranet Website",
40
+ "childCount": 15
41
+ },
42
+ {
43
+ "id": {
44
+ "id": "2",
45
+ "source": "2"
46
+ },
47
+ "type": "site",
48
+ "selected": false,
49
+ "label": "Social Website",
50
+ "childCount": 10
51
+ }
52
+ ]
53
+ },
54
+ {
55
+ "id": "3",
56
+ "name": "Other system",
57
+ "nodes": [
58
+ {
59
+ "id": {
60
+ "id": "1",
61
+ "source": "3"
62
+ },
63
+ "type": "folder",
64
+ "selected": false,
65
+ "label": "Digital asset manager",
66
+ "childCount": 0
67
+ },
68
+ {
69
+ "id": {
70
+ "id": "2",
71
+ "source": "3"
72
+ },
73
+ "type": "image",
74
+ "selected": false,
75
+ "label": "Unsplash image library",
76
+ "childCount": 0
77
+ },
78
+ {
79
+ "id": {
80
+ "id": "3",
81
+ "source": "3"
82
+ },
83
+ "type": "image",
84
+ "selected": false,
85
+ "label": "Unsplash image library",
86
+ "childCount": 0
87
+ },
88
+ {
89
+ "id": {
90
+ "id": "4",
91
+ "source": "3"
92
+ },
93
+ "type": "image",
94
+ "selected": false,
95
+ "label": "Unsplash image library",
96
+ "childCount": 0
97
+ },
98
+ {
99
+ "id": {
100
+ "id": "5",
101
+ "source": "3"
102
+ },
103
+ "type": "image",
104
+ "selected": false,
105
+ "label": "Unsplash image library",
106
+ "childCount": 0
107
+ }
108
+ ]
109
+ }
110
+ ]
@@ -0,0 +1,224 @@
1
+ /* eslint-disable @typescript-eslint/no-empty-function */
2
+ import React from 'react';
3
+ import { screen, render, waitFor, within } from '@testing-library/react';
4
+ import userEvent from '@testing-library/user-event';
5
+
6
+ import { useOverlayTriggerState, OverlayTriggerState } from 'react-stately';
7
+ import SourceList from './SourceList';
8
+
9
+ const sources = [
10
+ {
11
+ id: '1',
12
+ name: 'Source 1',
13
+ nodes: [
14
+ {
15
+ id: {
16
+ id: '1',
17
+ source: '1',
18
+ },
19
+ type: 'site',
20
+ selected: false,
21
+ label: 'Node 1',
22
+ childCount: 21,
23
+ },
24
+ {
25
+ id: {
26
+ id: '2',
27
+ source: '1',
28
+ },
29
+ type: 'site',
30
+ selected: false,
31
+ label: 'Node 2',
32
+ childCount: 13,
33
+ },
34
+ ],
35
+ },
36
+ {
37
+ id: '2',
38
+ name: 'Source 2',
39
+ nodes: [
40
+ {
41
+ id: {
42
+ id: '1',
43
+ source: '2',
44
+ },
45
+ type: 'site',
46
+ selected: false,
47
+ label: 'Node 3',
48
+ childCount: 15,
49
+ },
50
+ {
51
+ id: {
52
+ id: '2',
53
+ source: '2',
54
+ },
55
+ type: 'site',
56
+ selected: false,
57
+ label: 'Node 4',
58
+ childCount: 10,
59
+ },
60
+ ],
61
+ },
62
+ ];
63
+
64
+ function SourceListTestWrapper({
65
+ constructFunction,
66
+ }: {
67
+ constructFunction: (previewModalState: OverlayTriggerState) => JSX.Element;
68
+ }) {
69
+ const previewModalState = useOverlayTriggerState({});
70
+ return constructFunction(previewModalState);
71
+ }
72
+
73
+ describe('SourceList', () => {
74
+ it('Shows loading when isLoading is true', async () => {
75
+ render(
76
+ <SourceListTestWrapper
77
+ constructFunction={(previewModalState) => {
78
+ return (
79
+ <SourceList
80
+ sources={sources}
81
+ previewModalState={previewModalState}
82
+ isLoading={true}
83
+ onSourceSelect={() => {}}
84
+ onSourceDrillDown={() => {}}
85
+ />
86
+ );
87
+ }}
88
+ />,
89
+ );
90
+
91
+ await waitFor(() => {
92
+ expect(screen.getByLabelText('loading Source list')).toBeTruthy();
93
+ });
94
+ });
95
+
96
+ it('Source list render each source', async () => {
97
+ render(
98
+ <SourceListTestWrapper
99
+ constructFunction={(previewModalState) => {
100
+ return (
101
+ <SourceList
102
+ sources={sources}
103
+ previewModalState={previewModalState}
104
+ isLoading={false}
105
+ onSourceSelect={() => {}}
106
+ onSourceDrillDown={() => {}}
107
+ />
108
+ );
109
+ }}
110
+ />,
111
+ );
112
+
113
+ await waitFor(() => {
114
+ expect(screen.queryByText('Source 1')).toBeTruthy();
115
+ expect(screen.queryByText('Source 2')).toBeTruthy();
116
+ });
117
+ });
118
+
119
+ it('Focus is moved to the source list', async () => {
120
+ render(
121
+ <SourceListTestWrapper
122
+ constructFunction={(previewModalState) => {
123
+ return (
124
+ <SourceList
125
+ sources={sources}
126
+ previewModalState={previewModalState}
127
+ isLoading={false}
128
+ onSourceSelect={() => {}}
129
+ onSourceDrillDown={() => {}}
130
+ />
131
+ );
132
+ }}
133
+ />,
134
+ );
135
+
136
+ await waitFor(() => {
137
+ expect(screen.queryByLabelText('Source list')).toHaveFocus();
138
+ });
139
+ });
140
+
141
+ it('Source list renders each sources nodes', async () => {
142
+ render(
143
+ <SourceListTestWrapper
144
+ constructFunction={(previewModalState) => {
145
+ return (
146
+ <SourceList
147
+ sources={sources}
148
+ previewModalState={previewModalState}
149
+ isLoading={false}
150
+ onSourceSelect={() => {}}
151
+ onSourceDrillDown={() => {}}
152
+ />
153
+ );
154
+ }}
155
+ />,
156
+ );
157
+
158
+ await waitFor(() => {
159
+ const source1 = screen.getByLabelText('Source 1 nodes');
160
+ expect(within(source1).queryByText('Node 1')).toBeTruthy();
161
+ expect(within(source1).queryByText('Node 2')).toBeTruthy();
162
+
163
+ const source2 = screen.getByLabelText('Source 2 nodes');
164
+ expect(within(source2).queryByText('Node 3')).toBeTruthy();
165
+ expect(within(source2).queryByText('Node 4')).toBeTruthy();
166
+ });
167
+ });
168
+
169
+ it('Clicking node body triggers correct onSourceSelect', async () => {
170
+ const onSourceSelect = jest.fn();
171
+
172
+ render(
173
+ <SourceListTestWrapper
174
+ constructFunction={(previewModalState) => {
175
+ return (
176
+ <SourceList
177
+ sources={sources}
178
+ previewModalState={previewModalState}
179
+ isLoading={false}
180
+ onSourceSelect={onSourceSelect}
181
+ onSourceDrillDown={() => {}}
182
+ />
183
+ );
184
+ }}
185
+ />,
186
+ );
187
+
188
+ const user = userEvent.setup();
189
+ const itemButton = screen.getByRole('button', { name: 'site Node 1' });
190
+ user.click(itemButton);
191
+
192
+ await waitFor(() => {
193
+ // Provides the item that was clicked and an id reference to the button that was clicked
194
+ expect(onSourceSelect).toHaveBeenCalledWith({ source: '1', id: '1' }, { id: expect.any(String) });
195
+ });
196
+ });
197
+
198
+ it('Clicking node child count triggers correct onSourceDrillDown', async () => {
199
+ const onSourceDrillDown = jest.fn();
200
+
201
+ render(
202
+ <SourceListTestWrapper
203
+ constructFunction={(previewModalState) => {
204
+ return (
205
+ <SourceList
206
+ sources={sources}
207
+ previewModalState={previewModalState}
208
+ isLoading={false}
209
+ onSourceSelect={() => {}}
210
+ onSourceDrillDown={onSourceDrillDown}
211
+ />
212
+ );
213
+ }}
214
+ />,
215
+ );
216
+
217
+ const user = userEvent.setup();
218
+ user.click(screen.getByRole('button', { name: 'Drill down to Node 1 children' }));
219
+
220
+ await waitFor(() => {
221
+ expect(onSourceDrillDown).toHaveBeenCalledWith({ source: '1', id: '1' });
222
+ });
223
+ });
224
+ });
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { StoryFn, Meta } from '@storybook/react';
3
+ import { useOverlayTriggerState } from 'react-stately';
4
+
5
+ import SourceList from './SourceList';
6
+ import sampleSources from './sample-sources.json';
7
+
8
+ export default {
9
+ title: 'Source List',
10
+ component: SourceList,
11
+ } as Meta<typeof SourceList>;
12
+
13
+ const Template: StoryFn<typeof SourceList> = ({ sources, isLoading, allowedTypes }) => {
14
+ const previewModalState = useOverlayTriggerState({});
15
+
16
+ return (
17
+ <SourceList
18
+ sources={sources}
19
+ previewModalState={previewModalState}
20
+ isLoading={isLoading}
21
+ onSourceSelect={({ source, id }) => alert(`Source Select: ${source} - ${id}`)}
22
+ onSourceDrillDown={({ source, id }) => alert(`Child Drill Down: ${source} - ${id}`)}
23
+ allowedTypes={allowedTypes}
24
+ />
25
+ );
26
+ };
27
+
28
+ export const Primary = Template.bind({});
29
+ Primary.args = {
30
+ sources: sampleSources,
31
+ isLoading: false,
32
+ allowedTypes: ['site', 'image'],
33
+ };
34
+
35
+ export const Loading = Template.bind({});
36
+ Loading.args = {
37
+ ...Primary.args,
38
+ isLoading: true,
39
+ allowedTypes: ['site', 'image'],
40
+ };
@@ -0,0 +1,93 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { OverlayTriggerState } from 'react-stately';
3
+ import { DOMAttributes, FocusableElement } from '@react-types/shared';
4
+
5
+ import ResourceItem from '../ResourceItem/ResourceItem';
6
+ import { NodeIdentifier, Source } from '../index';
7
+ import { SkeletonList } from '../Skeleton/List/SkeletonList';
8
+ import clsx from 'clsx';
9
+
10
+ export interface SourceListProps {
11
+ sources: Array<Source>;
12
+ previewModalState: OverlayTriggerState;
13
+ isLoading: boolean;
14
+ onSourceSelect: (node: NodeIdentifier, overlayProps: DOMAttributes<FocusableElement>) => void;
15
+ onSourceDrillDown: (node: NodeIdentifier) => void;
16
+ allowedTypes?: string[] | undefined;
17
+ }
18
+
19
+ const SourceList = function ({
20
+ sources,
21
+ previewModalState,
22
+ isLoading,
23
+ onSourceSelect,
24
+ onSourceDrillDown,
25
+ allowedTypes,
26
+ }: SourceListProps) {
27
+ const listRef = useRef<HTMLUListElement>(null);
28
+
29
+ useEffect(() => {
30
+ if (listRef.current) {
31
+ listRef.current?.focus({
32
+ preventScroll: true,
33
+ });
34
+ }
35
+ }, []);
36
+
37
+ return (
38
+ <ul
39
+ ref={listRef}
40
+ tabIndex={-1}
41
+ aria-label={`${isLoading ? 'loading' : ''} Source list`}
42
+ className={clsx('flex flex-col bg-gray-100 min-h-full', !isLoading && 'px-7 py-4')}
43
+ >
44
+ {isLoading && (
45
+ <>
46
+ <li>
47
+ <SkeletonList itemCount={3} />
48
+ </li>
49
+ <li>
50
+ <SkeletonList itemCount={3} />
51
+ </li>
52
+ </>
53
+ )}
54
+
55
+ {!isLoading &&
56
+ sources.map(({ id: sourceId, name, nodes }, index) => {
57
+ return (
58
+ <li
59
+ key={sourceId}
60
+ className={`flex flex-col text-sm font-semibold text-grey-800 ${index > 0 ? 'mt-3' : ''}`}
61
+ >
62
+ <div className="relative flex justify-center before:w-full before:h-px before:bg-gray-300 before:absolute before:top-2/4 before:z-0">
63
+ <span className="z-10 bg-gray-100 px-2.5">{name}</span>
64
+ </div>
65
+ {nodes?.length > 0 && (
66
+ <ul aria-label={`${name} nodes`} className="flex flex-col">
67
+ {nodes.map(({ type, id: nodeId, selected, label, childCount }) => {
68
+ return (
69
+ <ResourceItem
70
+ key={`${sourceId}-${nodeId.id}`}
71
+ id={nodeId}
72
+ selected={selected}
73
+ label={label}
74
+ type={type}
75
+ childCount={childCount}
76
+ previewModalState={previewModalState}
77
+ onSelect={onSourceSelect}
78
+ onDrillDown={onSourceDrillDown}
79
+ className="mt-3 rounded-lg"
80
+ allowedTypes={allowedTypes}
81
+ />
82
+ );
83
+ })}
84
+ </ul>
85
+ )}
86
+ </li>
87
+ );
88
+ })}
89
+ </ul>
90
+ );
91
+ };
92
+
93
+ export default SourceList;