@squiz/formatted-text-editor 1.12.0-alpha.8 → 1.12.1-alpha.0

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 (163) hide show
  1. package/.eslintrc.json +34 -0
  2. package/CHANGELOG.md +48 -0
  3. package/README.md +2 -3
  4. package/build.js +21 -0
  5. package/cypress/e2e/bold.spec.cy.ts +18 -0
  6. package/cypress/global.d.ts +9 -0
  7. package/cypress/support/commands.ts +130 -0
  8. package/cypress/support/e2e.ts +20 -0
  9. package/cypress/tsconfig.json +8 -0
  10. package/cypress.config.ts +7 -0
  11. package/demo/App.tsx +39 -0
  12. package/demo/index.html +13 -0
  13. package/demo/index.scss +40 -0
  14. package/demo/main.tsx +10 -0
  15. package/demo/public/favicon-dxp.svg +3 -0
  16. package/demo/vite-env.d.ts +1 -0
  17. package/file-transformer.js +1 -0
  18. package/jest.bootstrap.ts +3 -0
  19. package/jest.config.ts +30 -0
  20. package/lib/Editor/Editor.d.ts +4 -2
  21. package/lib/Editor/Editor.js +11 -14
  22. package/lib/EditorToolbar/FloatingToolbar.d.ts +1 -0
  23. package/lib/EditorToolbar/FloatingToolbar.js +31 -0
  24. package/lib/EditorToolbar/Toolbar.d.ts +1 -0
  25. package/lib/EditorToolbar/Toolbar.js +25 -0
  26. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.d.ts +10 -0
  27. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +23 -0
  28. package/lib/EditorToolbar/Tools/Link/LinkButton.d.ts +5 -0
  29. package/lib/EditorToolbar/Tools/Link/LinkButton.js +34 -0
  30. package/lib/EditorToolbar/Tools/Link/LinkModal.d.ts +8 -0
  31. package/lib/EditorToolbar/Tools/Link/LinkModal.js +14 -0
  32. package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.d.ts +2 -0
  33. package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +16 -0
  34. package/lib/EditorToolbar/Tools/Redo/RedoButton.d.ts +2 -0
  35. package/lib/EditorToolbar/Tools/Redo/RedoButton.js +16 -0
  36. package/lib/EditorToolbar/Tools/TextAlign/TextAlignButtons.js +4 -1
  37. package/lib/EditorToolbar/Tools/TextType/Heading/HeadingButton.d.ts +5 -0
  38. package/lib/EditorToolbar/Tools/TextType/Heading/HeadingButton.js +32 -0
  39. package/lib/EditorToolbar/Tools/TextType/Paragraph/ParagraphButton.d.ts +2 -0
  40. package/lib/EditorToolbar/Tools/TextType/Paragraph/ParagraphButton.js +16 -0
  41. package/lib/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.d.ts +2 -0
  42. package/lib/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.js +16 -0
  43. package/lib/EditorToolbar/Tools/TextType/TextTypeDropdown.d.ts +2 -0
  44. package/lib/EditorToolbar/Tools/TextType/TextTypeDropdown.js +35 -0
  45. package/lib/EditorToolbar/Tools/Undo/UndoButton.d.ts +2 -0
  46. package/lib/EditorToolbar/Tools/Undo/UndoButton.js +16 -0
  47. package/lib/EditorToolbar/index.d.ts +2 -0
  48. package/lib/EditorToolbar/index.js +2 -0
  49. package/lib/Extensions/Extensions.d.ts +4 -0
  50. package/lib/Extensions/Extensions.js +20 -0
  51. package/lib/Extensions/LinkExtension/LinkExtension.d.ts +16 -0
  52. package/lib/Extensions/LinkExtension/LinkExtension.js +91 -0
  53. package/lib/Extensions/PreformattedExtension/PreformattedExtension.d.ts +10 -0
  54. package/lib/Extensions/PreformattedExtension/PreformattedExtension.js +46 -0
  55. package/lib/FormattedTextEditor.d.ts +2 -2
  56. package/lib/FormattedTextEditor.js +1 -6
  57. package/lib/hooks/index.d.ts +1 -0
  58. package/lib/hooks/index.js +1 -0
  59. package/lib/hooks/useExtensionNames.d.ts +1 -0
  60. package/lib/hooks/useExtensionNames.js +12 -0
  61. package/lib/index.css +787 -3686
  62. package/lib/ui/Inputs/Select/Select.d.ts +12 -0
  63. package/lib/ui/Inputs/Select/Select.js +23 -0
  64. package/lib/ui/Inputs/Text/TextInput.d.ts +4 -0
  65. package/lib/ui/Inputs/Text/TextInput.js +7 -0
  66. package/lib/ui/Modal/FormModal.d.ts +5 -0
  67. package/lib/ui/Modal/FormModal.js +11 -0
  68. package/lib/ui/Modal/Modal.d.ts +10 -0
  69. package/lib/ui/Modal/Modal.js +48 -0
  70. package/lib/ui/ToolbarButton/ToolbarButton.d.ts +1 -1
  71. package/lib/ui/ToolbarButton/ToolbarButton.js +1 -1
  72. package/lib/ui/ToolbarDropdown/ToolbarDropdown.d.ts +6 -0
  73. package/lib/ui/ToolbarDropdown/ToolbarDropdown.js +20 -0
  74. package/lib/ui/ToolbarDropdownButton/ToolbarDropdownButton.d.ts +9 -0
  75. package/lib/ui/ToolbarDropdownButton/ToolbarDropdownButton.js +8 -0
  76. package/lib/utils/createToolbarPositioner.d.ts +18 -0
  77. package/lib/utils/createToolbarPositioner.js +81 -0
  78. package/lib/utils/getCursorRect.d.ts +2 -0
  79. package/lib/utils/getCursorRect.js +3 -0
  80. package/package.json +22 -13
  81. package/postcss.config.js +12 -0
  82. package/src/Editor/Editor.mock.tsx +43 -0
  83. package/src/Editor/Editor.spec.tsx +254 -0
  84. package/src/Editor/Editor.tsx +46 -0
  85. package/src/Editor/_editor.scss +82 -0
  86. package/src/EditorToolbar/FloatingToolbar.spec.tsx +30 -0
  87. package/src/EditorToolbar/FloatingToolbar.tsx +40 -0
  88. package/src/EditorToolbar/Toolbar.tsx +33 -0
  89. package/src/EditorToolbar/Tools/Bold/BoldButton.spec.tsx +19 -0
  90. package/src/EditorToolbar/Tools/Bold/BoldButton.tsx +30 -0
  91. package/src/EditorToolbar/Tools/Italic/ItalicButton.spec.tsx +19 -0
  92. package/src/EditorToolbar/Tools/Italic/ItalicButton.tsx +30 -0
  93. package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +30 -0
  94. package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +48 -0
  95. package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +277 -0
  96. package/src/EditorToolbar/Tools/Link/LinkButton.tsx +56 -0
  97. package/src/EditorToolbar/Tools/Link/LinkModal.tsx +29 -0
  98. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +46 -0
  99. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +27 -0
  100. package/src/EditorToolbar/Tools/Redo/RedoButton.spec.tsx +59 -0
  101. package/src/EditorToolbar/Tools/Redo/RedoButton.tsx +30 -0
  102. package/src/EditorToolbar/Tools/TextAlign/CenterAlign/CenterAlignButton.spec.tsx +39 -0
  103. package/src/EditorToolbar/Tools/TextAlign/CenterAlign/CenterAlignButton.tsx +31 -0
  104. package/src/EditorToolbar/Tools/TextAlign/JustifyAlign/JustifyAlignButton.spec.tsx +39 -0
  105. package/src/EditorToolbar/Tools/TextAlign/JustifyAlign/JustifyAlignButton.tsx +31 -0
  106. package/src/EditorToolbar/Tools/TextAlign/LeftAlign/LeftAlignButton.spec.tsx +39 -0
  107. package/src/EditorToolbar/Tools/TextAlign/LeftAlign/LeftAlignButton.tsx +31 -0
  108. package/src/EditorToolbar/Tools/TextAlign/RightAlign/RightAlignButton.spec.tsx +39 -0
  109. package/src/EditorToolbar/Tools/TextAlign/RightAlign/RightAlignButton.tsx +31 -0
  110. package/src/EditorToolbar/Tools/TextAlign/TextAlignButtons.tsx +21 -0
  111. package/src/EditorToolbar/Tools/TextType/Heading/HeadingButton.spec.tsx +56 -0
  112. package/src/EditorToolbar/Tools/TextType/Heading/HeadingButton.tsx +52 -0
  113. package/src/EditorToolbar/Tools/TextType/Paragraph/ParagraphButton.spec.tsx +30 -0
  114. package/src/EditorToolbar/Tools/TextType/Paragraph/ParagraphButton.tsx +25 -0
  115. package/src/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.spec.tsx +47 -0
  116. package/src/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.tsx +30 -0
  117. package/src/EditorToolbar/Tools/TextType/TextTypeDropdown.spec.tsx +51 -0
  118. package/src/EditorToolbar/Tools/TextType/TextTypeDropdown.tsx +44 -0
  119. package/src/EditorToolbar/Tools/Underline/Underline.spec.tsx +19 -0
  120. package/src/EditorToolbar/Tools/Underline/UnderlineButton.tsx +30 -0
  121. package/src/EditorToolbar/Tools/Undo/UndoButton.spec.tsx +49 -0
  122. package/src/EditorToolbar/Tools/Undo/UndoButton.tsx +30 -0
  123. package/src/EditorToolbar/_floating-toolbar.scss +4 -0
  124. package/src/EditorToolbar/_toolbar.scss +16 -0
  125. package/src/EditorToolbar/index.ts +2 -0
  126. package/src/Extensions/Extensions.ts +29 -0
  127. package/src/Extensions/LinkExtension/LinkExtension.ts +116 -0
  128. package/src/Extensions/PreformattedExtension/PreformattedExtension.ts +50 -0
  129. package/src/FormattedTextEditor.spec.tsx +10 -0
  130. package/src/FormattedTextEditor.tsx +3 -0
  131. package/src/hooks/index.ts +1 -0
  132. package/src/hooks/useExtensionNames.ts +15 -0
  133. package/src/index.scss +19 -0
  134. package/src/index.ts +3 -0
  135. package/src/ui/Inputs/Select/Select.spec.tsx +30 -0
  136. package/src/ui/Inputs/Select/Select.tsx +66 -0
  137. package/src/ui/Inputs/Text/TextInput.spec.tsx +43 -0
  138. package/src/ui/Inputs/Text/TextInput.tsx +20 -0
  139. package/src/ui/Modal/FormModal.spec.tsx +20 -0
  140. package/src/ui/Modal/FormModal.tsx +17 -0
  141. package/src/ui/Modal/Modal.spec.tsx +113 -0
  142. package/src/ui/Modal/Modal.tsx +97 -0
  143. package/src/ui/Modal/_modal.scss +24 -0
  144. package/src/ui/ToolbarButton/ToolbarButton.tsx +26 -0
  145. package/src/ui/ToolbarButton/_toolbar-button.scss +17 -0
  146. package/src/ui/ToolbarDropdown/ToolbarDropdown.spec.tsx +78 -0
  147. package/src/ui/ToolbarDropdown/ToolbarDropdown.tsx +42 -0
  148. package/src/ui/ToolbarDropdown/_toolbar-dropdown.scss +32 -0
  149. package/src/ui/ToolbarDropdownButton/ToolbarDropdownButton.spec.tsx +48 -0
  150. package/src/ui/ToolbarDropdownButton/ToolbarDropdownButton.tsx +29 -0
  151. package/src/ui/ToolbarDropdownButton/_toolbar-dropdown-button.scss +14 -0
  152. package/src/ui/_buttons.scss +19 -0
  153. package/src/ui/_forms.scss +16 -0
  154. package/src/utils/createToolbarPositioner.ts +115 -0
  155. package/src/utils/getCursorRect.ts +5 -0
  156. package/tailwind.config.cjs +83 -0
  157. package/tests/index.ts +2 -0
  158. package/tests/renderWithEditor.tsx +110 -0
  159. package/tests/select.tsx +16 -0
  160. package/tsconfig.json +22 -0
  161. package/vite.config.ts +19 -0
  162. package/lib/EditorToolbar/EditorToolbar.d.ts +0 -7
  163. package/lib/EditorToolbar/EditorToolbar.js +0 -22
