@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.
- package/.storybook/main.ts +23 -0
- package/.storybook/preview-head.html +15 -0
- package/.storybook/preview.ts +16 -0
- package/build.js +21 -0
- package/jest.config.ts +18 -0
- package/lib/Icons/Generics/ArrowDown.d.ts +15 -0
- package/lib/Icons/Generics/ArrowDown.js +23 -0
- package/lib/Icons/Generics/ArrowRight.d.ts +15 -0
- package/lib/Icons/Generics/ArrowRight.js +23 -0
- package/lib/Icons/Generics/Close.d.ts +15 -0
- package/lib/Icons/Generics/Close.js +23 -0
- package/lib/Icons/Generics/GenericIconMap.d.ts +10 -0
- package/lib/Icons/Generics/GenericIconMap.js +14 -0
- package/lib/Icons/Generics/ResourceSelect.d.ts +15 -0
- package/lib/Icons/Generics/ResourceSelect.js +28 -0
- package/lib/Icons/Generics/Root.d.ts +15 -0
- package/lib/Icons/Generics/Root.js +23 -0
- package/lib/Icons/Generics/Selected.d.ts +15 -0
- package/lib/Icons/Generics/Selected.js +23 -0
- package/lib/Icons/Generics/index.d.ts +6 -0
- package/lib/Icons/Generics/index.js +19 -0
- package/lib/Icons/Icon.d.ts +47 -0
- package/lib/Icons/Icon.js +44 -0
- package/lib/Icons/MatrixResources/Audio.d.ts +15 -0
- package/lib/Icons/MatrixResources/Audio.js +28 -0
- package/lib/Icons/MatrixResources/Excel.d.ts +15 -0
- package/lib/Icons/MatrixResources/Excel.js +27 -0
- package/lib/Icons/MatrixResources/Folder.d.ts +15 -0
- package/lib/Icons/MatrixResources/Folder.js +24 -0
- package/lib/Icons/MatrixResources/GenericFile.d.ts +15 -0
- package/lib/Icons/MatrixResources/GenericFile.js +28 -0
- package/lib/Icons/MatrixResources/Image.d.ts +15 -0
- package/lib/Icons/MatrixResources/Image.js +26 -0
- package/lib/Icons/MatrixResources/MatrixResourceMap.d.ts +15 -0
- package/lib/Icons/MatrixResources/MatrixResourceMap.js +19 -0
- package/lib/Icons/MatrixResources/Page.d.ts +15 -0
- package/lib/Icons/MatrixResources/Page.js +30 -0
- package/lib/Icons/MatrixResources/Pdf.d.ts +15 -0
- package/lib/Icons/MatrixResources/Pdf.js +31 -0
- package/lib/Icons/MatrixResources/Powerpoint.d.ts +15 -0
- package/lib/Icons/MatrixResources/Powerpoint.js +28 -0
- package/lib/Icons/MatrixResources/Site.d.ts +15 -0
- package/lib/Icons/MatrixResources/Site.js +30 -0
- package/lib/Icons/MatrixResources/Video.d.ts +15 -0
- package/lib/Icons/MatrixResources/Video.js +24 -0
- package/lib/Icons/MatrixResources/Word.d.ts +17 -0
- package/lib/Icons/MatrixResources/Word.js +28 -0
- package/lib/Icons/MatrixResources/index.d.ts +11 -0
- package/lib/Icons/MatrixResources/index.js +29 -0
- package/lib/Modal/Modal.d.ts +11 -0
- package/lib/Modal/Modal.js +46 -0
- package/lib/Modal/ModalOpeningButton.d.ts +10 -0
- package/lib/Modal/ModalOpeningButton.js +13 -0
- package/lib/Modal/ModalTrigger.d.ts +9 -0
- package/lib/Modal/ModalTrigger.js +24 -0
- package/lib/PreviewPanel/PreviewModal.d.ts +11 -0
- package/lib/PreviewPanel/PreviewModal.js +81 -0
- package/lib/PreviewPanel/PreviewPanel.d.ts +16 -0
- package/lib/PreviewPanel/PreviewPanel.js +87 -0
- package/lib/PreviewPanel/details/MatrixResource.d.ts +12 -0
- package/lib/PreviewPanel/details/MatrixResource.js +41 -0
- package/lib/ResourceBreadcrumb/ResourceBreadcrumb.d.ts +9 -0
- package/lib/ResourceBreadcrumb/ResourceBreadcrumb.js +20 -0
- package/lib/ResourceItem/ResourceItem.d.ts +19 -0
- package/lib/ResourceItem/ResourceItem.js +26 -0
- package/lib/ResourceList/ResourceList.d.ts +14 -0
- package/lib/ResourceList/ResourceList.js +51 -0
- package/lib/ResourcePickerContainer/ResourcePickerContainer.d.ts +15 -0
- package/lib/ResourcePickerContainer/ResourcePickerContainer.js +145 -0
- package/lib/Skeleton/List/SkeletonList.d.ts +6 -0
- package/lib/Skeleton/List/SkeletonList.js +13 -0
- package/lib/Skeleton/ListItem/SkeletonListItem.d.ts +2 -0
- package/lib/Skeleton/ListItem/SkeletonListItem.js +15 -0
- package/lib/SourceDropdown/SourceDropdown.d.ts +9 -0
- package/lib/SourceDropdown/SourceDropdown.js +106 -0
- package/lib/SourceList/SourceList.d.ts +14 -0
- package/lib/SourceList/SourceList.js +58 -0
- package/lib/Spinner/Spinner.d.ts +8 -0
- package/lib/Spinner/Spinner.js +12 -0
- package/lib/index.css +968 -0
- package/lib/index.d.ts +37 -0
- package/lib/index.js +15 -0
- package/lib/uuid.d.ts +1 -0
- package/lib/uuid.js +8 -0
- package/package.json +74 -0
- package/postcss.config.js +11 -0
- package/src/Icons/Generics/ArrowDown.tsx +27 -0
- package/src/Icons/Generics/ArrowRight.tsx +27 -0
- package/src/Icons/Generics/Close.tsx +26 -0
- package/src/Icons/Generics/GenericIconMap.ts +14 -0
- package/src/Icons/Generics/ResourceSelect.tsx +40 -0
- package/src/Icons/Generics/Root.tsx +24 -0
- package/src/Icons/Generics/Selected.tsx +27 -0
- package/src/Icons/Generics/index.tsx +7 -0
- package/src/Icons/Icon.spec.tsx +62 -0
- package/src/Icons/Icon.stories.tsx +105 -0
- package/src/Icons/Icon.tsx +61 -0
- package/src/Icons/MatrixResources/Audio.tsx +30 -0
- package/src/Icons/MatrixResources/Excel.tsx +29 -0
- package/src/Icons/MatrixResources/Folder.tsx +29 -0
- package/src/Icons/MatrixResources/GenericFile.tsx +34 -0
- package/src/Icons/MatrixResources/Image.tsx +36 -0
- package/src/Icons/MatrixResources/MatrixResourceMap.ts +19 -0
- package/src/Icons/MatrixResources/Page.tsx +33 -0
- package/src/Icons/MatrixResources/Pdf.tsx +34 -0
- package/src/Icons/MatrixResources/Powerpoint.tsx +34 -0
- package/src/Icons/MatrixResources/Site.tsx +37 -0
- package/src/Icons/MatrixResources/Video.tsx +27 -0
- package/src/Icons/MatrixResources/Word.tsx +30 -0
- package/src/Icons/MatrixResources/index.tsx +12 -0
- package/src/Modal/Modal.spec.tsx +244 -0
- package/src/Modal/Modal.tsx +58 -0
- package/src/Modal/ModalContainer.stories.tsx +33 -0
- package/src/Modal/ModalOpeningButton.tsx +20 -0
- package/src/Modal/ModalTrigger.tsx +45 -0
- package/src/PreviewPanel/PreviewModal.spec.tsx +164 -0
- package/src/PreviewPanel/PreviewModal.tsx +92 -0
- package/src/PreviewPanel/PreviewPanel.spec.tsx +197 -0
- package/src/PreviewPanel/PreviewPanel.stories.tsx +61 -0
- package/src/PreviewPanel/PreviewPanel.tsx +123 -0
- package/src/PreviewPanel/details/MatrixResource.tsx +59 -0
- package/src/ResourceBreadcrumb/ResourceBreadcrumb.spec.tsx +76 -0
- package/src/ResourceBreadcrumb/ResourceBreadcrumb.stories.tsx +24 -0
- package/src/ResourceBreadcrumb/ResourceBreadcrumb.tsx +39 -0
- package/src/ResourceBreadcrumb/sample-hierarchy.json +23 -0
- package/src/ResourceItem/ResourceItem.spec.tsx +69 -0
- package/src/ResourceItem/ResourceItem.tsx +82 -0
- package/src/ResourceList/ResourceList.spec.tsx +196 -0
- package/src/ResourceList/ResourceList.stories.tsx +40 -0
- package/src/ResourceList/ResourceList.tsx +74 -0
- package/src/ResourceList/sample-resources.json +75 -0
- package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +706 -0
- package/src/ResourcePickerContainer/ResourcePickerContainer.stories.tsx +56 -0
- package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +224 -0
- package/src/Skeleton/List/SkeletonList.spec.tsx +18 -0
- package/src/Skeleton/List/SkeletonList.stories.tsx +15 -0
- package/src/Skeleton/List/SkeletonList.tsx +16 -0
- package/src/Skeleton/ListItem/SkeletonListItem.stories.tsx +15 -0
- package/src/Skeleton/ListItem/SkeletonListItem.tsx +14 -0
- package/src/SourceDropdown/SourceDropdown.spec.tsx +263 -0
- package/src/SourceDropdown/SourceDropdown.stories.tsx +36 -0
- package/src/SourceDropdown/SourceDropdown.tsx +175 -0
- package/src/SourceDropdown/sample-sources.json +110 -0
- package/src/SourceList/SourceList.spec.tsx +224 -0
- package/src/SourceList/SourceList.stories.tsx +40 -0
- package/src/SourceList/SourceList.tsx +93 -0
- package/src/SourceList/sample-sources.json +110 -0
- package/src/Spinner/Spinner.spec.tsx +18 -0
- package/src/Spinner/Spinner.stories.tsx +26 -0
- package/src/Spinner/Spinner.tsx +18 -0
- package/src/Spinner/_spinner.scss +11 -0
- package/src/__mocks__/JestHelpers.ts +65 -0
- package/src/__mocks__/jestHelpers.spec.ts +38 -0
- package/src/__mocks__/styleMock.ts +1 -0
- package/src/index.scss +7 -0
- package/src/index.stories.tsx +70 -0
- package/src/index.tsx +71 -0
- package/src/uuid.ts +7 -0
- package/tailwind.config.cjs +84 -0
- 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;
|