@squiz/formatted-text-editor 1.33.1-alpha.2 → 1.33.1-alpha.4
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/demo/App.tsx +4 -24
- package/demo/AppContext.tsx +28 -0
- package/demo/index.scss +0 -2
- package/demo/main.tsx +2 -0
- package/demo/resources.json +28 -0
- package/demo/sources.json +23 -0
- package/lib/Editor/EditorContext.d.ts +0 -7
- package/lib/Editor/EditorContext.js +0 -2
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +16 -32
- package/lib/EditorToolbar/Tools/Image/ImageModal.js +3 -2
- package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +18 -58
- package/lib/EditorToolbar/Tools/Link/LinkModal.js +3 -2
- package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +1 -1
- package/lib/Extensions/Extensions.js +0 -2
- package/lib/Extensions/ImageExtension/AssetImageExtension.d.ts +0 -1
- package/lib/Extensions/ImageExtension/AssetImageExtension.js +1 -2
- package/lib/Extensions/LinkExtension/AssetLinkExtension.d.ts +0 -1
- package/lib/Extensions/LinkExtension/AssetLinkExtension.js +2 -3
- package/lib/Extensions/LinkExtension/LinkExtension.js +1 -1
- package/lib/index.css +84 -4
- package/lib/types.d.ts +3 -3
- package/lib/ui/Fields/Checkbox/Checkbox.d.ts +8 -0
- package/lib/ui/Fields/Checkbox/Checkbox.js +47 -0
- package/lib/ui/Fields/Input/Input.d.ts +2 -4
- package/lib/ui/Fields/Input/Input.js +3 -9
- package/lib/ui/Fields/InputContainer/InputContainer.d.ts +9 -0
- package/lib/ui/Fields/InputContainer/InputContainer.js +16 -0
- package/lib/ui/Fields/MatrixAsset/MatrixAsset.d.ts +17 -0
- package/lib/ui/Fields/MatrixAsset/MatrixAsset.js +29 -0
- package/lib/ui/Modal/Modal.d.ts +1 -0
- package/lib/ui/Modal/Modal.js +3 -2
- package/lib/ui/Tabs/Tabs.d.ts +10 -0
- package/lib/ui/Tabs/Tabs.js +46 -0
- package/lib/utils/validation.d.ts +2 -1
- package/lib/utils/validation.js +8 -2
- package/package.json +4 -3
- package/src/Editor/Editor.spec.tsx +1 -1
- package/src/Editor/EditorContext.spec.tsx +11 -13
- package/src/Editor/EditorContext.ts +0 -11
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +29 -12
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +37 -53
- package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +76 -49
- package/src/EditorToolbar/Tools/Image/ImageModal.spec.tsx +1 -0
- package/src/EditorToolbar/Tools/Image/ImageModal.tsx +3 -2
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +22 -13
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +35 -57
- package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +52 -36
- package/src/EditorToolbar/Tools/Link/LinkModal.tsx +3 -2
- package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +47 -4
- package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +3 -2
- package/src/Extensions/Extensions.ts +0 -2
- package/src/Extensions/ImageExtension/AssetImageExtension.ts +1 -3
- package/src/Extensions/LinkExtension/AssetLinkExtension.ts +2 -4
- package/src/Extensions/LinkExtension/LinkExtension.ts +1 -1
- package/src/index.scss +1 -0
- package/src/types.ts +7 -5
- package/src/ui/Fields/Checkbox/Checkbox.spec.tsx +50 -0
- package/src/ui/Fields/Checkbox/Checkbox.tsx +49 -0
- package/src/ui/Fields/Checkbox/_checkbox.scss +26 -0
- package/src/ui/Fields/Input/Input.tsx +4 -18
- package/src/ui/Fields/InputContainer/InputContainer.spec.tsx +18 -0
- package/src/ui/Fields/InputContainer/InputContainer.tsx +29 -0
- package/src/ui/Fields/MatrixAsset/MatrixAsset.spec.tsx +103 -0
- package/src/ui/Fields/MatrixAsset/MatrixAsset.tsx +55 -0
- package/src/ui/Modal/FormModal.spec.tsx +2 -1
- package/src/ui/Modal/Modal.spec.tsx +15 -7
- package/src/ui/Modal/Modal.tsx +4 -2
- package/src/ui/Tabs/Tabs.spec.tsx +44 -0
- package/src/ui/Tabs/Tabs.tsx +41 -0
- package/src/ui/_forms.scss +4 -2
- package/src/utils/validation.spec.ts +22 -0
- package/src/utils/validation.ts +9 -1
- package/tests/index.ts +2 -0
- package/tests/mockResourceBrowserContext.tsx +63 -0
- package/tests/renderWithContext.tsx +18 -0
- package/tests/renderWithEditor.tsx +18 -21
- package/vite.config.ts +8 -0
- package/lib/ui/Fields/Select/Select.d.ts +0 -12
- package/lib/ui/Fields/Select/Select.js +0 -53
@@ -0,0 +1,103 @@
|
|
1
|
+
import '@testing-library/jest-dom';
|
2
|
+
import React from 'react';
|
3
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
4
|
+
import { MatrixAsset } from './MatrixAsset';
|
5
|
+
import { mockResourceBrowserContext } from '../../../../tests';
|
6
|
+
|
7
|
+
describe('MatrixAsset', () => {
|
8
|
+
it('Renders empty state when no value is provided', () => {
|
9
|
+
render(<MatrixAsset modalTitle="Insert asset" onChange={jest.fn()} />);
|
10
|
+
|
11
|
+
expect(screen.getByRole('button', { name: 'Choose asset' })).toBeInTheDocument();
|
12
|
+
});
|
13
|
+
|
14
|
+
it('Renders a selected state when a value is provided', () => {
|
15
|
+
const { MockResourceBrowserContext } = mockResourceBrowserContext({
|
16
|
+
sources: [{ id: 'my-source-id' }],
|
17
|
+
resources: [{ id: 'my-resource-id', name: 'My resource' }],
|
18
|
+
});
|
19
|
+
|
20
|
+
render(
|
21
|
+
<MockResourceBrowserContext>
|
22
|
+
<MatrixAsset
|
23
|
+
modalTitle="Insert asset"
|
24
|
+
value={{
|
25
|
+
matrixIdentifier: 'my-source-id',
|
26
|
+
matrixAssetId: 'my-resource-id',
|
27
|
+
addional: 'addditional data',
|
28
|
+
}}
|
29
|
+
onChange={jest.fn()}
|
30
|
+
/>
|
31
|
+
</MockResourceBrowserContext>,
|
32
|
+
);
|
33
|
+
|
34
|
+
expect(screen.getByText('My resource')).toBeInTheDocument();
|
35
|
+
});
|
36
|
+
|
37
|
+
it('Calls onChange with expected value when resources is selected', async () => {
|
38
|
+
const handleChange = jest.fn();
|
39
|
+
const { MockResourceBrowserContext, selectResource } = mockResourceBrowserContext({
|
40
|
+
sources: [{ id: 'my-source-id' }],
|
41
|
+
resources: [{ id: 'my-resource-id', name: 'My resource' }],
|
42
|
+
});
|
43
|
+
|
44
|
+
render(
|
45
|
+
<MockResourceBrowserContext>
|
46
|
+
<MatrixAsset
|
47
|
+
modalTitle="Insert asset"
|
48
|
+
value={{ matrixIdentifier: undefined, matrixAssetId: undefined, additional: 'additional data' }}
|
49
|
+
onChange={handleChange}
|
50
|
+
/>
|
51
|
+
</MockResourceBrowserContext>,
|
52
|
+
);
|
53
|
+
|
54
|
+
await selectResource(screen.getByRole('button', { name: 'Choose asset' }), 'My resource');
|
55
|
+
|
56
|
+
expect(handleChange).toHaveBeenCalledWith({
|
57
|
+
target: {
|
58
|
+
value: {
|
59
|
+
additional: 'additional data',
|
60
|
+
matrixAssetId: 'my-resource-id',
|
61
|
+
matrixIdentifier: 'my-source-id',
|
62
|
+
},
|
63
|
+
},
|
64
|
+
});
|
65
|
+
});
|
66
|
+
|
67
|
+
it('Calls onChange with expected value when resources is cleared', async () => {
|
68
|
+
const handleChange = jest.fn();
|
69
|
+
const { MockResourceBrowserContext } = mockResourceBrowserContext({
|
70
|
+
sources: [{ id: 'my-source-id' }],
|
71
|
+
resources: [
|
72
|
+
{ id: 'my-resource-id', name: 'My resource' },
|
73
|
+
{ id: 'updated-resource-id', name: 'Updated resource' },
|
74
|
+
],
|
75
|
+
});
|
76
|
+
|
77
|
+
render(
|
78
|
+
<MockResourceBrowserContext>
|
79
|
+
<MatrixAsset
|
80
|
+
modalTitle="Insert asset"
|
81
|
+
value={{
|
82
|
+
matrixIdentifier: 'my-source-id',
|
83
|
+
matrixAssetId: 'my-resource-id',
|
84
|
+
additional: 'additional data',
|
85
|
+
}}
|
86
|
+
onChange={handleChange}
|
87
|
+
/>
|
88
|
+
</MockResourceBrowserContext>,
|
89
|
+
);
|
90
|
+
|
91
|
+
fireEvent.click(screen.getByRole('button', { name: 'Remove selection' }));
|
92
|
+
|
93
|
+
expect(handleChange).toHaveBeenCalledWith({
|
94
|
+
target: {
|
95
|
+
value: {
|
96
|
+
additional: 'additional data',
|
97
|
+
matrixAssetId: undefined,
|
98
|
+
matrixIdentifier: undefined,
|
99
|
+
},
|
100
|
+
},
|
101
|
+
});
|
102
|
+
});
|
103
|
+
});
|
@@ -0,0 +1,55 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { HydratedResourceReference, ResourceBrowserInput } from '@squiz/resource-browser';
|
3
|
+
import { InputContainer, InputContainerProps } from '../InputContainer/InputContainer';
|
4
|
+
|
5
|
+
type MatrixAssetValue = {
|
6
|
+
matrixIdentifier?: string;
|
7
|
+
matrixAssetId?: string;
|
8
|
+
};
|
9
|
+
|
10
|
+
export type MatrixAssetProps<T extends MatrixAssetValue> = Omit<InputContainerProps, 'children'> & {
|
11
|
+
modalTitle: string;
|
12
|
+
allowedTypes?: string[];
|
13
|
+
value?: T | null;
|
14
|
+
// LinkForm contains a "target" property.
|
15
|
+
// react-hook-form treats the presence of this property as an "Event" object being passed through, see:
|
16
|
+
// https://github.com/react-hook-form/react-hook-form/blob/master/src/logic/getEventValue.ts
|
17
|
+
// Nest the value under a "target" object to work around the behaviour.
|
18
|
+
onChange: (value: { target: { value: T } }) => void;
|
19
|
+
};
|
20
|
+
|
21
|
+
export const MatrixAsset = <T extends MatrixAssetValue>({
|
22
|
+
modalTitle,
|
23
|
+
allowedTypes,
|
24
|
+
value,
|
25
|
+
onChange,
|
26
|
+
...props
|
27
|
+
}: MatrixAssetProps<T>) => {
|
28
|
+
return (
|
29
|
+
<InputContainer {...props}>
|
30
|
+
<ResourceBrowserInput
|
31
|
+
modalTitle={modalTitle}
|
32
|
+
allowedTypes={allowedTypes}
|
33
|
+
value={
|
34
|
+
value && value.matrixIdentifier && value.matrixAssetId
|
35
|
+
? {
|
36
|
+
source: value.matrixIdentifier,
|
37
|
+
resource: value.matrixAssetId,
|
38
|
+
}
|
39
|
+
: null
|
40
|
+
}
|
41
|
+
onChange={(reference: HydratedResourceReference | null) => {
|
42
|
+
onChange({
|
43
|
+
target: {
|
44
|
+
value: {
|
45
|
+
...value,
|
46
|
+
matrixIdentifier: reference?.source?.id,
|
47
|
+
matrixAssetId: reference?.resource?.id,
|
48
|
+
} as T,
|
49
|
+
},
|
50
|
+
});
|
51
|
+
}}
|
52
|
+
/>
|
53
|
+
</InputContainer>
|
54
|
+
);
|
55
|
+
};
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import '@testing-library/jest-dom';
|
2
2
|
import { fireEvent, render, screen } from '@testing-library/react';
|
3
|
+
import InsertLinkRoundedIcon from '@mui/icons-material/InsertLinkRounded';
|
3
4
|
import React from 'react';
|
4
5
|
import FormModal from './FormModal';
|
5
6
|
|
@@ -8,7 +9,7 @@ describe('FormModal', () => {
|
|
8
9
|
const handleSubmit = jest.fn();
|
9
10
|
|
10
11
|
render(
|
11
|
-
<FormModal title="Modal title" onCancel={jest.fn()}>
|
12
|
+
<FormModal title="Modal title" icon={<InsertLinkRoundedIcon />} onCancel={jest.fn()}>
|
12
13
|
<form onSubmit={handleSubmit}></form>
|
13
14
|
</FormModal>,
|
14
15
|
);
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import '@testing-library/jest-dom';
|
2
2
|
import { render, screen, fireEvent } from '@testing-library/react';
|
3
|
+
import InsertLinkRoundedIcon from '@mui/icons-material/InsertLinkRounded';
|
3
4
|
import React from 'react';
|
4
5
|
import Modal from './Modal';
|
5
6
|
import { Select } from '../Fields/Select/Select';
|
@@ -11,7 +12,7 @@ describe('Modal', () => {
|
|
11
12
|
|
12
13
|
const ModalComponent = () => {
|
13
14
|
return (
|
14
|
-
<Modal title="Modal title" onCancel={mockOnCancel}>
|
15
|
+
<Modal title="Modal title" icon={<InsertLinkRoundedIcon />} onCancel={mockOnCancel}>
|
15
16
|
<div>I am a child in the modal</div>
|
16
17
|
</Modal>
|
17
18
|
);
|
@@ -26,6 +27,13 @@ describe('Modal', () => {
|
|
26
27
|
expect(modalHeading).toBeInTheDocument();
|
27
28
|
});
|
28
29
|
|
30
|
+
it('Renders the modal image', () => {
|
31
|
+
render(<ModalComponent />);
|
32
|
+
// Check that the modal image displays
|
33
|
+
const modalImage = screen.getByTestId('InsertLinkRoundedIcon');
|
34
|
+
expect(modalImage).toBeInTheDocument();
|
35
|
+
});
|
36
|
+
|
29
37
|
it('Renders the child', () => {
|
30
38
|
render(<ModalComponent />);
|
31
39
|
// Check that the modal heading displays
|
@@ -52,7 +60,7 @@ describe('Modal', () => {
|
|
52
60
|
|
53
61
|
it('Renders the submit button if there is a submit function supplied', () => {
|
54
62
|
render(
|
55
|
-
<Modal title="Modal title" onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
63
|
+
<Modal title="Modal title" icon={<InsertLinkRoundedIcon />} onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
56
64
|
<div>I am a child in the modal</div>
|
57
65
|
</Modal>,
|
58
66
|
);
|
@@ -63,7 +71,7 @@ describe('Modal', () => {
|
|
63
71
|
|
64
72
|
it('Checks that the submit function fires if you click on the submit button', () => {
|
65
73
|
render(
|
66
|
-
<Modal title="Modal title" onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
74
|
+
<Modal title="Modal title" icon={<InsertLinkRoundedIcon />} onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
67
75
|
<div>I am a child in the modal</div>
|
68
76
|
</Modal>,
|
69
77
|
);
|
@@ -77,7 +85,7 @@ describe('Modal', () => {
|
|
77
85
|
|
78
86
|
it('Calls the onSubmit handler when the enter key is pressed', () => {
|
79
87
|
render(
|
80
|
-
<Modal title="Modal title" onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
88
|
+
<Modal title="Modal title" icon={<InsertLinkRoundedIcon />} onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
81
89
|
<div>Modal content</div>
|
82
90
|
</Modal>,
|
83
91
|
);
|
@@ -89,7 +97,7 @@ describe('Modal', () => {
|
|
89
97
|
|
90
98
|
it('Calls the onCancel handler when the escape key is pressed', () => {
|
91
99
|
render(
|
92
|
-
<Modal title="Modal title" onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
100
|
+
<Modal title="Modal title" icon={<InsertLinkRoundedIcon />} onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
93
101
|
<div>Modal content</div>
|
94
102
|
</Modal>,
|
95
103
|
);
|
@@ -101,7 +109,7 @@ describe('Modal', () => {
|
|
101
109
|
|
102
110
|
it('Auto-focuses on the first non-hidden input on mount', () => {
|
103
111
|
render(
|
104
|
-
<Modal title="Modal title" onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
112
|
+
<Modal title="Modal title" icon={<InsertLinkRoundedIcon />} onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
105
113
|
<>
|
106
114
|
<input id="hidden-input" type="hidden" />
|
107
115
|
<label htmlFor="my-input">My input</label>
|
@@ -115,7 +123,7 @@ describe('Modal', () => {
|
|
115
123
|
|
116
124
|
it('Auto-focuses on the first select field on mount', () => {
|
117
125
|
render(
|
118
|
-
<Modal title="Modal title" onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
126
|
+
<Modal title="Modal title" icon={<InsertLinkRoundedIcon />} onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
119
127
|
<>
|
120
128
|
<Select label="Dropdown" name="select" options={{}} />
|
121
129
|
<Input label="Input" name="input" />
|
package/src/ui/Modal/Modal.tsx
CHANGED
@@ -6,6 +6,7 @@ import clsx from 'clsx';
|
|
6
6
|
|
7
7
|
export type ModalProps = {
|
8
8
|
title: string;
|
9
|
+
icon: ReactElement;
|
9
10
|
children: ReactElement;
|
10
11
|
onCancel: () => void;
|
11
12
|
onSubmit?: () => void;
|
@@ -13,7 +14,7 @@ export type ModalProps = {
|
|
13
14
|
};
|
14
15
|
|
15
16
|
const Modal = (
|
16
|
-
{ children, title, onCancel, onSubmit, className }: ModalProps,
|
17
|
+
{ children, title, icon, onCancel, onSubmit, className }: ModalProps,
|
17
18
|
ref: ForwardedRef<HTMLDivElement>,
|
18
19
|
): ReactElement => {
|
19
20
|
const content = useRef<HTMLDivElement>(null);
|
@@ -55,7 +56,8 @@ const Modal = (
|
|
55
56
|
<div ref={ref} className={clsx('squiz-fte-modal-wrapper', className)} tabIndex={-1}>
|
56
57
|
<div className="w-modal-sm my-6 mx-auto">
|
57
58
|
<div className="squiz-fte-modal">
|
58
|
-
<div className="squiz-fte-modal-header p-6 pb-
|
59
|
+
<div className="squiz-fte-modal-header p-6 pb-4">
|
60
|
+
<div className="squiz-fte-modal-header-icon mr-1.5 mt-[-1px]">{icon}</div>
|
59
61
|
<h2 className="font-semibold text-gray-900 text-heading-2">{title}</h2>
|
60
62
|
<button
|
61
63
|
type="button"
|
@@ -0,0 +1,44 @@
|
|
1
|
+
import '@testing-library/jest-dom';
|
2
|
+
import React from 'react';
|
3
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
4
|
+
import { Tabs, TabOptions } from './Tabs';
|
5
|
+
import { MarkName } from '../../Extensions/Extensions';
|
6
|
+
|
7
|
+
const linkTypeOptions: TabOptions = {
|
8
|
+
[MarkName.AssetLink]: { label: 'From source' },
|
9
|
+
[MarkName.Link]: { label: 'From URL' },
|
10
|
+
};
|
11
|
+
|
12
|
+
describe('Tabs', () => {
|
13
|
+
const mockOnChange = jest.fn();
|
14
|
+
|
15
|
+
const TabsComponent = () => {
|
16
|
+
return <Tabs value={MarkName.AssetLink} options={linkTypeOptions} onChange={mockOnChange} />;
|
17
|
+
};
|
18
|
+
|
19
|
+
it('renders the Tabs component', () => {
|
20
|
+
render(<TabsComponent />);
|
21
|
+
expect(screen.getByText('From source')).toBeInTheDocument();
|
22
|
+
expect(screen.getByText('From URL')).toBeInTheDocument();
|
23
|
+
});
|
24
|
+
|
25
|
+
it('defaults active to asset link', () => {
|
26
|
+
render(<TabsComponent />);
|
27
|
+
expect(screen.getByRole('tab', { selected: true })).toHaveTextContent('From source');
|
28
|
+
});
|
29
|
+
|
30
|
+
it('changes selected state when clicking the other tab', () => {
|
31
|
+
render(<TabsComponent />);
|
32
|
+
|
33
|
+
const assetLink = screen.getByTestId('assetLink');
|
34
|
+
const link = screen.getByTestId('link');
|
35
|
+
|
36
|
+
expect(assetLink).toHaveAttribute('aria-selected', 'true');
|
37
|
+
expect(link).not.toHaveAttribute('aria-selected', 'true');
|
38
|
+
|
39
|
+
fireEvent.click(link);
|
40
|
+
|
41
|
+
expect(assetLink).not.toHaveAttribute('aria-selected', 'true');
|
42
|
+
expect(link).toHaveAttribute('aria-selected', 'true');
|
43
|
+
});
|
44
|
+
});
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import { Tab } from '@headlessui/react';
|
2
|
+
import React, { Fragment } from 'react';
|
3
|
+
import clsx from 'clsx';
|
4
|
+
|
5
|
+
export type TabOptions = Record<string, TabOption>;
|
6
|
+
export type TabOption = {
|
7
|
+
label: string;
|
8
|
+
};
|
9
|
+
|
10
|
+
export type TabsProps = {
|
11
|
+
value: string;
|
12
|
+
options: TabOptions;
|
13
|
+
onChange?: (value: string) => void;
|
14
|
+
};
|
15
|
+
|
16
|
+
export const Tabs = ({ value, options, onChange }: TabsProps) => (
|
17
|
+
<Tab.Group
|
18
|
+
// Check what index the selected tab is, otherwise default to first tab
|
19
|
+
defaultIndex={Object.keys(options).indexOf(value) || 0}
|
20
|
+
// Check what the selected tab key is and trigger onChange
|
21
|
+
onChange={(index) => {
|
22
|
+
const selectedTab = Object.keys(options)[index];
|
23
|
+
onChange?.(selectedTab);
|
24
|
+
}}
|
25
|
+
>
|
26
|
+
<Tab.List className="grid grid-flow-col h-10 border-b border-gray-300">
|
27
|
+
{Object.entries(options).map(([key, option]) => (
|
28
|
+
<Tab key={key} as={Fragment}>
|
29
|
+
{({ selected }) => (
|
30
|
+
<div className="flex flex-col justify-between" data-testid={key} aria-selected={selected}>
|
31
|
+
<button type="button" className={clsx('mt-[7px] text-gray-800', selected && 'font-bold')}>
|
32
|
+
{option.label}
|
33
|
+
</button>
|
34
|
+
{selected && <span className="h-[3px] bg-gray-800 w-11/12 self-center rounded-t-sm" />}
|
35
|
+
</div>
|
36
|
+
)}
|
37
|
+
</Tab>
|
38
|
+
))}
|
39
|
+
</Tab.List>
|
40
|
+
</Tab.Group>
|
41
|
+
);
|
package/src/ui/_forms.scss
CHANGED
@@ -6,6 +6,7 @@
|
|
6
6
|
@apply mb-1 text-md font-semibold text-gray-600;
|
7
7
|
}
|
8
8
|
&-form-control {
|
9
|
+
height: 36px;
|
9
10
|
padding: 6px 12px;
|
10
11
|
@apply placeholder-slate-300 text-gray-800 relative bg-white rounded text-md border-2 border-gray-300 outline-0 focus:outline-0 focus:border-blue-300 w-full;
|
11
12
|
&:focus,
|
@@ -14,10 +15,11 @@
|
|
14
15
|
}
|
15
16
|
}
|
16
17
|
&-invalid-form-field {
|
17
|
-
.squiz-fte-form-control
|
18
|
+
.squiz-fte-form-control,
|
19
|
+
.resource-picker {
|
18
20
|
@apply border-red-300 bg-no-repeat pr-8;
|
19
21
|
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHdpZHRoPSIyNCI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0ibm9uZSIvPjxnIGNsYXNzPSJjdXJyZW50TGF5ZXIiPjxwYXRoIGQ9Ik00LjQ3IDIxaDE1LjA2YzEuNTQgMCAyLjUtMS42NyAxLjczLTNMMTMuNzMgNC45OWMtLjc3LTEuMzMtMi42OS0xLjMzLTMuNDYgMEwyLjc0IDE4Yy0uNzcgMS4zMy4xOSAzIDEuNzMgM3pNMTIgMTRjLS41NSAwLTEtLjQ1LTEtMXYtMmMwLS41NS40NS0xIDEtMXMxIC40NSAxIDF2MmMwIC41NS0uNDUgMS0xIDF6bTEgNGgtMnYtMmgydjJ6IiBjbGFzcz0ic2VsZWN0ZWQiIGZpbGw9IiNkNzIzMjEiLz48L2c+PC9zdmc+');
|
20
|
-
background-position:
|
22
|
+
background-position: center right 0.25rem;
|
21
23
|
background-size: 1.5rem;
|
22
24
|
}
|
23
25
|
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import { hasProperties, noEmptySpacesValidation } from './validation';
|
2
|
+
|
3
|
+
describe('validation', () => {
|
4
|
+
it.each([
|
5
|
+
['String with only spaces', ' ', 'Empty space is not allowed'],
|
6
|
+
['Empty string', '', undefined],
|
7
|
+
['Non-empty string', 'test value', undefined],
|
8
|
+
['String with trailing/leading spaces', ' test value ', undefined],
|
9
|
+
])('noEmptySpacesValidation - %s', (description: string, value: string, expected: string | undefined) => {
|
10
|
+
expect(noEmptySpacesValidation(value)).toBe(expected);
|
11
|
+
});
|
12
|
+
|
13
|
+
it.each([
|
14
|
+
['Value is null', null, ['prop1'], 'Field is invalid'],
|
15
|
+
['Value is undefined', undefined, ['prop1'], 'Field is invalid'],
|
16
|
+
['Value has none of the required props', { prop2: 'Not what we want' }, ['prop1'], 'Field is invalid'],
|
17
|
+
['Value has all of the required props', { prop1: 'Valid' }, ['prop1'], undefined],
|
18
|
+
['Value has all of the required props + extra', { prop1: 'Valid', prop2: 'Another prop' }, ['prop1'], undefined],
|
19
|
+
])('hasProperties - %s', (description: string, value: any, properties: string[], expected: string | undefined) => {
|
20
|
+
expect(hasProperties('Field is invalid', properties)(value)).toBe(expected);
|
21
|
+
});
|
22
|
+
});
|
package/src/utils/validation.ts
CHANGED
@@ -1,8 +1,16 @@
|
|
1
|
-
export const noEmptySpacesValidation =
|
1
|
+
export const noEmptySpacesValidation = (value: string | undefined) => {
|
2
2
|
if (value && !(value.trim().length > 0)) {
|
3
3
|
return 'Empty space is not allowed';
|
4
4
|
}
|
5
5
|
};
|
6
6
|
|
7
|
+
export const hasProperties =
|
8
|
+
<T>(message: string, properties: (keyof T)[]) =>
|
9
|
+
(value: T) => {
|
10
|
+
if (!value || properties.filter((property) => value[property]).length !== properties.length) {
|
11
|
+
return message;
|
12
|
+
}
|
13
|
+
};
|
14
|
+
|
7
15
|
export const regexDataURI =
|
8
16
|
/^data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@/?%\s]*)$/i;
|
package/tests/index.ts
CHANGED
@@ -0,0 +1,63 @@
|
|
1
|
+
import React, { ReactNode } from 'react';
|
2
|
+
import { ResourceBrowserContext, Source, Resource, ResourceReference } from '@squiz/resource-browser';
|
3
|
+
import { fireEvent, screen } from '@testing-library/react';
|
4
|
+
import { DeepPartial } from '../src/types';
|
5
|
+
|
6
|
+
export type MockResourceBrowserContextOptions = DeepPartial<{
|
7
|
+
sources: Source[];
|
8
|
+
resources: Resource[];
|
9
|
+
}>;
|
10
|
+
|
11
|
+
const mockResource = (resource: DeepPartial<Resource> = {}): Resource =>
|
12
|
+
({
|
13
|
+
id: 'default-resource',
|
14
|
+
name: 'Default resource',
|
15
|
+
url: 'https://default-resource/',
|
16
|
+
urls: [],
|
17
|
+
childCount: 0,
|
18
|
+
type: {
|
19
|
+
code: 'unspecified',
|
20
|
+
name: 'Unspecified',
|
21
|
+
},
|
22
|
+
status: {
|
23
|
+
code: 'live',
|
24
|
+
name: 'Live',
|
25
|
+
},
|
26
|
+
...resource,
|
27
|
+
} as Resource);
|
28
|
+
|
29
|
+
const mockSource = (source: DeepPartial<Source>): Source => ({
|
30
|
+
id: 'default-source',
|
31
|
+
name: 'Default source',
|
32
|
+
...source,
|
33
|
+
nodes: (source.nodes || [mockResource()]).map((resource) => mockResource(resource)),
|
34
|
+
});
|
35
|
+
|
36
|
+
export const mockResourceBrowserContext = ({ sources, resources }: MockResourceBrowserContextOptions) => {
|
37
|
+
sources = (sources || []).map((source) => mockSource(source));
|
38
|
+
resources = (resources || []).map((resource) => mockResource(resource));
|
39
|
+
|
40
|
+
const onRequestSources = jest.fn().mockResolvedValue(sources);
|
41
|
+
const onRequestChildren = jest.fn().mockResolvedValue(resources);
|
42
|
+
const onRequestResource = jest
|
43
|
+
.fn()
|
44
|
+
.mockImplementation(
|
45
|
+
(reference: ResourceReference) => resources?.find((resource) => resource.id === reference.resource) || null,
|
46
|
+
);
|
47
|
+
|
48
|
+
return {
|
49
|
+
MockResourceBrowserContext: ({ children }: { children: ReactNode }) => (
|
50
|
+
<ResourceBrowserContext.Provider value={{ onRequestSources, onRequestChildren, onRequestResource }}>
|
51
|
+
{children}
|
52
|
+
</ResourceBrowserContext.Provider>
|
53
|
+
),
|
54
|
+
selectResource: async (opener: HTMLElement, resourceName: string) => {
|
55
|
+
const sourceLabel = `Drill down to ${sources?.[0]?.nodes?.[0]?.name} children`;
|
56
|
+
|
57
|
+
fireEvent.click(opener);
|
58
|
+
fireEvent.click(await screen.findByRole('button', { name: sourceLabel }));
|
59
|
+
fireEvent.click(await screen.findByTitle(resourceName));
|
60
|
+
fireEvent.click(await screen.findByRole('button', { name: 'Select' }));
|
61
|
+
},
|
62
|
+
};
|
63
|
+
};
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import React, { ReactElement } from 'react';
|
2
|
+
import { render, RenderOptions, RenderResult } from '@testing-library/react';
|
3
|
+
import merge from 'deepmerge';
|
4
|
+
import { EditorContext } from '../src';
|
5
|
+
import { defaultEditorContext, EditorContextOptions } from '../src/Editor/EditorContext';
|
6
|
+
import { DeepPartial } from '../src/types';
|
7
|
+
|
8
|
+
export type ContextRenderOptions = RenderOptions & {
|
9
|
+
context?: DeepPartial<{
|
10
|
+
editor: EditorContextOptions;
|
11
|
+
}>;
|
12
|
+
};
|
13
|
+
|
14
|
+
export const renderWithContext = (ui: ReactElement | null, options?: ContextRenderOptions): RenderResult => {
|
15
|
+
const editorContext = merge(defaultEditorContext, options?.context?.editor || {}) as EditorContextOptions;
|
16
|
+
|
17
|
+
return render(<EditorContext.Provider value={editorContext}>{ui}</EditorContext.Provider>);
|
18
|
+
};
|
@@ -1,22 +1,18 @@
|
|
1
1
|
import React, { ReactElement, useContext, useEffect } from 'react';
|
2
|
-
import { render, RenderOptions } from '@testing-library/react';
|
3
2
|
import { Extension, RemirrorContentType, RemirrorManager } from '@remirror/core';
|
4
3
|
import { CorePreset } from '@remirror/preset-core';
|
5
4
|
import { BuiltinPreset } from 'remirror';
|
6
5
|
import { EditorComponent, Remirror, useRemirror } from '@remirror/react';
|
7
6
|
import { RemirrorTestChain } from 'jest-remirror';
|
8
|
-
import merge from 'deepmerge';
|
9
7
|
import { createExtensions } from '../src/Extensions/Extensions';
|
10
|
-
import { EditorContext } from '../src
|
11
|
-
import { FloatingToolbar } from '../src/EditorToolbar
|
12
|
-
import {
|
13
|
-
import { DeepPartial } from '../src/types';
|
8
|
+
import { EditorContext } from '../src';
|
9
|
+
import { FloatingToolbar } from '../src/EditorToolbar';
|
10
|
+
import { renderWithContext, ContextRenderOptions } from './renderWithContext';
|
14
11
|
|
15
|
-
export type EditorRenderOptions =
|
12
|
+
export type EditorRenderOptions = ContextRenderOptions & {
|
16
13
|
content?: RemirrorContentType;
|
17
14
|
editable?: boolean;
|
18
15
|
extensions?: Extension[];
|
19
|
-
context?: DeepPartial<EditorContextOptions>;
|
20
16
|
};
|
21
17
|
|
22
18
|
type TestEditorProps = EditorRenderOptions & {
|
@@ -41,6 +37,9 @@ const TestEditor = ({ children, extensions, content, onReady, editable }: TestEd
|
|
41
37
|
content: content,
|
42
38
|
selection: 'start',
|
43
39
|
stringHandler: 'html',
|
40
|
+
onError: ({ error }) => {
|
41
|
+
throw error;
|
42
|
+
},
|
44
43
|
});
|
45
44
|
|
46
45
|
useEffect(() => {
|
@@ -89,7 +88,6 @@ export const renderWithEditor = async (
|
|
89
88
|
ui: ReactElement | null,
|
90
89
|
options?: EditorRenderOptions,
|
91
90
|
): Promise<EditorRenderResult> => {
|
92
|
-
const context = merge(defaultEditorContext, options?.context || {}) as EditorContextOptions;
|
93
91
|
const result: Partial<EditorRenderResult> = {
|
94
92
|
getHtmlContent: () => document.querySelector('.remirror-editor')?.innerHTML,
|
95
93
|
getJsonContent: () => result.editor?.state.doc.content.child(0).toJSON(),
|
@@ -97,18 +95,17 @@ export const renderWithEditor = async (
|
|
97
95
|
};
|
98
96
|
let isReady = false;
|
99
97
|
|
100
|
-
const { container } =
|
101
|
-
<
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
</EditorContext.Provider>,
|
98
|
+
const { container } = renderWithContext(
|
99
|
+
<TestEditor
|
100
|
+
onReady={(manager) => {
|
101
|
+
result.editor = RemirrorTestChain.create(manager);
|
102
|
+
isReady = true;
|
103
|
+
}}
|
104
|
+
{...options}
|
105
|
+
>
|
106
|
+
{ui}
|
107
|
+
</TestEditor>,
|
108
|
+
options,
|
112
109
|
);
|
113
110
|
|
114
111
|
if (!isReady) {
|
package/vite.config.ts
CHANGED
@@ -2,10 +2,18 @@ import { defineConfig } from 'vite';
|
|
2
2
|
import react from '@vitejs/plugin-react';
|
3
3
|
|
4
4
|
// https://vitejs.dev/config/
|
5
|
+
// Dependencies from within the monorepo need to be configured in a special way, relates to:
|
6
|
+
// https://github.com/vitejs/vite/issues/5668
|
5
7
|
export default defineConfig({
|
6
8
|
root: 'demo',
|
9
|
+
optimizeDeps: {
|
10
|
+
include: ['@squiz/resource-browser'],
|
11
|
+
},
|
7
12
|
build: {
|
8
13
|
outDir: 'build/demo',
|
14
|
+
commonjsOptions: {
|
15
|
+
include: [/node_modules/, /resource-browser/],
|
16
|
+
},
|
9
17
|
},
|
10
18
|
plugins: [
|
11
19
|
react({
|
@@ -1,12 +0,0 @@
|
|
1
|
-
export type SelectOptions = Record<string, SelectOption>;
|
2
|
-
export type SelectOption = {
|
3
|
-
label: string;
|
4
|
-
};
|
5
|
-
export type SelectProps = {
|
6
|
-
name: string;
|
7
|
-
label?: string;
|
8
|
-
value?: string;
|
9
|
-
options: SelectOptions;
|
10
|
-
onChange?: (value: string) => void;
|
11
|
-
};
|
12
|
-
export declare const Select: ({ name, label, value, onChange, options }: SelectProps) => JSX.Element;
|