@squiz/formatted-text-editor 2.0.1 → 2.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/formatted-text-editor",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "private": false,
@@ -25,8 +25,10 @@
25
25
  "@remirror/extension-react-tables": "^2.2.19",
26
26
  "@remirror/react": "2.0.35",
27
27
  "@squiz/dx-json-schema-lib": "^1.65.1",
28
+ "@squiz/dxp-ai-client-react": "^0.1.3-alpha",
28
29
  "@squiz/matrix-resource-browser-plugin": "^2.0.0",
29
30
  "@squiz/resource-browser": "^2.0.0",
31
+ "@squiz/sds": "^1.0.0-alpha.50",
30
32
  "clsx": "2.1.1",
31
33
  "react-hook-form": "7.51.4",
32
34
  "react-image-size": "2.0.0",
@@ -15,6 +15,7 @@ import ClearFormattingButton from './Tools/ClearFormatting/ClearFormattingButton
15
15
  import ListButtons from './Tools/Lists/ListButtons';
16
16
  import HorizontalLineButton from './Tools/HorizontalLine/HorizontalLineButton';
17
17
  import TableButton from './Tools/Table/TableButton';
18
+ import ContentToolsDropdown from './Tools/ContentTools/ContentToolsDropdown';
18
19
  import { useExtensionNames } from '../hooks';
19
20
 
