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