@squiz/generic-browser-lib 1.35.1-alpha.34
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/README.md +10 -0
- package/build.js +21 -0
- package/jest.config.ts +29 -0
- package/lib/Hooks/useAsync.d.ts +21 -0
- package/lib/Hooks/useAsync.js +53 -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/Back.d.ts +4 -0
- package/lib/Icons/Generics/Back.js +12 -0
- package/lib/Icons/Generics/Close.d.ts +15 -0
- package/lib/Icons/Generics/Close.js +23 -0
- package/lib/Icons/Generics/Empty.d.ts +4 -0
- package/lib/Icons/Generics/Empty.js +12 -0
- package/lib/Icons/Generics/Error.d.ts +4 -0
- package/lib/Icons/Generics/Error.js +12 -0
- package/lib/Icons/Generics/GenericIconMap.d.ts +14 -0
- package/lib/Icons/Generics/GenericIconMap.js +18 -0
- package/lib/Icons/Generics/ResourceSelect.d.ts +15 -0
- package/lib/Icons/Generics/ResourceSelect.js +28 -0
- package/lib/Icons/Generics/Retry.d.ts +4 -0
- package/lib/Icons/Generics/Retry.js +12 -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 +10 -0
- package/lib/Icons/Generics/index.js +27 -0
- package/lib/Icons/Icon.d.ts +50 -0
- package/lib/Icons/Icon.js +42 -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 +10 -0
- package/lib/Modal/Modal.js +47 -0
- package/lib/Modal/ModalOpeningButton.d.ts +10 -0
- package/lib/Modal/ModalOpeningButton.js +14 -0
- package/lib/Modal/ModalTrigger.d.ts +9 -0
- package/lib/Modal/ModalTrigger.js +26 -0
- package/lib/PreviewPanel/PreviewModal.d.ts +11 -0
- package/lib/PreviewPanel/PreviewModal.js +79 -0
- package/lib/PreviewPanel/PreviewPanel.d.ts +13 -0
- package/lib/PreviewPanel/PreviewPanel.js +58 -0
- package/lib/PreviewPanel/PreviewPanelHOC.d.ts +6 -0
- package/lib/PreviewPanel/PreviewPanelHOC.js +16 -0
- package/lib/ResetButton/ResetButton.d.ts +5 -0
- package/lib/ResetButton/ResetButton.js +12 -0
- package/lib/ResourceItem/ResourceItem.d.ts +19 -0
- package/lib/ResourceItem/ResourceItem.js +29 -0
- package/lib/ResourceState/ResourceState.d.ts +7 -0
- package/lib/ResourceState/ResourceState.js +17 -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 +1 -0
- package/lib/Skeleton/ListItem/SkeletonListItem.js +15 -0
- package/lib/Spinner/Spinner.d.ts +7 -0
- package/lib/Spinner/Spinner.js +13 -0
- package/lib/index.css +885 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.js +28 -0
- package/package.json +76 -0
- package/postcss.config.js +11 -0
- package/src/Hooks/useAsync.spec.ts +106 -0
- package/src/Hooks/useAsync.ts +62 -0
- package/src/Icons/Generics/ArrowDown.tsx +27 -0
- package/src/Icons/Generics/ArrowRight.tsx +27 -0
- package/src/Icons/Generics/Back.tsx +13 -0
- package/src/Icons/Generics/Close.tsx +26 -0
- package/src/Icons/Generics/Empty.tsx +13 -0
- package/src/Icons/Generics/Error.tsx +13 -0
- package/src/Icons/Generics/GenericIconMap.ts +18 -0
- package/src/Icons/Generics/ResourceSelect.tsx +40 -0
- package/src/Icons/Generics/Retry.tsx +13 -0
- package/src/Icons/Generics/Root.tsx +24 -0
- package/src/Icons/Generics/Selected.tsx +27 -0
- package/src/Icons/Generics/index.tsx +11 -0
- package/src/Icons/Icon.spec.tsx +62 -0
- package/src/Icons/Icon.stories.tsx +110 -0
- package/src/Icons/Icon.tsx +54 -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 +269 -0
- package/src/Modal/Modal.tsx +55 -0
- package/src/Modal/ModalContainer.stories.tsx +33 -0
- package/src/Modal/ModalOpeningButton.tsx +20 -0
- package/src/Modal/ModalTrigger.tsx +54 -0
- package/src/PreviewPanel/PreviewModal.spec.tsx +164 -0
- package/src/PreviewPanel/PreviewModal.tsx +94 -0
- package/src/PreviewPanel/PreviewPanel.spec.tsx +162 -0
- package/src/PreviewPanel/PreviewPanel.stories.tsx +66 -0
- package/src/PreviewPanel/PreviewPanel.tsx +83 -0
- package/src/PreviewPanel/PreviewPanelHOC.spec.tsx +45 -0
- package/src/PreviewPanel/PreviewPanelHOC.tsx +17 -0
- package/src/ResetButton/ResetButton.spec.tsx +42 -0
- package/src/ResetButton/ResetButton.tsx +22 -0
- package/src/ResourceItem/ResourceItem.spec.tsx +65 -0
- package/src/ResourceItem/ResourceItem.tsx +90 -0
- package/src/ResourceState/ResourceState.spec.tsx +26 -0
- package/src/ResourceState/ResourceState.stories.tsx +24 -0
- package/src/ResourceState/ResourceState.tsx +31 -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 +20 -0
- package/src/Skeleton/ListItem/SkeletonListItem.stories.tsx +15 -0
- package/src/Skeleton/ListItem/SkeletonListItem.tsx +14 -0
- package/src/Skeleton/_skeleton.scss +15 -0
- package/src/Spinner/Spinner.spec.tsx +18 -0
- package/src/Spinner/Spinner.stories.tsx +26 -0
- package/src/Spinner/Spinner.tsx +16 -0
- package/src/Spinner/_spinner.scss +14 -0
- package/src/index.scss +22 -0
- package/src/index.stories.tsx +26 -0
- package/src/index.ts +20 -0
- package/tailwind.config.cjs +89 -0
- package/tsconfig.json +22 -0
@@ -0,0 +1,162 @@
|
|
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
|
+
function mockResource(properties: Partial<any> = {}): any {
|
28
|
+
return {
|
29
|
+
id: 'test-resource',
|
30
|
+
name: 'Test resource',
|
31
|
+
type: { code: 'folder', name: 'Folder' },
|
32
|
+
status: { code: 'live', name: 'Live' },
|
33
|
+
url: 'https://no-where.com',
|
34
|
+
urls: [],
|
35
|
+
childCount: 0,
|
36
|
+
...properties,
|
37
|
+
};
|
38
|
+
}
|
39
|
+
|
40
|
+
const ResourceComponent = () => {
|
41
|
+
return <div>TestResource</div>;
|
42
|
+
};
|
43
|
+
|
44
|
+
describe('PreviewPanel', () => {
|
45
|
+
it('Renders a message when no item selected', async () => {
|
46
|
+
const onSelect = jest.fn();
|
47
|
+
const onClose = jest.fn();
|
48
|
+
|
49
|
+
render(
|
50
|
+
<PreviewPanelTestWrapper
|
51
|
+
constructFunction={(previewModalState, overlayProps) => {
|
52
|
+
return (
|
53
|
+
<PreviewPanel
|
54
|
+
resource={null}
|
55
|
+
allowedTypes={undefined}
|
56
|
+
modalState={previewModalState}
|
57
|
+
previewModalOverlayProps={overlayProps}
|
58
|
+
onSelect={onSelect}
|
59
|
+
onClose={onClose}
|
60
|
+
ResourceComponent={ResourceComponent}
|
61
|
+
/>
|
62
|
+
);
|
63
|
+
}}
|
64
|
+
/>,
|
65
|
+
);
|
66
|
+
|
67
|
+
await waitFor(() => {
|
68
|
+
expect(screen.getAllByText('Make a selection to see more info here.')).toBeTruthy();
|
69
|
+
});
|
70
|
+
});
|
71
|
+
|
72
|
+
it('Renders tablet / desktop very above 640px', async () => {
|
73
|
+
const onSelect = jest.fn();
|
74
|
+
const onClose = jest.fn();
|
75
|
+
|
76
|
+
render(
|
77
|
+
<ResponsiveContext.Provider value={{ width: 641 }}>
|
78
|
+
<PreviewPanelTestWrapper
|
79
|
+
constructFunction={(previewModalState, overlayProps) => {
|
80
|
+
return (
|
81
|
+
<PreviewPanel
|
82
|
+
resource={mockResource({ name: 'TestResource' })}
|
83
|
+
allowedTypes={undefined}
|
84
|
+
modalState={previewModalState}
|
85
|
+
previewModalOverlayProps={overlayProps}
|
86
|
+
onSelect={onSelect}
|
87
|
+
onClose={onClose}
|
88
|
+
ResourceComponent={ResourceComponent}
|
89
|
+
/>
|
90
|
+
);
|
91
|
+
}}
|
92
|
+
/>
|
93
|
+
</ResponsiveContext.Provider>,
|
94
|
+
);
|
95
|
+
|
96
|
+
await waitFor(() => {
|
97
|
+
expect(screen.queryByRole('dialog')).toBeFalsy();
|
98
|
+
expect(screen.queryByText('TestResource')).toBeTruthy();
|
99
|
+
});
|
100
|
+
});
|
101
|
+
|
102
|
+
it('Renders mobile below 640px', async () => {
|
103
|
+
const onSelect = jest.fn();
|
104
|
+
const onClose = jest.fn();
|
105
|
+
|
106
|
+
render(
|
107
|
+
<ResponsiveContext.Provider value={{ width: 640 }}>
|
108
|
+
<PreviewPanelTestWrapper
|
109
|
+
constructFunction={(previewModalState, overlayProps) => {
|
110
|
+
return (
|
111
|
+
<PreviewPanel
|
112
|
+
resource={mockResource({ name: 'TestResource' })}
|
113
|
+
allowedTypes={undefined}
|
114
|
+
modalState={previewModalState}
|
115
|
+
previewModalOverlayProps={overlayProps}
|
116
|
+
onSelect={onSelect}
|
117
|
+
onClose={onClose}
|
118
|
+
ResourceComponent={ResourceComponent}
|
119
|
+
/>
|
120
|
+
);
|
121
|
+
}}
|
122
|
+
/>
|
123
|
+
</ResponsiveContext.Provider>,
|
124
|
+
);
|
125
|
+
|
126
|
+
await waitFor(() => {
|
127
|
+
expect(screen.queryByRole('dialog')).toBeTruthy();
|
128
|
+
expect(screen.queryByText('TestResource')).toBeTruthy();
|
129
|
+
});
|
130
|
+
});
|
131
|
+
|
132
|
+
it('Clicking select button return source and id of resource being shown', async () => {
|
133
|
+
const onSelect = jest.fn();
|
134
|
+
const onClose = jest.fn();
|
135
|
+
const resource = mockResource();
|
136
|
+
|
137
|
+
render(
|
138
|
+
<PreviewPanelTestWrapper
|
139
|
+
constructFunction={(previewModalState, overlayProps) => {
|
140
|
+
return (
|
141
|
+
<PreviewPanel
|
142
|
+
resource={resource}
|
143
|
+
allowedTypes={undefined}
|
144
|
+
modalState={previewModalState}
|
145
|
+
previewModalOverlayProps={overlayProps}
|
146
|
+
onSelect={onSelect}
|
147
|
+
onClose={onClose}
|
148
|
+
ResourceComponent={ResourceComponent}
|
149
|
+
/>
|
150
|
+
);
|
151
|
+
}}
|
152
|
+
/>,
|
153
|
+
);
|
154
|
+
|
155
|
+
const user = userEvent.setup();
|
156
|
+
user.click(screen.getByRole('button'));
|
157
|
+
|
158
|
+
await waitFor(() => {
|
159
|
+
expect(onSelect).toHaveBeenCalledWith(resource);
|
160
|
+
});
|
161
|
+
});
|
162
|
+
});
|
@@ -0,0 +1,66 @@
|
|
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
|
+
import { Icon, IconOptions } from '../Icons/Icon';
|
8
|
+
|
9
|
+
export default {
|
10
|
+
title: 'Preview Panel',
|
11
|
+
component: PreviewPanel,
|
12
|
+
} as Meta<typeof PreviewPanel>;
|
13
|
+
|
14
|
+
const MatrixResource = ({ resource: { name, type } }) => {
|
15
|
+
return (
|
16
|
+
<div>
|
17
|
+
<div className="flex flex-col items-center text-gray-800 mt-7 mx-5 pb-4 border-b border-gray-300">
|
18
|
+
<Icon icon={type.code as IconOptions} resourceSource="matrix" className="w-14 h-14" />
|
19
|
+
<div className="mt-4 font-semibold text-base">{name}</div>
|
20
|
+
</div>
|
21
|
+
</div>
|
22
|
+
);
|
23
|
+
};
|
24
|
+
|
25
|
+
const Template: StoryFn<typeof PreviewPanel> = ({ resource, allowedTypes }) => {
|
26
|
+
const previewModalState = useOverlayTriggerState({});
|
27
|
+
const { overlayProps } = useOverlayTrigger({ type: 'dialog' }, previewModalState);
|
28
|
+
|
29
|
+
return (
|
30
|
+
<PreviewPanel
|
31
|
+
resource={resource}
|
32
|
+
modalState={previewModalState}
|
33
|
+
previewModalOverlayProps={overlayProps}
|
34
|
+
allowedTypes={allowedTypes}
|
35
|
+
onSelect={({ id, name }) => alert(`Resource Selected: ${id} - ${name}`)}
|
36
|
+
onClose={() => alert(`OnClose Selected`)}
|
37
|
+
ResourceComponent={MatrixResource}
|
38
|
+
/>
|
39
|
+
);
|
40
|
+
};
|
41
|
+
|
42
|
+
export const Primary = Template.bind({});
|
43
|
+
Primary.args = {
|
44
|
+
resource: {
|
45
|
+
id: '1',
|
46
|
+
name: 'Products',
|
47
|
+
type: {
|
48
|
+
code: 'page_standard',
|
49
|
+
name: 'Standard Page',
|
50
|
+
},
|
51
|
+
status: {
|
52
|
+
code: 'live',
|
53
|
+
name: 'Live',
|
54
|
+
},
|
55
|
+
url: 'http://my-squiz.net/assets/1',
|
56
|
+
urls: [],
|
57
|
+
childCount: 0,
|
58
|
+
},
|
59
|
+
allowedTypes: undefined,
|
60
|
+
};
|
61
|
+
|
62
|
+
export const NoSelected = Template.bind({});
|
63
|
+
NoSelected.args = {
|
64
|
+
...Primary.args,
|
65
|
+
resource: null,
|
66
|
+
};
|
@@ -0,0 +1,83 @@
|
|
1
|
+
import React, { useEffect } 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 PreviewModal from './PreviewModal';
|
8
|
+
|
9
|
+
export interface PreviewPanelProps {
|
10
|
+
resource: any | null;
|
11
|
+
modalState: OverlayTriggerState;
|
12
|
+
previewModalOverlayProps: DOMAttributes<FocusableElement>;
|
13
|
+
allowedTypes: string[] | undefined;
|
14
|
+
onSelect: (resource: any) => void;
|
15
|
+
onClose: () => void;
|
16
|
+
ResourceComponent?: React.ElementType;
|
17
|
+
}
|
18
|
+
|
19
|
+
export const PreviewPanel = function ({
|
20
|
+
resource,
|
21
|
+
previewModalOverlayProps,
|
22
|
+
modalState,
|
23
|
+
onSelect,
|
24
|
+
onClose,
|
25
|
+
ResourceComponent,
|
26
|
+
}: PreviewPanelProps) {
|
27
|
+
// Watch the media size to see if we are on mobile size
|
28
|
+
const isMobile = useMediaQuery({ query: '(max-width: 640px)' });
|
29
|
+
|
30
|
+
// If we are on mobile and the selected resource changes show the preview panel modal.
|
31
|
+
useEffect(() => {
|
32
|
+
if (!modalState.isOpen) {
|
33
|
+
modalState.setOpen(Boolean(resource && isMobile));
|
34
|
+
}
|
35
|
+
}, [resource, isMobile]);
|
36
|
+
|
37
|
+
const previewPanel = resource && (
|
38
|
+
<>
|
39
|
+
<div className="flex flex-col grow">{ResourceComponent && <ResourceComponent resource={resource} />}</div>
|
40
|
+
<div className="flex justify-end border-t border-gray-300">
|
41
|
+
<button
|
42
|
+
type="button"
|
43
|
+
onClick={() => onSelect(resource)}
|
44
|
+
className="rounded text-sm text-white bg-blue-300 py-2 px-2.5 m-5"
|
45
|
+
>
|
46
|
+
Select
|
47
|
+
</button>
|
48
|
+
</div>
|
49
|
+
</>
|
50
|
+
);
|
51
|
+
|
52
|
+
return (
|
53
|
+
<div className="sm:overflow-y-scroll sm:flex-1 sm:grow-[2] bg-white">
|
54
|
+
{/* Dialog has its own title */}
|
55
|
+
{!isMobile && <h3 className="sr-only">Resource Details</h3>}
|
56
|
+
|
57
|
+
{/* Nothing selected, show an info message */}
|
58
|
+
{resource === null && (
|
59
|
+
<div className="max-sm:hidden flex flex-col h-full">
|
60
|
+
<div className="flex flex-col grow items-center mt-20 mx-20">
|
61
|
+
<Icon icon={'resource-select' as IconOptions} aria-hidden />
|
62
|
+
<div className="text-sm text-gray-600 text-center mt-4">Make a selection to see more info here.</div>
|
63
|
+
</div>
|
64
|
+
<div className="flex justify-end border-t border-gray-300">
|
65
|
+
<button disabled type="button" className="rounded text-sm text-white bg-blue-300/[.6] py-2 px-2.5 m-5">
|
66
|
+
Select
|
67
|
+
</button>
|
68
|
+
</div>
|
69
|
+
</div>
|
70
|
+
)}
|
71
|
+
|
72
|
+
{/* Resource details shows in a new modal / bottom popover on mobile size */}
|
73
|
+
{resource && isMobile && modalState.isOpen && (
|
74
|
+
<PreviewModal state={modalState} overlayProps={previewModalOverlayProps} onClose={onClose}>
|
75
|
+
{previewPanel}
|
76
|
+
</PreviewModal>
|
77
|
+
)}
|
78
|
+
|
79
|
+
{/* If not mobile, just print the details out */}
|
80
|
+
{resource && !isMobile && <div className="flex flex-col h-full">{previewPanel}</div>}
|
81
|
+
</div>
|
82
|
+
);
|
83
|
+
};
|
@@ -0,0 +1,45 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { render } from '@testing-library/react';
|
3
|
+
import { PreviewPanelProps } from './PreviewPanel';
|
4
|
+
import { PreviewPanelHOC, PreviewPanelHOCProps } from './PreviewPanelHOC';
|
5
|
+
import { OverlayTriggerState } from 'react-stately';
|
6
|
+
import { DOMAttributes, FocusableElement } from '@react-types/shared';
|
7
|
+
|
8
|
+
const modalStateMock: OverlayTriggerState = {
|
9
|
+
isOpen: false,
|
10
|
+
setOpen: jest.fn(() => null),
|
11
|
+
open: jest.fn(() => null),
|
12
|
+
close: jest.fn(() => null),
|
13
|
+
toggle: jest.fn(() => null),
|
14
|
+
};
|
15
|
+
|
16
|
+
const previewModalOverlayPropsMock: DOMAttributes<FocusableElement> = {
|
17
|
+
onFocus: jest.fn(() => null),
|
18
|
+
onBlur: jest.fn(() => null),
|
19
|
+
onClick: jest.fn(() => null),
|
20
|
+
};
|
21
|
+
|
22
|
+
describe('PreviewPanelHOC', () => {
|
23
|
+
const ResourceComponentMock: React.FC = () => <div>Resource Component Mock</div>;
|
24
|
+
|
25
|
+
const defaultProps: PreviewPanelHOCProps & PreviewPanelProps = {
|
26
|
+
ResourceComponent: ResourceComponentMock,
|
27
|
+
resource: null,
|
28
|
+
modalState: modalStateMock,
|
29
|
+
previewModalOverlayProps: previewModalOverlayPropsMock,
|
30
|
+
allowedTypes: undefined,
|
31
|
+
onSelect: () => {
|
32
|
+
return;
|
33
|
+
},
|
34
|
+
onClose: () => {
|
35
|
+
return;
|
36
|
+
},
|
37
|
+
};
|
38
|
+
|
39
|
+
it('renders OriginalComponent with ResourceComponent', () => {
|
40
|
+
const WrappedComponent = PreviewPanelHOC(ResourceComponentMock);
|
41
|
+
const { getByText } = render(<WrappedComponent {...defaultProps} />);
|
42
|
+
|
43
|
+
expect(getByText('Make a selection to see more info here.')).toBeInTheDocument();
|
44
|
+
});
|
45
|
+
});
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import React, { ComponentType } from 'react';
|
2
|
+
import { PreviewPanel, PreviewPanelProps } from './PreviewPanel';
|
3
|
+
|
4
|
+
export interface PreviewPanelHOCProps {
|
5
|
+
ResourceComponent?: ComponentType<any>;
|
6
|
+
}
|
7
|
+
|
8
|
+
export const PreviewPanelHOC = (
|
9
|
+
ResourceComponent: ComponentType<any>,
|
10
|
+
): React.FC<PreviewPanelHOCProps & PreviewPanelProps> => {
|
11
|
+
const NewComponent: React.FC<PreviewPanelHOCProps & PreviewPanelProps> = (props) => {
|
12
|
+
// Render OriginalComponent and pass on its props.
|
13
|
+
return <PreviewPanel {...props} ResourceComponent={ResourceComponent} />;
|
14
|
+
};
|
15
|
+
|
16
|
+
return NewComponent;
|
17
|
+
};
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { render, fireEvent } from '@testing-library/react';
|
3
|
+
import { ResetButton, ResetButtonProps } from './ResetButton';
|
4
|
+
|
5
|
+
describe('ResetButton', () => {
|
6
|
+
const onClickMock = jest.fn();
|
7
|
+
|
8
|
+
const defaultProps: ResetButtonProps = {
|
9
|
+
onClick: onClickMock,
|
10
|
+
};
|
11
|
+
|
12
|
+
it('renders button with correct attributes', () => {
|
13
|
+
const { getByLabelText } = render(<ResetButton {...defaultProps} />);
|
14
|
+
|
15
|
+
const buttonElement = getByLabelText('Remove selection');
|
16
|
+
expect(buttonElement).toBeInTheDocument();
|
17
|
+
expect(buttonElement).toHaveAttribute('title', 'Remove selection');
|
18
|
+
expect(buttonElement).toHaveClass(
|
19
|
+
'text-gray-500 hover:text-gray-800 focus:text-gray-800 w-6 h-6 disabled:text-gray-500 disabled:cursor-not-allowed',
|
20
|
+
);
|
21
|
+
expect(buttonElement).not.toBeDisabled();
|
22
|
+
});
|
23
|
+
|
24
|
+
it('calls onClick function when clicked', () => {
|
25
|
+
const { getByLabelText } = render(<ResetButton {...defaultProps} />);
|
26
|
+
|
27
|
+
const buttonElement = getByLabelText('Remove selection');
|
28
|
+
fireEvent.click(buttonElement);
|
29
|
+
|
30
|
+
expect(onClickMock).toHaveBeenCalledTimes(1);
|
31
|
+
});
|
32
|
+
|
33
|
+
it('disables the button when isDisabled is true', () => {
|
34
|
+
const { getByLabelText } = render(<ResetButton {...defaultProps} isDisabled={true} />);
|
35
|
+
|
36
|
+
const buttonElement = getByLabelText('Remove selection');
|
37
|
+
expect(buttonElement).toBeDisabled();
|
38
|
+
fireEvent.click(buttonElement);
|
39
|
+
|
40
|
+
expect(onClickMock).toHaveBeenCalledTimes(0);
|
41
|
+
});
|
42
|
+
});
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
3
|
+
|
4
|
+
export type ResetButtonProps = {
|
5
|
+
onClick: () => void;
|
6
|
+
isDisabled?: boolean;
|
7
|
+
};
|
8
|
+
|
9
|
+
export const ResetButton = ({ onClick, isDisabled }: ResetButtonProps) => (
|
10
|
+
<div className="squiz-gb-scope">
|
11
|
+
<button
|
12
|
+
type="button"
|
13
|
+
aria-label={`Remove selection`}
|
14
|
+
title={`Remove selection`}
|
15
|
+
className="text-gray-500 hover:text-gray-800 focus:text-gray-800 w-6 h-6 disabled:text-gray-500 disabled:cursor-not-allowed"
|
16
|
+
disabled={isDisabled}
|
17
|
+
onClick={onClick}
|
18
|
+
>
|
19
|
+
<CloseRoundedIcon />
|
20
|
+
</button>
|
21
|
+
</div>
|
22
|
+
);
|
@@ -0,0 +1,65 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { render, fireEvent } from '@testing-library/react';
|
3
|
+
import { ResourceItem } from './ResourceItem';
|
4
|
+
|
5
|
+
const mockOnSelect = jest.fn();
|
6
|
+
const mockOnDrillDown = jest.fn();
|
7
|
+
const previewModalState = jest.fn();
|
8
|
+
|
9
|
+
const defaultProps: any = {
|
10
|
+
id: '123',
|
11
|
+
selected: false,
|
12
|
+
label: 'My Resource',
|
13
|
+
type: 'site',
|
14
|
+
childCount: 0,
|
15
|
+
previewModalState: previewModalState,
|
16
|
+
onSelect: mockOnSelect,
|
17
|
+
onDrillDown: mockOnDrillDown,
|
18
|
+
className: '',
|
19
|
+
};
|
20
|
+
|
21
|
+
describe('ResourceItem', () => {
|
22
|
+
it('should render the component with the correct label', () => {
|
23
|
+
const { getByText } = render(<ResourceItem {...defaultProps} />);
|
24
|
+
const labelElement = getByText('My Resource');
|
25
|
+
|
26
|
+
expect(labelElement).toBeInTheDocument();
|
27
|
+
});
|
28
|
+
|
29
|
+
it('should call the onSelect function when the button is pressed', () => {
|
30
|
+
const { getByRole } = render(<ResourceItem {...defaultProps} />);
|
31
|
+
const buttonElement = getByRole('button');
|
32
|
+
fireEvent.click(buttonElement);
|
33
|
+
|
34
|
+
expect(mockOnSelect).toHaveBeenCalledTimes(1);
|
35
|
+
});
|
36
|
+
|
37
|
+
it('should call the onDrillDown function when the drill down button is pressed', () => {
|
38
|
+
const { getByLabelText } = render(<ResourceItem {...defaultProps} childCount={1} />);
|
39
|
+
const drillDownButtonElement = getByLabelText('Drill down to My Resource children');
|
40
|
+
fireEvent.click(drillDownButtonElement);
|
41
|
+
|
42
|
+
expect(mockOnDrillDown).toHaveBeenCalledTimes(1);
|
43
|
+
});
|
44
|
+
|
45
|
+
it('should disable the button when the allowedTypes prop is not in the list of allowedItems', () => {
|
46
|
+
const { getByRole } = render(<ResourceItem {...defaultProps} allowedTypes={['page']} />);
|
47
|
+
const buttonElement = getByRole('button');
|
48
|
+
|
49
|
+
expect(buttonElement).toBeDisabled();
|
50
|
+
});
|
51
|
+
|
52
|
+
it('should not disable the button when the allowedTypes prop is in the list of allowedItems', () => {
|
53
|
+
const { getByRole } = render(<ResourceItem {...defaultProps} allowedTypes={['site']} />);
|
54
|
+
const buttonElement = getByRole('button');
|
55
|
+
|
56
|
+
expect(buttonElement).not.toBeDisabled();
|
57
|
+
});
|
58
|
+
|
59
|
+
it('should not disable the button when the allowedTypes is undefined', () => {
|
60
|
+
const { getByRole } = render(<ResourceItem {...defaultProps} />);
|
61
|
+
const buttonElement = getByRole('button');
|
62
|
+
|
63
|
+
expect(buttonElement).not.toBeDisabled();
|
64
|
+
});
|
65
|
+
});
|
@@ -0,0 +1,90 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { DOMAttributes, FocusableElement } from '@react-types/shared';
|
3
|
+
import { useOverlayTrigger } from 'react-aria';
|
4
|
+
import { OverlayTriggerState } from 'react-stately';
|
5
|
+
|
6
|
+
import { ModalOpeningButton } from '../Modal/ModalOpeningButton';
|
7
|
+
import { Icon, IconOptions, ResourceSources } from '../Icons/Icon';
|
8
|
+
|
9
|
+
interface ResourceItem<T> {
|
10
|
+
item: T;
|
11
|
+
selected?: boolean;
|
12
|
+
label: string;
|
13
|
+
type: string;
|
14
|
+
childCount?: number;
|
15
|
+
previewModalState: OverlayTriggerState;
|
16
|
+
onSelect: (node: T, overlayProps: DOMAttributes<FocusableElement>) => void;
|
17
|
+
onDrillDown?: (node: T) => void;
|
18
|
+
className: string;
|
19
|
+
allowedTypes?: string[] | undefined;
|
20
|
+
iconSource?: ResourceSources;
|
21
|
+
showChevron?: boolean;
|
22
|
+
}
|
23
|
+
|
24
|
+
const ResourceItem = <T,>({
|
25
|
+
item,
|
26
|
+
selected,
|
27
|
+
label,
|
28
|
+
type,
|
29
|
+
childCount,
|
30
|
+
previewModalState,
|
31
|
+
onSelect,
|
32
|
+
onDrillDown,
|
33
|
+
className,
|
34
|
+
allowedTypes,
|
35
|
+
iconSource = 'matrix',
|
36
|
+
showChevron = false,
|
37
|
+
}: ResourceItem<T>) => {
|
38
|
+
const { triggerProps, overlayProps } = useOverlayTrigger({ type: 'dialog' }, previewModalState);
|
39
|
+
const isDisabled = allowedTypes !== undefined && !allowedTypes.includes(type);
|
40
|
+
const title = isDisabled ? "You can't select this item" : label;
|
41
|
+
|
42
|
+
return (
|
43
|
+
<li className={`flex items-stretch p-1 bg-white border-1 border-grey-200 min-h-[64px] ${className}`}>
|
44
|
+
<ModalOpeningButton
|
45
|
+
type="button"
|
46
|
+
{...triggerProps}
|
47
|
+
isDisabled={isDisabled}
|
48
|
+
onPress={() => onSelect(item, overlayProps)}
|
49
|
+
aria-label={childCount === undefined ? `Drill down to ${label} children` : ''}
|
50
|
+
className={`
|
51
|
+
relative grow flex items-center px-4 py-2 rounded outline-0 ${selected ? 'bg-blue-100 text-blue-400' : ''} ${
|
52
|
+
childCount !== undefined && childCount > 0 ? 'mr-2' : ''
|
53
|
+
} ${isDisabled ? 'font-normal text-gray-600 cursor-not-allowed' : 'hover:bg-gray-50 focus:bg-gray-50'}
|
54
|
+
`}
|
55
|
+
title={title}
|
56
|
+
>
|
57
|
+
<Icon
|
58
|
+
icon={type as IconOptions}
|
59
|
+
resourceSource={iconSource}
|
60
|
+
aria-label={type}
|
61
|
+
className={`mr-4 shrink-0 ${isDisabled && 'opacity-40'}`}
|
62
|
+
/>
|
63
|
+
<span className={`relative flex items-center ${selected ? 'mr-8' : ''}`}>
|
64
|
+
<span className="line-clamp-2 text-left break-word">{label}</span>
|
65
|
+
{selected && <Icon icon={'selected' as IconOptions} aria-label="selected" className="absolute -right-8" />}
|
66
|
+
</span>
|
67
|
+
{childCount === undefined && showChevron && (
|
68
|
+
<Icon icon={'arrow-right' as IconOptions} className="absolute right-5" />
|
69
|
+
)}
|
70
|
+
</ModalOpeningButton>
|
71
|
+
{childCount !== undefined && childCount > 0 && onDrillDown && (
|
72
|
+
<button
|
73
|
+
type="button"
|
74
|
+
aria-label={`Drill down to ${label} children`}
|
75
|
+
onClick={() => onDrillDown(item)}
|
76
|
+
className={`relative shrink-0 flex items-center p-4 rounded outline-0 before:w-px before:h-[calc(100%-0.75rem)] before:bg-gray-200 before:absolute before:top-1.5 before:-left-1 hover:bg-gray-50 focus:bg-gray-50`}
|
77
|
+
>
|
78
|
+
<span className="ml-auto flex items-center">
|
79
|
+
<span className="truncate w-14 text-right" title={String(childCount)}>
|
80
|
+
{childCount}
|
81
|
+
</span>
|
82
|
+
<Icon icon={'arrow-right' as IconOptions} className="ml-1" />
|
83
|
+
</span>
|
84
|
+
</button>
|
85
|
+
)}
|
86
|
+
</li>
|
87
|
+
);
|
88
|
+
};
|
89
|
+
|
90
|
+
export { ResourceItem };
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { render, fireEvent } from '@testing-library/react';
|
3
|
+
import { ResourceState } from './ResourceState';
|
4
|
+
|
5
|
+
const defaultProps: any = {
|
6
|
+
state: 'error',
|
7
|
+
message: 'This is a test error!',
|
8
|
+
handleReload: jest.fn(),
|
9
|
+
};
|
10
|
+
|
11
|
+
describe('ResourceError', () => {
|
12
|
+
it('should render the component with the correct error message', () => {
|
13
|
+
const { getByText } = render(<ResourceState {...defaultProps} />);
|
14
|
+
const errorMessage = getByText(defaultProps.message);
|
15
|
+
|
16
|
+
expect(errorMessage).toBeInTheDocument();
|
17
|
+
});
|
18
|
+
|
19
|
+
it('should call the reload function when the button is pressed', () => {
|
20
|
+
const { getByRole } = render(<ResourceState {...defaultProps} />);
|
21
|
+
const buttonElement = getByRole('button');
|
22
|
+
fireEvent.click(buttonElement);
|
23
|
+
|
24
|
+
expect(defaultProps.handleReload).toHaveBeenCalledTimes(1);
|
25
|
+
});
|
26
|
+
});
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { StoryFn, Meta } from '@storybook/react';
|
3
|
+
|
4
|
+
import { ResourceState } from './ResourceState';
|
5
|
+
|
6
|
+
export default {
|
7
|
+
title: 'Resource State',
|
8
|
+
component: ResourceState,
|
9
|
+
} as Meta<typeof ResourceState>;
|
10
|
+
|
11
|
+
const Template: StoryFn<typeof ResourceState> = ({ state, message }) => (
|
12
|
+
<ResourceState state={state} message={message} handleReload={() => alert('Resource browser reload')} />
|
13
|
+
);
|
14
|
+
|
15
|
+
export const Error = Template.bind({});
|
16
|
+
Error.args = {
|
17
|
+
state: 'error',
|
18
|
+
message: 'This is a resource browser error!',
|
19
|
+
};
|
20
|
+
|
21
|
+
export const Empty = Template.bind({});
|
22
|
+
Empty.args = {
|
23
|
+
state: 'empty',
|
24
|
+
};
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Icon, IconOptions } from '../Icons/Icon';
|
3
|
+
|
4
|
+
interface ResourceState {
|
5
|
+
state: 'error' | 'empty';
|
6
|
+
message?: string;
|
7
|
+
handleReload: () => void;
|
8
|
+
}
|
9
|
+
|
10
|
+
const ResourceState = function ({ state, message, handleReload }: ResourceState) {
|
11
|
+
return (
|
12
|
+
<div className="flex flex-col items-center rounded-lg py-8 bg-white h-204 gap-3">
|
13
|
+
<Icon icon={state as IconOptions} aria-hidden />
|
14
|
+
{/* Message */}
|
15
|
+
<span className="text-md text-gray-800 font-semibold leading-5">
|
16
|
+
{state === 'empty' ? 'There are no items to display' : message}
|
17
|
+
</span>
|
18
|
+
{/* Retry button */}
|
19
|
+
<button
|
20
|
+
type="button"
|
21
|
+
onClick={handleReload}
|
22
|
+
className="flex flex-row items-center justify-center gap-3 bg-black bg-opacity-10 h-9 mt-3 rounded text-md font-bold text-gray-700 py-2 px-3"
|
23
|
+
>
|
24
|
+
<Icon icon={state === 'empty' ? 'back' : 'retry'} aria-hidden />
|
25
|
+
{state === 'empty' ? 'Back to source list' : 'Try again'}
|
26
|
+
</button>
|
27
|
+
</div>
|
28
|
+
);
|
29
|
+
};
|
30
|
+
|
31
|
+
export { ResourceState };
|