20
21
  type ToolbarProps = {
@@ -54,6 +55,7 @@ export const Toolbar = ({ isVisible, enableTableTool }: ToolbarProps) => {
54
55
  {extensionNames.image && <ImageButton />}
55
56
  {extensionNames.clearFormatting && <ClearFormattingButton />}
56
57
  {enableTableTool && extensionNames.table && <TableButton />}
58
+ <ContentToolsDropdown />
57
59
  </div>
58
60
  </RemirrorToolbar>
59
61
  );
@@ -0,0 +1,78 @@
1
+ import '@testing-library/jest-dom';
2
+ import { fireEvent } from '@testing-library/react';
3
+ import React from 'react';
4
+ import { useAiService } from '@squiz/dxp-ai-client-react';
5
+ import ContentToolsDropdown from './ContentToolsDropdown';
6
+ import { renderWithContext } from '../../../../tests';
7
+
8
+ // Mock the module
9
+ jest.mock('@squiz/dxp-ai-client-react', () => ({
10
+ useAiService: jest.fn(),
11
+ }));
12
+
13
+ // Cast useAiService to a Jest mock
14
+ const mockedUseAiService = useAiService as jest.MockedFunction<typeof useAiService>;
15
+
16
+ describe('Content tools dropdown', () => {
17
+ it('should not render the content tools dropdown component when there are no content tools', () => {
18
+ mockedUseAiService.mockReturnValue({
19
+ contentTools: [],
20
+ } as any);
21
+
22
+ const { baseElement } = renderWithContext(<ContentToolsDropdown />);
23
+
24
+ expect(baseElement).toBeTruthy();
25
+ expect(baseElement.querySelector('.sds-dropdown.active')).toBeFalsy();
26
+ });
27
+
28
+ it('should render the content tools dropdown when there are content tools', () => {
29
+ mockedUseAiService.mockReturnValue({
30
+ contentTools: [
31
+ {
32
+ id: 'unique-id',
33
+ name: 'Testing content tools',
34
+ },
35
+ ],
36
+ } as any);
37
+
38
+ const { baseElement } = renderWithContext(<ContentToolsDropdown />);
39
+
40
+ expect(baseElement).toBeTruthy();
41
+ expect(baseElement.querySelector(`.sds-dropdown.active`)).toBeFalsy();
42
+
43
+ const launchButton = baseElement.querySelector(`.sds-button`) as HTMLButtonElement;
44
+ expect(launchButton).toBeTruthy();
45
+
46
+ fireEvent.click(launchButton);
47
+ expect(baseElement.querySelector(`.sds-dropdown.active`)).toBeTruthy();
48
+ });
49
+
50
+ it('should handle when a content tool is clicked', () => {
51
+ mockedUseAiService.mockReturnValue({
52
+ contentTools: [
53
+ {
54
+ id: 'unique-id',
55
+ name: 'Testing content tools',
56
+ },
57
+ ],
58
+ } as any);
59
+
60
+ const { baseElement } = renderWithContext(<ContentToolsDropdown />);
61
+
62
+ expect(baseElement).toBeTruthy();
63
+ expect(baseElement.querySelector(`.sds-dropdown.active`)).toBeFalsy();
64
+
65
+ const launchButton = baseElement.querySelector(`.sds-button`) as HTMLButtonElement;
66
+ expect(launchButton).toBeTruthy();
67
+
68
+ fireEvent.click(launchButton);
69
+ expect(baseElement.querySelector(`.sds-dropdown.active`)).toBeTruthy();
70
+
71
+ const actionItems = baseElement.querySelectorAll(`.sds-dropdown-button`);
72
+ const firstItem = actionItems[0] as HTMLButtonElement;
73
+ expect(firstItem.textContent).toBe('Testing content tools');
74
+
75
+ fireEvent.click(firstItem);
76
+ expect(baseElement.querySelector(`.sds-dropdown.active`)).toBeFalsy();
77
+ });
78
+ });
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import { ICON_AI } from '../../../Icons/AiIcon';
3
+ import { BUTTON_FORMAT_TRANSPARENT, BUTTON_THEME_DEFAULT, DROPDOWN_POSITION_RIGHT, Dropdown } from '@squiz/sds';
4
+ import { useAiService } from '@squiz/dxp-ai-client-react';
5
+ import { VerticalDivider } from '@remirror/react';
6
+
7
+ const ContentToolsDropdown = () => {
8
+ const { contentTools } = useAiService();
9
+
10
+ const dropdownItems = contentTools.map((item) => ({
11
+ items: [
12
+ {
13
+ action: () => alert(JSON.stringify(item, null, 2)),
14
+ key: item.id,
15
+ label: <span>{item.name}</span>,
16
+ },
17
+ ],
18
+ key: item.id,
19
+ }));
20
+
21
+ // No content tools to show, don't show dropdown at all
22
+ if (contentTools.length === 0) {
23
+ return null;
24
+ }
25
+
26
+ return (
27
+ <>
28
+ <VerticalDivider />
29
+ <Dropdown
30
+ title="Content tools"
31
+ aria-label="Content tools"
32
+ buttonProps={{
33
+ format: BUTTON_FORMAT_TRANSPARENT,
34
+ icon: ICON_AI,
35
+ theme: BUTTON_THEME_DEFAULT,
36
+ }}
37
+ className="content-tools-dropdown"
38
+ dropdownPosition={DROPDOWN_POSITION_RIGHT}
39
+ heading={'Rewrite to...'}
40
+ sections={dropdownItems}
41
+ />
42
+ </>
43
+ );
44
+ };
45
+
46
+ export default ContentToolsDropdown;
@@ -0,0 +1,45 @@
1
+ .content-tools-dropdown {
2
+ .sds-button-icon svg {
3
+ fill: none !important;
4
+ }
5
+
6
+ .sds-dropdown {
7
+ left: auto;
8
+ right: auto;
9
+ top: auto;
10
+
11
+ width: 188px;
12
+ margin-top: 0.45rem;
13
+ margin-left: -2rem;
14
+
15
+ .sds-dropdown-heading {
16
+ font-size: 0.8125rem;
17
+ font-weight: 600;
18
+ letter-spacing: 0;
19
+ line-height: 1rem;
20
+ background-color: #f5f5f5;
21
+ border-radius: 0.25rem;
22
+ padding: 0.25rem 0.5rem;
23
+ pointer-events: none;
24
+ text-transform: none;
25
+ }
26
+
27
+ .sds-dropdown-section {
28
+ border-top: 1px solid #ededed;
29
+ border-bottom: none;
30
+ padding-top: 0.25rem;
31
+ margin-bottom: 0;
32
+
33
+ .sds-dropdown-label {
34
+ font-size: 0.8125rem;
35
+ color: #4f4f4f;
36
+ line-height: 1rem;
37
+ }
38
+ }
39
+
40
+ &.active {
41
+ display: block;
42
+ padding: 0.25rem;
43
+ }
44
+ }
45
+ }
@@ -1,6 +1,5 @@
1
1
  import React from 'react';
2
2
  import { useCommands, useActive } from '@remirror/react';
3
- import { VerticalDivider } from '@remirror/react-components';
4
3
  import { TableExtension } from '@remirror/extension-react-tables';
5
4
  import Button from '../../../ui/Button/Button';
6
5
  import TableViewRoundedIcon from '@mui/icons-material/TableViewRounded';
@@ -24,7 +23,6 @@ const TableButton = () => {
24
23
  icon={<TableViewRoundedIcon />}
25
24
  label="Insert table"
26
25
  />
27
- <VerticalDivider />
28
26
  </>
29
27
  );
30
28
  };