@@ -0,0 +1,24 @@
1
+ .squiz-fte-modal {
2
+ @apply border-0 rounded shadow-lg flex flex-col w-full bg-white font-base;
3
+ border-radius: 8px;
4
+
5
+ &-wrapper {
6
+ @apply justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 outline-none focus:outline-none;
7
+ }
8
+
9
+ &-header {
10
+ @apply flex items-start justify-between;
11
+ }
12
+
13
+ &-content {
14
+ @apply relative px-6 flex-auto;
15
+ }
16
+
17
+ &-footer {
18
+ @apply flex items-center justify-end rounded-b;
19
+
20
+ &__button {
21
+ @apply squiz-fte-btn text-md font-bold;
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,26 @@
1
+ import React, { ReactElement } from 'react';
2
+
3
+ type ToolbarButtonProps = {
4
+ handleOnClick: () => void;
5
+ isDisabled?: boolean;
6
+ isActive: boolean;
7
+ icon: ReactElement;
8
+ label: string;
9
+ };
10
+
11
+ const ToolbarButton = ({ handleOnClick, isDisabled, isActive, icon, label }: ToolbarButtonProps) => {
12
+ return (
13
+ <button
14
+ aria-label={label}
15
+ title={label}
16
+ type="button"
17
+ onClick={handleOnClick}
18
+ disabled={isDisabled}
19
+ className={`squiz-fte-btn toolbar-button ${isActive ? 'is-active' : ''}`}
20
+ >
21
+ {icon}
22
+ </button>
23
+ );
24
+ };
25
+
26
+ export default ToolbarButton;
@@ -0,0 +1,17 @@
1
+ .toolbar-button {
2
+ @apply bg-white text-gray-600 p-1;
3
+
4
+ ~ .toolbar-button {
5
+ margin-left: 2px;
6
+ }
7
+
8
+ &:hover,
9
+ &:focus {
10
+ background-color: rgba(black, 0.04);
11
+ }
12
+
13
+ &.is-active,
14
+ &:active {
15
+ @apply text-blue-300 bg-blue-100;
16
+ }
17
+ }
@@ -0,0 +1,78 @@
1
+ import '@testing-library/jest-dom';
2
+ import { fireEvent, render, screen } from '@testing-library/react';
3
+ import React from 'react';
4
+ import ToolbarDropdown from './ToolbarDropdown';
5
+
6
+ describe('Toolbar dropdown', () => {
7
+ const ToolbarDropdownComponent = () => {
8
+ return (
9
+ <ToolbarDropdown label="Test dropdown">
10
+ <button>Test button</button>
11
+ <button>Test button 2</button>
12
+ </ToolbarDropdown>
13
+ );
14
+ };
15
+
16
+ it('Renders the dropdown button', () => {
17
+ render(<ToolbarDropdownComponent />);
18
+ // Check that the supplied label renders
19
+ const dropdownButton = screen.getByRole('button', { name: 'Test dropdown' });
20
+ expect(dropdownButton).toBeInTheDocument();
21
+ });
22
+
23
+ it('Renders the child elements for the dropdown', () => {
24
+ render(<ToolbarDropdownComponent />);
25
+ // Check that default value supplied renders
26
+ expect(screen.getByRole('button', { name: 'Test button' })).toBeInTheDocument();
27
+ expect(screen.getByRole('button', { name: 'Test button 2' })).toBeInTheDocument();
28
+ });
29
+
30
+ it('Renders the dropdown menu after clicking the dropdown button', () => {
31
+ const { baseElement } = render(<ToolbarDropdownComponent />);
32
+ expect(baseElement).toBeTruthy();
33
+
34
+ const dropdownButton = baseElement.querySelector('button#dropdownHoverButton') as HTMLButtonElement;
35
+ expect(dropdownButton).toBeTruthy();
36
+ fireEvent.click(dropdownButton);
37
+
38
+ const dropdownMenu = baseElement.querySelector('div.toolbar-dropdown__menu') as HTMLDivElement;
39
+ expect(dropdownMenu).toBeTruthy();
40
+ expect(dropdownMenu.classList).toContain('block');
41
+ });
42
+
43
+ it('Handles the onBlur call when focusing out of the menu', () => {
44
+ const { baseElement } = render(<ToolbarDropdownComponent />);
45
+ expect(baseElement).toBeTruthy();
46
+
47
+ const dropdownButton = baseElement.querySelector('button#dropdownHoverButton') as HTMLButtonElement;
48
+ expect(dropdownButton).toBeTruthy();
49
+ fireEvent.click(dropdownButton);
50
+
51
+ const dropdownMenu = baseElement.querySelector('div.toolbar-dropdown__menu') as HTMLDivElement;
52
+ expect(dropdownMenu).toBeTruthy();
53
+ expect(dropdownMenu.classList).toContain('block');
54
+
55
+ fireEvent.blur(dropdownMenu);
56
+
57
+ expect(dropdownMenu.classList).toContain('hidden');
58
+ });
59
+
60
+ it('Closes the dropdown menu after clicking a child element', () => {
61
+ const { baseElement } = render(<ToolbarDropdownComponent />);
62
+ expect(baseElement).toBeTruthy();
63
+
64
+ const dropdownButton = baseElement.querySelector('button#dropdownHoverButton') as HTMLButtonElement;
65
+ expect(dropdownButton).toBeTruthy();
66
+ fireEvent.click(dropdownButton);
67
+
68
+ const dropdownMenu = baseElement.querySelector('div.toolbar-dropdown__menu') as HTMLDivElement;
69
+ expect(dropdownMenu).toBeTruthy();
70
+ expect(dropdownMenu.classList).toContain('block');
71
+
72
+ const childElement = screen.getByRole('button', { name: 'Test button' });
73
+ expect(childElement).toBeTruthy();
74
+ fireEvent.click(childElement);
75
+
76
+ expect(dropdownMenu.classList).toContain('hidden');
77
+ });
78
+ });
@@ -0,0 +1,42 @@
1
+ import React, { FocusEventHandler, useState } from 'react';
2
+ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
3
+
4
+ type ToolbarDropdownProps = {
5
+ children: JSX.Element | JSX.Element[];
6
+ label: string;
7
+ };
8
+
9
+ const ToolbarDropdown = ({ children, label }: ToolbarDropdownProps) => {
10
+ const [isOpen, setOpen] = useState<boolean>(false);
11
+
12
+ const handleDropDown = () => {
13
+ setOpen(!isOpen);
14
+ };
15
+
16
+ const handleBlur: FocusEventHandler<HTMLDivElement> = (event) => {
17
+ if (event.relatedTarget?.id !== 'dropdownMenuButton' && !event.target?.className.includes('is-active')) {
18
+ isOpen && handleDropDown();
19
+ }
20
+ };
21
+
22
+ return (
23
+ <div className="toolbar-dropdown" onBlur={handleBlur}>
24
+ <button id="dropdownHoverButton" className="toolbar-dropdown__button" onClick={handleDropDown}>
25
+ <span className="toolbar-dropdown__label">{label}</span>
26
+ <ExpandMoreIcon className="toolbar-dropdown__icon" aria-hidden="true" />
27
+ </button>
28
+
29
+ <div
30
+ id="dropdown"
31
+ className={`toolbar-dropdown__menu z-10 ${isOpen ? 'block' : 'hidden'} bg-white divide-y w-169`}
32
+ >
33
+ {/* eslint-disable-next-line */}
34
+ <ul aria-labelledby="dropdownHoverButton" onClick={handleDropDown}>
35
+ {children}
36
+ </ul>
37
+ </div>
38
+ </div>
39
+ );
40
+ };
41
+
42
+ export default ToolbarDropdown;
@@ -0,0 +1,32 @@
1
+ .toolbar-dropdown {
2
+ &__button {
3
+ @apply flex items-center font-base text-md font-semibold text-gray-600;
4
+ align-self: center;
5
+
6
+ height: 2rem;
7
+ padding-left: 0.5rem;
8
+
9
+ &:active,
10
+ &:hover,
11
+ &:focus {
12
+ background-color: rgba(black, 0.04);
13
+ }
14
+
15
+ .toolbar-dropdown__label {
16
+ display: inline-flex;
17
+ width: 7rem;
18
+ }
19
+
20
+ .toolbar-dropdown__icon {
21
+ width: 1rem;
22
+ height: 1.5rem;
23
+ }
24
+ }
25
+
26
+ &__menu {
27
+ @apply rounded shadow-sm border border-gray-300;
28
+
29
+ position: absolute;
30
+ margin-top: 0.75rem;
31
+ }
32
+ }
@@ -0,0 +1,48 @@
1
+ import '@testing-library/jest-dom';
2
+ import { render } from '@testing-library/react';
3
+ import React from 'react';
4
+ import ToolbarDropdownButton from './ToolbarDropdownButton';
5
+
6
+ describe('Toolbar dropdown', () => {
7
+ const handleSelect = jest.fn();
8
+
9
+ const ToolbarDropdownButtonComponent = () => {
10
+ return (
11
+ <ToolbarDropdownButton handleOnClick={handleSelect} isDisabled={false} isActive={true} label={'Test label'}>
12
+ <h1>Test heading 1</h1>
13
+ </ToolbarDropdownButton>
14
+ );
15
+ };
16
+
17
+ const EmptyToolbarDropdownButtonComponent = () => {
18
+ return (
19
+ <ToolbarDropdownButton handleOnClick={handleSelect} isDisabled={false} isActive={false} label={'Test label'} />
20
+ );
21
+ };
22
+
23
+ it('Renders the dropdown menu button', () => {
24
+ const { baseElement } = render(<ToolbarDropdownButtonComponent />);
25
+ expect(baseElement).toBeTruthy();
26
+
27
+ const buttonElement = baseElement.querySelector('button#dropdownMenuButton') as HTMLButtonElement;
28
+ expect(buttonElement).toBeTruthy();
29
+ expect(buttonElement.textContent).toBe('Test heading 1');
30
+ });
31
+
32
+ it('Renders the dropdown menu button active icon', () => {
33
+ const { baseElement } = render(<ToolbarDropdownButtonComponent />);
34
+ expect(baseElement).toBeTruthy();
35
+
36
+ const iconElement = baseElement.querySelector('svg.dropdown-button-icon') as SVGElement;
37
+ expect(iconElement).toBeTruthy();
38
+ });
39
+
40
+ it('Should render the label as button text if no children are provided', () => {
41
+ const { baseElement } = render(<EmptyToolbarDropdownButtonComponent />);
42
+ expect(baseElement).toBeTruthy();
43
+
44
+ const buttonElement = baseElement.querySelector('button#dropdownMenuButton') as HTMLButtonElement;
45
+ expect(buttonElement).toBeTruthy();
46
+ expect(buttonElement.textContent).toBe('Test label');
47
+ });
48
+ });
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import CheckIcon from '@mui/icons-material/Check';
3
+
4
+ type DropdownButtonProps = {
5
+ children?: JSX.Element;
6
+ handleOnClick: () => void;
7
+ isDisabled: boolean;
8
+ isActive: boolean;
9
+ label: string;
10
+ };
11
+
12
+ const DropdownButton = ({ children, handleOnClick, isDisabled, isActive, label }: DropdownButtonProps) => {
13
+ return (
14
+ <button
15
+ aria-label={label}
16
+ id="dropdownMenuButton"
17
+ title={label}
18
+ type="button"
19
+ onClick={handleOnClick}
20
+ disabled={isDisabled}
21
+ className={`btn dropdown-button ${isActive ? 'is-active' : ''}`}
22
+ >
23
+ <span>{children || label}</span>
24
+ {isActive && <CheckIcon className="dropdown-button-icon" />}
25
+ </button>
26
+ );
27
+ };
28
+
29
+ export default DropdownButton;
@@ -0,0 +1,14 @@
1
+ .dropdown-button {
2
+ @apply px-2 py-1 text-gray-600;
3
+
4
+ height: 40px;
5
+ width: 100%;
6
+ justify-content: space-between;
7
+ align-items: center;
8
+ display: flex;
9
+
10
+ &:hover,
11
+ &:focus {
12
+ background-color: rgba(black, 0.04);
13
+ }
14
+ }
@@ -0,0 +1,19 @@
1
+ .squiz-fte-btn {
2
+ @apply font-normal rounded ease-linear transition-all duration-150;
3
+ display: flex;
4
+ align-items: center;
5
+ text-align: center;
6
+ white-space: nowrap;
7
+ vertical-align: middle;
8
+ touch-action: manipulation;
9
+ cursor: pointer;
10
+ background-image: none;
11
+ border: 1px solid transparent;
12
+ padding: 6px 12px;
13
+
14
+ &.disabled,
15
+ &[disabled] {
16
+ cursor: not-allowed;
17
+ @apply opacity-50;
18
+ }
19
+ }
@@ -0,0 +1,16 @@
1
+ .squiz-fte {
2
+ &-form-group {
3
+ @apply flex flex-col;
4
+ }
5
+ &-form-label {
6
+ @apply mb-1 text-md font-semibold text-gray-600;
7
+ }
8
+ &-form-control {
9
+ padding: 6px 12px;
10
+ @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
+ &:focus,
12
+ &:active {
13
+ box-shadow: none;
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,115 @@
1
+ import { hasStateChanged, isPositionVisible, Positioner } from 'remirror/extensions';
2
+ import { Coords, getMarkRange, getTextSelection } from 'remirror';
3
+ import { getCursorRect } from './getCursorRect';
4
+
5
+ export type ToolbarPositionerProps = {
6
+ types: string[];
7
+ };
8
+
9
+ export type ToolbarPositionerRange = {
10
+ isSelectionInView: boolean;
11
+ visible: boolean;
12
+ marks: Record<
13
+ string,
14
+ {
15
+ isExclusivelyActive: boolean;
16
+ isActive: boolean;
17
+ }
18
+ >;
19
+ cursor: {
20
+ from: Coords;
21
+ to: Coords;
22
+ };
23
+ };
24
+
25
+ /* istanbul ignore next */
26
+ export const createToolbarPositioner = ({ types }: ToolbarPositionerProps) => {
27
+ // Inspired by "createMarkPositioner".
28
+ // See: https://github.com/remirror/remirror/blob/107cba/packages/remirror__extension-positioner/src/core-positioners.ts#L267
29
+ return Positioner.create({
30
+ hasChanged: hasStateChanged,
31
+ getActive: (props) => {
32
+ const { state, view } = props;
33
+
34
+ try {
35
+ const selection = getTextSelection(state.selection, state.doc);
36
+ const cursor = { from: view.coordsAtPos(selection.from), to: view.coordsAtPos(selection.to) };
37
+ const data: ToolbarPositionerRange = {
38
+ isSelectionInView:
39
+ isPositionVisible(getCursorRect(cursor.from), view.dom) ||
40
+ isPositionVisible(getCursorRect(cursor.to), view.dom),
41
+ cursor: cursor,
42
+ visible: false,
43
+ marks: {},
44
+ };
45
+
46
+ data.visible = !selection.empty && data.isSelectionInView;
47
+
48
+ types.forEach((type) => {
49
+ const markRange = getMarkRange(selection.$from, type, selection.$to);
50
+
51
+ if (!markRange) {
52
+ data.marks[type] = { isActive: false, isExclusivelyActive: false };
53
+ return;
54
+ }
55
+
56
+ // exclusively active =
57
+ // the entire selection has the mark applied.
58
+ // active =
59
+ // at least part of the selection has the mark applied. "getMarkRanges" will return an empty array
60
+ // if this isn't the case. if there is no selection the cursor must be within the bounds of the mark,
61
+ // not on the edges.
62
+ const isExclusivelyActive = selection.empty
63
+ ? selection.from > markRange.from && selection.to < markRange.to
64
+ : selection.from >= markRange.from && selection.to <= markRange.to;
65
+ const isActive = selection.empty ? selection.from > markRange.from && selection.to < markRange.to : true;
66
+
67
+ // the toolbar will be visible if there is either a selection, or we are within the bounds of a mark
68
+ // we have formatting tools for.
69
+ data.visible = data.visible || isExclusivelyActive;
70
+ data.marks[type] = { isExclusivelyActive, isActive };
71
+ });
72
+
73
+ return data.visible ? [data] : Positioner.EMPTY;
74
+ } catch {
75
+ return Positioner.EMPTY;
76
+ }
77
+ },
78
+ getPosition: (props) => {
79
+ const { element, data, view } = props;
80
+ const { cursor, visible } = data;
81
+ const { from, to } = cursor;
82
+ const parent = element.offsetParent ?? view.dom;
83
+ const parentRect = parent.getBoundingClientRect();
84
+ const height = Math.abs(to.bottom - from.top);
85
+
86
+ // Hack to get JSDOM to work, positioning doesn't work great here.
87
+ if (isNaN(parentRect.top)) {
88
+ return {
89
+ rect: new DOMRect(0, 0, 0, 0),
90
+ y: 0,
91
+ x: 0,
92
+ height: 0,
93
+ width: 0,
94
+ visible: false,
95
+ };
96
+ }
97
+
98
+ // True when the selection spans multiple lines.
99
+ const spansMultipleLines = height > from.bottom - from.top;
100
+
101
+ // The position furthest to the left.
102
+ const leftmost = Math.min(from.left, to.left);
103
+
104
+ // The position nearest the top.
105
+ const topmost = Math.min(from.top, to.top);
106
+
107
+ const left = parent.scrollLeft + (spansMultipleLines ? to.left - parentRect.left : leftmost - parentRect.left);
108
+ const top = parent.scrollTop + topmost - parentRect.top;
109
+ const width = spansMultipleLines ? 1 : Math.abs(from.left - to.right);
110
+ const rect = new DOMRect(spansMultipleLines ? to.left : leftmost, topmost, width, height);
111
+
112
+ return { rect, y: top, x: left, height, width, visible };
113
+ },
114
+ });
115
+ };
@@ -0,0 +1,5 @@
1
+ import { Coords } from 'remirror';
2
+
3
+ export const getCursorRect = (coords: Coords): DOMRect => {
4
+ return new DOMRect(coords.left, coords.top, 1, coords.top - coords.bottom);
5
+ };
@@ -0,0 +1,83 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}', './node_modules/flowbite/**/*.js'],
4
+ theme: {
5
+ extend: {
6
+ borderRadius: {
7
+ DEFAULT: '4px',
8
+ md: '6px',
9
+ },
10
+ fontFamily: {
11
+ base: 'Open Sans, Arial, sans-serif',
12
+ },
13
+ fontWeight: {
14
+ normal: '400',
15
+ medium: '500',
16
+ semibold: '600',
17
+ bold: '700',
18
+ },
19
+ fontSize: {
20
+ xlg: '1.125rem',
21
+ lg: '1rem',
22
+ md: '0.875rem',
23
+ sm: '0.8125rem',
24
+ base: '1rem',
25
+ 'heading-1': ['1.625rem', '2rem'],
26
+ 'heading-2': ['1.25rem', '1.5rem'],
27
+ 'heading-3': ['1.125rem', '1.375rem'],
28
+ 'heading-4': ['1rem', '1.25rem'],
29
+ },
30
+ fontFamily: {
31
+ base: 'Open Sans, Arial, sans-serif',
32
+ },
33
+ boxShadow: {
34
+ outline: '0 0 0 1px rgba(0,0,0,0.10)',
35
+ sm: '0 0 0 1px rgba(0,0,0,0.04), 0 1px 4px 2px rgba(0,0,0,0.08)',
36
+ DEFAULT: '0 0 0 1px rgba(0,0,0,0.04), 0 1px 12px 4px rgba(0,0,0,0.12)',
37
+ md: '0 0 0 1px rgba(0,0,0,0.04), 0 1px 12px 4px rgba(0,0,0,0.12)',
38
+ lg: '0 0 0 1px rgba(0,0,0,0.04), 0 1px 24px 12px rgba(0,0,0,0.12)',
39
+ inner: 'inset 0 2px 4px 0 rgb(0 0 0 / 0.05)',
40
+ none: 'none',
41
+ },
42
+ width: {
43
+ 'modal-sm': '25rem',
44
+ 'modal-md': '37.5rem',
45
+ 'modal-lg': '50rem',
46
+ 'modal-xl': '62.5rem',
47
+ },
48
+ spacing: {
49
+ 1: '0.25rem', // 4px
50
+ 2: '0.5rem', // 8px
51
+ 3: '0.75rem', // 12px
52
+ 4: '1rem', // 16px
53
+ 5: '1.25rem', // 20px
54
+ 6: '1.5rem', // 24px
55
+ 7: '1.75rem', // 28px
56
+ 8: '2rem', // 32px
57
+ 169: '169px', // 169px
58
+ },
59
+ colors: {
60
+ gray: {
61
+ 50: '#F7F7F7',
62
+ 100: '#F5F5F5',
63
+ 200: '#ededed',
64
+ 300: '#e0e0e0',
65
+ 400: '#BABABA',
66
+ 500: '#949494',
67
+ 600: '#707070',
68
+ 700: '#4F4F4F',
69
+ 800: '#3D3D3D',
70
+ 900: '#2B2B2B',
71
+ },
72
+ blue: {
73
+ 100: '#e6f1fa',
74
+ 200: '#8FC0EB',
75
+ 300: '#0774d2',
76
+ 400: '#044985',
77
+ },
78
+ },
79
+ },
80
+ },
81
+ plugins: [],
82
+ important: true,
83
+ };
package/tests/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './renderWithEditor';
2
+ export * from './select';
@@ -0,0 +1,110 @@
1
+ import React, { ReactElement, useEffect } from 'react';
2
+ import { render, RenderOptions } from '@testing-library/react';
3
+ import { Extension, RemirrorContentType, RemirrorManager } from '@remirror/core';
4
+ import { CorePreset } from '@remirror/preset-core';
5
+ import { BuiltinPreset } from 'remirror';
6
+ import { EditorComponent, Remirror, useRemirror } from '@remirror/react';
7
+ import { Extensions } from '../src/Extensions/Extensions';
8
+ import { RemirrorTestChain } from 'jest-remirror';
9
+
10
+ export type EditorRenderOptions = RenderOptions & {
11
+ content?: RemirrorContentType;
12
+ extensions?: Extension[];
13
+ };
14
+
15
+ type TestEditorProps = EditorRenderOptions & {
16
+ children: ReactElement;
17
+ onReady: (manager: RemirrorManager<Extension>) => void;
18
+ };
19
+
20
+ type EditorRenderResult = {
21
+ editor: RemirrorTestChain<Extension | CorePreset | BuiltinPreset>;
22
+ getHtmlContent: () => string | undefined;
23
+ getJsonContent: () => any;
24
+ getSelectedText: () => string | undefined;
25
+ elements: {
26
+ editor: HTMLDivElement;
27
+ };
28
+ };
29
+
30
+ const TestEditor = ({ children, extensions, content, onReady }: TestEditorProps) => {
31
+ const { manager, state, setState } = useRemirror({
32
+ extensions: () => extensions || Extensions(),
33
+ content: content,
34
+ selection: 'start',
35
+ stringHandler: 'html',
36
+ });
37
+
38
+ useEffect(() => {
39
+ onReady(manager);
40
+ }, []);
41
+
42
+ return (
43
+ <Remirror
44
+ manager={manager}
45
+ state={state}
46
+ onChange={(params) => {
47
+ setState(params.state);
48
+ }}
49
+ >
50
+ {children}
51
+ <EditorComponent />
52
+ </Remirror>
53
+ );
54
+ };
55
+
56
+ /**
57
+ * Renders a React component alongside a very minimalistic editor within a Remirror context.
58
+ *
59
+ * We are using the "jest-remirror" library to expose some helper functions via the "editor" property we return.
60
+ *
61
+ * When interacting with the editor helper functions you should wrap calls in an "act" and await on it to avoid
62
+ * console errors during tests.
63
+ *
64
+ * We are NOT using the "renderEditor" method exposed by that library as it is not suitable for our use case.
65
+ * Its purpose is for rendering a plain Remirror editor outside of the context of React. This allows for testing
66
+ * extension logic but not testing any interaction between the extension logic and UI components.
67
+ *
68
+ * Interacting with the editor should typically trigger a re-render of components that use the Remirror hooks.
69
+ * Note that the "useRemirrorContext" hook has an "autoUpdate" option which controls if the hook re-renders on
70
+ * all changes. This defaults to false, some hooks which use this internally (eg. "useCommands") and
71
+ * leave this as the default.
72
+ *
73
+ * @param {ReactElement} ui The React component to render alongside the editor.
74
+ * @param {EditorRenderOptions} options Options to configure initial content, loaded extensions, etc.
75
+ *
76
+ * @return {Promise<EditorRenderResult>}
77
+ */
78
+ export const renderWithEditor = async (
79
+ ui: ReactElement,
80
+ options?: EditorRenderOptions,
81
+ ): Promise<EditorRenderResult> => {
82
+ const result: Partial<EditorRenderResult> = {
83
+ getHtmlContent: () => document.querySelector('.remirror-editor')?.innerHTML,
84
+ getJsonContent: () => result.editor?.state.doc.content.child(0).toJSON(),
85
+ getSelectedText: () => result.editor?.state.doc.textBetween(result.editor?.from, result.editor?.to),
86
+ };
87
+ let isReady = false;
88
+
89
+ const { container } = render(
90
+ <TestEditor
91
+ onReady={(manager) => {
92
+ result.editor = RemirrorTestChain.create(manager);
93
+ isReady = true;
94
+ }}
95
+ {...options}
96
+ >
97
+ {ui}
98
+ </TestEditor>,
99
+ );
100
+
101
+ if (!isReady) {
102
+ throw new Error('The editor component did not mount.');
103
+ }
104
+
105
+ result.elements = {
106
+ editor: container.querySelector('.remirror-editor') as HTMLDivElement,
107
+ };
108
+
109
+ return result as EditorRenderResult;
110
+ };