@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.
Files changed (79) hide show
  1. package/demo/App.tsx +4 -24
  2. package/demo/AppContext.tsx +28 -0
  3. package/demo/index.scss +0 -2
  4. package/demo/main.tsx +2 -0
  5. package/demo/resources.json +28 -0
  6. package/demo/sources.json +23 -0
  7. package/lib/Editor/EditorContext.d.ts +0 -7
  8. package/lib/Editor/EditorContext.js +0 -2
  9. package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +16 -32
  10. package/lib/EditorToolbar/Tools/Image/ImageModal.js +3 -2
  11. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +18 -58
  12. package/lib/EditorToolbar/Tools/Link/LinkModal.js +3 -2
  13. package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +1 -1
  14. package/lib/Extensions/Extensions.js +0 -2
  15. package/lib/Extensions/ImageExtension/AssetImageExtension.d.ts +0 -1
  16. package/lib/Extensions/ImageExtension/AssetImageExtension.js +1 -2
  17. package/lib/Extensions/LinkExtension/AssetLinkExtension.d.ts +0 -1
  18. package/lib/Extensions/LinkExtension/AssetLinkExtension.js +2 -3
  19. package/lib/Extensions/LinkExtension/LinkExtension.js +1 -1
  20. package/lib/index.css +84 -4
  21. package/lib/types.d.ts +3 -3
  22. package/lib/ui/Fields/Checkbox/Checkbox.d.ts +8 -0
  23. package/lib/ui/Fields/Checkbox/Checkbox.js +47 -0
  24. package/lib/ui/Fields/Input/Input.d.ts +2 -4
  25. package/lib/ui/Fields/Input/Input.js +3 -9
  26. package/lib/ui/Fields/InputContainer/InputContainer.d.ts +9 -0
  27. package/lib/ui/Fields/InputContainer/InputContainer.js +16 -0
  28. package/lib/ui/Fields/MatrixAsset/MatrixAsset.d.ts +17 -0
  29. package/lib/ui/Fields/MatrixAsset/MatrixAsset.js +29 -0
  30. package/lib/ui/Modal/Modal.d.ts +1 -0
  31. package/lib/ui/Modal/Modal.js +3 -2
  32. package/lib/ui/Tabs/Tabs.d.ts +10 -0
  33. package/lib/ui/Tabs/Tabs.js +46 -0
  34. package/lib/utils/validation.d.ts +2 -1
  35. package/lib/utils/validation.js +8 -2
  36. package/package.json +4 -3
  37. package/src/Editor/Editor.spec.tsx +1 -1
  38. package/src/Editor/EditorContext.spec.tsx +11 -13
  39. package/src/Editor/EditorContext.ts +0 -11
  40. package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +29 -12
  41. package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +37 -53
  42. package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +76 -49
  43. package/src/EditorToolbar/Tools/Image/ImageModal.spec.tsx +1 -0
  44. package/src/EditorToolbar/Tools/Image/ImageModal.tsx +3 -2
  45. package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +22 -13
  46. package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +35 -57
  47. package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +52 -36
  48. package/src/EditorToolbar/Tools/Link/LinkModal.tsx +3 -2
  49. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +47 -4
  50. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +3 -2
  51. package/src/Extensions/Extensions.ts +0 -2
  52. package/src/Extensions/ImageExtension/AssetImageExtension.ts +1 -3
  53. package/src/Extensions/LinkExtension/AssetLinkExtension.ts +2 -4
  54. package/src/Extensions/LinkExtension/LinkExtension.ts +1 -1
  55. package/src/index.scss +1 -0
  56. package/src/types.ts +7 -5
  57. package/src/ui/Fields/Checkbox/Checkbox.spec.tsx +50 -0
  58. package/src/ui/Fields/Checkbox/Checkbox.tsx +49 -0
  59. package/src/ui/Fields/Checkbox/_checkbox.scss +26 -0
  60. package/src/ui/Fields/Input/Input.tsx +4 -18
  61. package/src/ui/Fields/InputContainer/InputContainer.spec.tsx +18 -0
  62. package/src/ui/Fields/InputContainer/InputContainer.tsx +29 -0
  63. package/src/ui/Fields/MatrixAsset/MatrixAsset.spec.tsx +103 -0
  64. package/src/ui/Fields/MatrixAsset/MatrixAsset.tsx +55 -0
  65. package/src/ui/Modal/FormModal.spec.tsx +2 -1
  66. package/src/ui/Modal/Modal.spec.tsx +15 -7
  67. package/src/ui/Modal/Modal.tsx +4 -2
  68. package/src/ui/Tabs/Tabs.spec.tsx +44 -0
  69. package/src/ui/Tabs/Tabs.tsx +41 -0
  70. package/src/ui/_forms.scss +4 -2
  71. package/src/utils/validation.spec.ts +22 -0
  72. package/src/utils/validation.ts +9 -1
  73. package/tests/index.ts +2 -0
  74. package/tests/mockResourceBrowserContext.tsx +63 -0
  75. package/tests/renderWithContext.tsx +18 -0
  76. package/tests/renderWithEditor.tsx +18 -21
  77. package/vite.config.ts +8 -0
  78. package/lib/ui/Fields/Select/Select.d.ts +0 -12
  79. 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" />
@@ -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-2">
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
+ );
@@ -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: top 0.25rem right 0.25rem;
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
+ });
@@ -1,8 +1,16 @@
1
- export const noEmptySpacesValidation = async (value: string | undefined) => {
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
@@ -1,2 +1,4 @@
1
1
  export * from './renderWithEditor';
2
+ export * from './renderWithContext';
3
+ export * from './mockResourceBrowserContext';
2
4
  export * from './select';
@@ -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/Editor/EditorContext';
11
- import { FloatingToolbar } from '../src/EditorToolbar/FloatingToolbar';
12
- import { defaultEditorContext, EditorContextOptions } from '../src/Editor/EditorContext';
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 = RenderOptions & {
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 } = render(
101
- <EditorContext.Provider value={context}>
102
- <TestEditor
103
- onReady={(manager) => {
104
- result.editor = RemirrorTestChain.create(manager);
105
- isReady = true;
106
- }}
107
- {...options}
108
- >
109
- {ui}
110
- </TestEditor>
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;