@@ -0,0 +1,140 @@
1
+ import React from 'react';
2
+
3
+ export const ICON_AI = (
4
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
5
+ <g id="sq-AI-icon" clipPath="url(#clip0_3060_44545)">
6
+ <g id="circle" filter="url(#filter0_dd_3060_44545)">
7
+ <rect x="2" y="2" width="20" height="20" rx="10" fill="url(#paint0_linear_3060_44545)" />
8
+ <rect x="1.5" y="1.5" width="21" height="21" rx="10.5" stroke="white" />
9
+ </g>
10
+ <g id="Vector" filter="url(#filter1_d_3060_44545)">
11
+ <path
12
+ d="M11.2084 9.79157L10.1219 7.39977C9.88269 6.86674 9.11731 6.86674 8.87813 7.39977L7.79157 9.79157L5.39977 10.8781C4.86674 11.1241 4.86674 11.8827 5.39977 12.1219L7.79157 13.2084L8.87813 15.6002C9.12415 16.1333 9.88269 16.1333 10.1219 15.6002L11.2084 13.2084L13.6002 12.1219C14.1333 11.8759 14.1333 11.1173 13.6002 10.8781L11.2084 9.79157Z"
13
+ fill="white"
14
+ />
15
+ </g>
16
+ <g id="Vector_2" opacity="0.8" filter="url(#filter2_d_3060_44545)">
17
+ <path
18
+ d="M17.4491 10.4491L16.8493 11.7779C16.7126 12.074 16.2874 12.074 16.1507 11.7779L15.5509 10.4415L14.2221 9.84169C13.926 9.70501 13.926 9.2874 14.2221 9.15072L15.5585 8.55087L16.1583 7.2221C16.295 6.92597 16.7126 6.92597 16.8493 7.2221L17.4491 8.55847L18.7779 9.15831C19.074 9.29499 19.074 9.7126 18.7779 9.84928L17.4491 10.4491Z"
19
+ fill="white"
20
+ />
21
+ </g>
22
+ <g id="Ellipse 55" opacity="0.8" filter="url(#filter3_d_3060_44545)">
23
+ <circle cx="14.5" cy="16.5" r="1.5" fill="white" />
24
+ </g>
25
+ </g>
26
+ <defs>
27
+ <filter
28
+ id="filter0_dd_3060_44545"
29
+ x="0"
30
+ y="0"
31
+ width="24"
32
+ height="24"
33
+ filterUnits="userSpaceOnUse"
34
+ colorInterpolationFilters="sRGB"
35
+ >
36
+ <feFlood floodOpacity="0" result="BackgroundImageFix" />
37
+ <feColorMatrix
38
+ in="SourceAlpha"
39
+ type="matrix"
40
+ values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
41
+ result="hardAlpha"
42
+ />
43
+ <feOffset dx="1" dy="1" />
44
+ <feComposite in2="hardAlpha" operator="out" />
45
+ <feColorMatrix type="matrix" values="0 0 0 0 0.2 0 0 0 0 0.709941 0 0 0 0 1 0 0 0 0.64 0" />
46
+ <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3060_44545" />
47
+ <feColorMatrix
48
+ in="SourceAlpha"
49
+ type="matrix"
50
+ values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
51
+ result="hardAlpha"
52
+ />
53
+ <feOffset dx="-1" dy="-1" />
54
+ <feComposite in2="hardAlpha" operator="out" />
55
+ <feColorMatrix type="matrix" values="0 0 0 0 0.96 0 0 0 0 0.76 0 0 0 0 0.48 0 0 0 0.64 0" />
56
+ <feBlend mode="normal" in2="effect1_dropShadow_3060_44545" result="effect2_dropShadow_3060_44545" />
57
+ <feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_3060_44545" result="shape" />
58
+ </filter>
59
+ <filter
60
+ id="filter1_d_3060_44545"
61
+ x="5"
62
+ y="7"
63
+ width="9"
64
+ height="10"
65
+ filterUnits="userSpaceOnUse"
66
+ colorInterpolationFilters="sRGB"
67
+ >
68
+ <feFlood floodOpacity="0" result="BackgroundImageFix" />
69
+ <feColorMatrix
70
+ in="SourceAlpha"
71
+ type="matrix"
72
+ values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
73
+ result="hardAlpha"
74
+ />
75
+ <feOffset dy="1" />
76
+ <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0" />
77
+ <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3060_44545" />
78
+ <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3060_44545" result="shape" />
79
+ </filter>
80
+ <filter
81
+ id="filter2_d_3060_44545"
82
+ x="14"
83
+ y="7"
84
+ width="5"
85
+ height="6.30682"
86
+ filterUnits="userSpaceOnUse"
87
+ colorInterpolationFilters="sRGB"
88
+ >
89
+ <feFlood floodOpacity="0" result="BackgroundImageFix" />
90
+ <feColorMatrix
91
+ in="SourceAlpha"
92
+ type="matrix"
93
+ values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
94
+ result="hardAlpha"
95
+ />
96
+ <feOffset dy="1.30682" />
97
+ <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0" />
98
+ <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3060_44545" />
99
+ <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3060_44545" result="shape" />
100
+ </filter>
101
+ <filter
102
+ id="filter3_d_3060_44545"
103
+ x="13"
104
+ y="15"
105
+ width="3"
106
+ height="4.32373"
107
+ filterUnits="userSpaceOnUse"
108
+ colorInterpolationFilters="sRGB"
109
+ >
110
+ <feFlood floodOpacity="0" result="BackgroundImageFix" />
111
+ <feColorMatrix
112
+ in="SourceAlpha"
113
+ type="matrix"
114
+ values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
115
+ result="hardAlpha"
116
+ />
117
+ <feOffset dy="1.32373" />
118
+ <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0" />
119
+ <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3060_44545" />
120
+ <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3060_44545" result="shape" />
121
+ </filter>
122
+ <linearGradient
123
+ id="paint0_linear_3060_44545"
124
+ x1="6.54545"
125
+ y1="3.81818"
126
+ x2="17"
127
+ y2="20.1818"
128
+ gradientUnits="userSpaceOnUse"
129
+ >
130
+ <stop stopColor="#F5D6AB" />
131
+ <stop offset="0.251162" stopColor="#F895A7" />
132
+ <stop offset="0.585" stopColor="#77A1F1" />
133
+ <stop offset="0.861262" stopColor="#68C8FF" />
134
+ </linearGradient>
135
+ <clipPath id="clip0_3060_44545">
136
+ <rect width="24" height="24" fill="white" />
137
+ </clipPath>
138
+ </defs>
139
+ </svg>
140
+ );
package/src/index.scss CHANGED
@@ -3,6 +3,8 @@
3
3
  @import 'tailwindcss/components';
4
4
  @import 'tailwindcss/utilities';
5
5
 
6
+ @import '@squiz/sds/lib/package.css';
7
+
6
8
  /* So we can use icons inside of FTE content */
7
9
  @import 'https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded';
8
10
 
@@ -22,3 +24,5 @@
22
24
  @import './ui/CollapseBox/collapseBox';
23
25
 
24
26
  @import './ui/Modal/modal';
27
+
28
+ @import './EditorToolbar/Tools/ContentTools/content-tools';