@squiz/formatted-text-editor 1.12.0-alpha.40 → 1.12.0-alpha.42
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/jest.config.ts +2 -0
- package/lib/EditorToolbar/EditorToolbar.js +3 -1
- package/lib/EditorToolbar/Tools/Link/Form/LinkForm.d.ts +3 -0
- package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +29 -0
- package/lib/EditorToolbar/Tools/Link/LinkButton.d.ts +7 -0
- package/lib/EditorToolbar/Tools/Link/LinkButton.js +8 -0
- package/lib/EditorToolbar/Tools/Link/Trigger/TriggerLinkModal.d.ts +3 -0
- package/lib/EditorToolbar/Tools/Link/Trigger/TriggerLinkModal.js +12 -0
- package/lib/index.css +401 -3744
- package/lib/ui/Inputs/Select/Select.d.ts +12 -0
- package/lib/ui/Inputs/Select/Select.js +19 -0
- package/lib/ui/Inputs/Text/TextInput.d.ts +8 -0
- package/lib/ui/Inputs/Text/TextInput.js +6 -0
- package/lib/ui/Modal/Modal.d.ts +9 -0
- package/lib/ui/Modal/Modal.js +30 -0
- package/lib/ui/ToolbarButton/ToolbarButton.js +1 -1
- package/package.json +10 -7
- package/src/Editor/_editor.scss +16 -9
- package/src/EditorToolbar/EditorToolbar.tsx +2 -0
- package/src/EditorToolbar/Tools/Bold/BoldButton.spec.tsx +1 -1
- package/src/EditorToolbar/Tools/Italic/ItalicButton.spec.tsx +1 -1
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +21 -0
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +41 -0
- package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +26 -0
- package/src/EditorToolbar/Tools/Link/LinkButton.tsx +25 -0
- package/src/EditorToolbar/Tools/Link/Trigger/TriggerLinkModal.spec.tsx +41 -0
- package/src/EditorToolbar/Tools/Link/Trigger/TriggerLinkModal.tsx +21 -0
- package/src/EditorToolbar/Tools/Underline/Underline.spec.tsx +1 -1
- package/src/EditorToolbar/_editor-toolbar.scss +1 -1
- package/src/index.scss +5 -2
- package/src/ui/DropdownButton/_dropdown-button.scss +3 -2
- package/src/ui/Inputs/Select/Select.spec.tsx +24 -0
- package/src/ui/Inputs/Select/Select.tsx +67 -0
- package/src/ui/Inputs/Text/TextInput.spec.tsx +43 -0
- package/src/ui/Inputs/Text/TextInput.tsx +20 -0
- package/src/ui/Modal/Modal.spec.tsx +75 -0
- package/src/ui/Modal/Modal.tsx +70 -0
- package/src/ui/Modal/_modal.scss +24 -0
- package/src/ui/ToolbarButton/ToolbarButton.tsx +1 -1
- package/src/ui/ToolbarButton/_toolbar-button.scss +5 -25
- package/src/ui/_buttons.scss +19 -0
- package/src/ui/_forms.scss +16 -0
- package/tailwind.config.cjs +23 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
import { Listbox } from '@headlessui/react';
|
2
|
+
import React, { useState } from 'react';
|
3
|
+
import ExpandMoreRoundedIcon from '@mui/icons-material/ExpandMoreRounded';
|
4
|
+
import ExpandLessRoundedIcon from '@mui/icons-material/ExpandLessRounded';
|
5
|
+
import clsx from 'clsx';
|
6
|
+
export const Select = ({ id, label, value, options }) => {
|
7
|
+
const [selectedOption, setSelectedOption] = useState(value);
|
8
|
+
return (React.createElement(React.Fragment, null,
|
9
|
+
label && (React.createElement("label", { htmlFor: id, className: "sds-form-label" }, label)),
|
10
|
+
React.createElement("div", { className: "relative text-md text-gray-800" },
|
11
|
+
React.createElement(Listbox, { value: selectedOption, onChange: setSelectedOption, name: id, "aria-labelledby": id }, ({ open }) => (React.createElement(React.Fragment, null,
|
12
|
+
React.createElement(Listbox.Label, { className: "sr-only" }, label),
|
13
|
+
React.createElement(Listbox.Button, { className: "relative w-full cursor-default rounded border-2 border-gray-300 bg-white py-2 pl-3 pr-10 focus:border-blue-300 focus:outline-none" },
|
14
|
+
React.createElement("span", { className: "flex items-center" },
|
15
|
+
React.createElement("span", { className: "block truncate" }, selectedOption.name)),
|
16
|
+
React.createElement("span", { className: "pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2" }, !open ? (React.createElement(ExpandMoreRoundedIcon, { className: "text-gray-500", "aria-hidden": "true" })) : (React.createElement(ExpandLessRoundedIcon, { className: "text-gray-500", "aria-hidden": "true" })))),
|
17
|
+
React.createElement(Listbox.Options, { className: "absolute z-10 mt-1 w-full overflow-auto rounded border-2 border-gray-300 bg-white" }, options.map((option) => (React.createElement(Listbox.Option, { key: option.id, className: ({ active }) => clsx(active ? 'bg-gray-100' : 'bg-white', 'relative cursor-default select-none flex hover:bg-gray-100 py-2 px-3'), value: option },
|
18
|
+
React.createElement("span", { className: "block truncate" }, option.name)))))))))));
|
19
|
+
};
|
@@ -0,0 +1,6 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
export const TextInput = ({ id, label, value, ...rest }) => {
|
3
|
+
return (React.createElement(React.Fragment, null,
|
4
|
+
label && (React.createElement("label", { htmlFor: id, className: "sds-form-label" }, label)),
|
5
|
+
React.createElement("input", { name: id, id: id, type: "text", className: "sds-form-control", defaultValue: value, ...rest })));
|
6
|
+
};
|
@@ -0,0 +1,9 @@
|
|
1
|
+
import { ReactElement } from 'react';
|
2
|
+
type ModalProps = {
|
3
|
+
children: ReactElement;
|
4
|
+
onCancel: () => void;
|
5
|
+
onSubmit?: () => void;
|
6
|
+
className?: string;
|
7
|
+
};
|
8
|
+
declare const Modal: ({ children, onCancel, onSubmit, className }: ModalProps) => ReactElement;
|
9
|
+
export default Modal;
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import React, { useEffect } from 'react';
|
2
|
+
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
3
|
+
const Modal = ({ children, onCancel, onSubmit, className }) => {
|
4
|
+
const keydown = (e) => {
|
5
|
+
if (e.key === 'Escape') {
|
6
|
+
onCancel();
|
7
|
+
}
|
8
|
+
if (e.key === 'Enter') {
|
9
|
+
onSubmit && onSubmit();
|
10
|
+
}
|
11
|
+
};
|
12
|
+
useEffect(() => {
|
13
|
+
window.addEventListener('keydown', keydown);
|
14
|
+
return () => window.removeEventListener('keydown', keydown);
|
15
|
+
}, []);
|
16
|
+
return (React.createElement(React.Fragment, null,
|
17
|
+
React.createElement("div", { className: `sds-modal-wrapper ${className ? className : ''}` },
|
18
|
+
React.createElement("div", { className: "w-modal-sm my-6 mx-auto" },
|
19
|
+
React.createElement("div", { className: "sds-modal" },
|
20
|
+
React.createElement("div", { className: "sds-modal-header p-6 pb-2" },
|
21
|
+
React.createElement("h2", { className: "font-semibold text-gray-900 text-heading-2" }, "Link"),
|
22
|
+
React.createElement("button", { className: "ml-auto -mr-3 -mt-3 bg-transparent border-0 text-gray-600 font-semibold outline-none focus:outline-none hover:text-color-gray-800", onClick: onCancel, "aria-label": "Close" },
|
23
|
+
React.createElement(CloseRoundedIcon, null))),
|
24
|
+
React.createElement("div", { className: "sds-modal-content" }, children),
|
25
|
+
React.createElement("div", { className: "sds-modal-footer p-6 pt-3" },
|
26
|
+
React.createElement("button", { className: "sds-modal-footer__button bg-gray-200 text-gray-700 mr-2 hover:bg-gray-300", type: "button", onClick: onCancel, "aria-label": "Cancel" }, "Cancel"),
|
27
|
+
onSubmit && (React.createElement("button", { className: "sds-modal-footer__button bg-blue-300 text-white hover:bg-blue-400", type: "button", onClick: onSubmit, "aria-label": "Apply" }, "Apply")))))),
|
28
|
+
React.createElement("div", { className: "opacity-25 fixed inset-0 z-40 bg-black" })));
|
29
|
+
};
|
30
|
+
export default Modal;
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
const ToolbarButton = ({ handleOnClick, isDisabled, isActive, icon, label }) => {
|
3
|
-
return (React.createElement("button", { "aria-label": label, title: label, type: "button", onClick: handleOnClick, disabled: isDisabled, className: `btn toolbar-button ${isActive ? 'is-active' : ''}` }, icon));
|
3
|
+
return (React.createElement("button", { "aria-label": label, title: label, type: "button", onClick: handleOnClick, disabled: isDisabled, className: `sds-btn toolbar-button ${isActive ? 'is-active' : ''}` }, icon));
|
4
4
|
};
|
5
5
|
export default ToolbarButton;
|
package/package.json
CHANGED
@@ -1,29 +1,32 @@
|
|
1
1
|
{
|
2
2
|
"name": "@squiz/formatted-text-editor",
|
3
|
-
"version": "1.12.0-alpha.
|
3
|
+
"version": "1.12.0-alpha.42",
|
4
4
|
"main": "lib/index.js",
|
5
5
|
"types": "lib/index.d.ts",
|
6
6
|
"scripts": {
|
7
|
-
"
|
7
|
+
"build": "vite build",
|
8
8
|
"clean": "rimraf ./lib",
|
9
9
|
"compile": "npm run clean && npm run compile:styles && npm run compile:code",
|
10
10
|
"compile:code": "tsc",
|
11
11
|
"compile:styles": "node build.js",
|
12
|
+
"dev": "vite",
|
12
13
|
"preview": "vite preview",
|
13
|
-
"build": "vite build",
|
14
14
|
"test": "jest",
|
15
|
-
"test:
|
16
|
-
"test:
|
15
|
+
"test:e2e": "vite build && vite preview --port 8080 & cypress open",
|
16
|
+
"test:watch": "jest --watch"
|
17
17
|
},
|
18
18
|
"dependencies": {
|
19
|
+
"@headlessui/react": "1.7.11",
|
19
20
|
"@mui/icons-material": "5.11.0",
|
20
21
|
"@remirror/react": "2.0.25",
|
22
|
+
"clsx": "^1.2.1",
|
21
23
|
"flowbite": "^1.6.3"
|
22
24
|
},
|
23
25
|
"devDependencies": {
|
24
26
|
"@testing-library/cypress": "9.0.0",
|
25
27
|
"@testing-library/jest-dom": "5.16.5",
|
26
|
-
"@testing-library/react": "
|
28
|
+
"@testing-library/react": "14.0.0",
|
29
|
+
"@testing-library/user-event": "14.4.3",
|
27
30
|
"@types/react": "18.0.26",
|
28
31
|
"@types/react-dom": "18.0.9",
|
29
32
|
"@vitejs/plugin-react": "3.0.0",
|
@@ -61,5 +64,5 @@
|
|
61
64
|
"volta": {
|
62
65
|
"node": "16.19.0"
|
63
66
|
},
|
64
|
-
"gitHead": "
|
67
|
+
"gitHead": "e6a1ba4e3c9f944bd0006ed59673996dc5dccbfa"
|
65
68
|
}
|
package/src/Editor/_editor.scss
CHANGED
@@ -10,11 +10,12 @@
|
|
10
10
|
}
|
11
11
|
|
12
12
|
.remirror-editor {
|
13
|
-
@apply bg-white shadow-none rounded-b;
|
13
|
+
@apply bg-white shadow-none rounded-b p-3;
|
14
|
+
min-height: 6rem;
|
14
15
|
|
15
16
|
&:active,
|
16
|
-
|
17
|
-
|
17
|
+
&:focus {
|
18
|
+
@apply outline-0;
|
18
19
|
}
|
19
20
|
|
20
21
|
p {
|
@@ -22,16 +23,22 @@
|
|
22
23
|
@apply block;
|
23
24
|
}
|
24
25
|
}
|
25
|
-
}
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
27
|
+
.remirror-is-empty:first-of-type::before {
|
28
|
+
position: absolute;
|
29
|
+
pointer-events: none;
|
30
|
+
height: 0;
|
31
|
+
font-style: italic;
|
32
|
+
content: attr(data-placeholder);
|
33
|
+
@apply text-gray-500;
|
32
34
|
}
|
33
35
|
}
|
34
36
|
|
37
|
+
a {
|
38
|
+
@apply text-blue-300;
|
39
|
+
text-decoration: underline;
|
40
|
+
}
|
41
|
+
|
35
42
|
.remirror-theme h1 {
|
36
43
|
font-size: 1.625rem;
|
37
44
|
font-weight: 600;
|
@@ -8,6 +8,7 @@ import TextAlignButtons from './Tools/TextAlign/TextAlignButtons';
|
|
8
8
|
import UndoButton from './Tools/Undo/UndoButton';
|
9
9
|
import RedoButton from './Tools/Redo/RedoButton';
|
10
10
|
import TextTypeDropdown from './Tools/TextType/TextTypeDropdown';
|
11
|
+
import TriggerLinkModal from './Tools/Link/Trigger/TriggerLinkModal';
|
11
12
|
|
12
13
|
type EditorToolbarProps = {
|
13
14
|
manager: RemirrorManager<any>;
|
@@ -38,6 +39,7 @@ export const EditorToolbar = ({ manager, isPopover }: EditorToolbarProps) => {
|
|
38
39
|
{extensionNames.italic && <ItalicButton />}
|
39
40
|
{extensionNames.underline && <UnderlineButton />}
|
40
41
|
{extensionNames.nodeFormatting && <TextAlignButtons />}
|
42
|
+
<TriggerLinkModal />
|
41
43
|
</Toolbar>
|
42
44
|
) : (
|
43
45
|
<FloatingToolbar className="remirror-floating-popover">
|
@@ -11,7 +11,7 @@ describe('Bold button', () => {
|
|
11
11
|
|
12
12
|
it('Activates the button if clicked', () => {
|
13
13
|
render(<Editor />);
|
14
|
-
expect(screen.getByRole('button', { name: 'Bold (cmd+B)' }).classList.contains('btn')).toBeTruthy();
|
14
|
+
expect(screen.getByRole('button', { name: 'Bold (cmd+B)' }).classList.contains('sds-btn')).toBeTruthy();
|
15
15
|
const bold = screen.getByRole('button', { name: 'Bold (cmd+B)' });
|
16
16
|
fireEvent.click(bold);
|
17
17
|
expect(bold.classList.contains('is-active')).toBeTruthy();
|
@@ -11,7 +11,7 @@ describe('Italic button', () => {
|
|
11
11
|
|
12
12
|
it('Activates the button if clicked', () => {
|
13
13
|
render(<Editor />);
|
14
|
-
expect(screen.getByRole('button', { name: 'Italic (cmd+I)' }).classList.contains('btn')).toBeTruthy();
|
14
|
+
expect(screen.getByRole('button', { name: 'Italic (cmd+I)' }).classList.contains('sds-btn')).toBeTruthy();
|
15
15
|
const italic = screen.getByRole('button', { name: 'Italic (cmd+I)' });
|
16
16
|
fireEvent.click(italic);
|
17
17
|
expect(italic.classList.contains('is-active')).toBeTruthy();
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import '@testing-library/jest-dom';
|
2
|
+
import { render, screen } from '@testing-library/react';
|
3
|
+
import React from 'react';
|
4
|
+
import LinkForm from './LinkForm';
|
5
|
+
|
6
|
+
describe('Link Form', () => {
|
7
|
+
it('Renders the form with the relevant fields', () => {
|
8
|
+
render(<LinkForm />);
|
9
|
+
expect(screen.getByLabelText('URL')).toBeInTheDocument();
|
10
|
+
expect(screen.getByLabelText('Text')).toBeInTheDocument();
|
11
|
+
expect(screen.getByLabelText('Title')).toBeInTheDocument();
|
12
|
+
expect(screen.getByLabelText('Target')).toBeInTheDocument();
|
13
|
+
});
|
14
|
+
|
15
|
+
it('Renders the form with the specified class', () => {
|
16
|
+
render(<LinkForm />);
|
17
|
+
|
18
|
+
const formClass = document.querySelector('.sds-form');
|
19
|
+
expect(formClass).toBeInTheDocument();
|
20
|
+
});
|
21
|
+
});
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import React, { ReactElement, useState } from 'react';
|
2
|
+
import { TextInput } from '../../../../ui/Inputs/Text/TextInput';
|
3
|
+
import { Select } from '../../../../ui/Inputs/Select/Select';
|
4
|
+
|
5
|
+
const selectOptions = [
|
6
|
+
{ id: 1, name: 'Current window', key: '_blank' },
|
7
|
+
{ id: 2, name: 'New window', key: '_parent' },
|
8
|
+
];
|
9
|
+
|
10
|
+
const LinkForm = (): ReactElement => {
|
11
|
+
const [formValues, setFormValues] = useState({
|
12
|
+
href: '',
|
13
|
+
target: selectOptions[0],
|
14
|
+
title: '',
|
15
|
+
text: '',
|
16
|
+
});
|
17
|
+
|
18
|
+
const handleChange = (event: { target: { name: string; value: string } }) => {
|
19
|
+
const { name, value } = event.target;
|
20
|
+
setFormValues({ ...formValues, [name]: value });
|
21
|
+
};
|
22
|
+
|
23
|
+
return (
|
24
|
+
<form action="" method="post" className="sds-form">
|
25
|
+
<div className="sds-form-group mb-2">
|
26
|
+
<TextInput id="href" label="URL" value={formValues.href} onChange={handleChange} />
|
27
|
+
</div>
|
28
|
+
<div className="sds-form-group mb-2">
|
29
|
+
<TextInput id="text" label="Text" value={formValues.text} onChange={handleChange} />
|
30
|
+
</div>
|
31
|
+
<div className="sds-form-group mb-2">
|
32
|
+
<TextInput id="title" label="Title" value={formValues.title} onChange={handleChange} />
|
33
|
+
</div>
|
34
|
+
<div className="sds-form-group mb-0">
|
35
|
+
<Select id="target" label="Target" value={formValues.target} options={selectOptions} />
|
36
|
+
</div>
|
37
|
+
</form>
|
38
|
+
);
|
39
|
+
};
|
40
|
+
|
41
|
+
export default LinkForm;
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import '@testing-library/jest-dom';
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
3
|
+
import React from 'react';
|
4
|
+
import LinkButton from './LinkButton';
|
5
|
+
|
6
|
+
describe('Link button', () => {
|
7
|
+
const mockOnClick = jest.fn();
|
8
|
+
|
9
|
+
it('Renders the link button', () => {
|
10
|
+
render(<LinkButton showModal={false} onClick={mockOnClick} enabled />);
|
11
|
+
expect(screen.getByRole('button', { name: 'Link' })).toBeInTheDocument();
|
12
|
+
});
|
13
|
+
|
14
|
+
it('Renders a disabled button if this is what you have set', () => {
|
15
|
+
render(<LinkButton showModal={false} onClick={mockOnClick} enabled={false} />);
|
16
|
+
expect(screen.getByRole('button', { name: 'Link' })).toBeDisabled();
|
17
|
+
});
|
18
|
+
|
19
|
+
it('Calls the modalFire function if you click it', () => {
|
20
|
+
render(<LinkButton showModal={false} onClick={mockOnClick} enabled />);
|
21
|
+
const linkButton = screen.getByRole('button', { name: 'Link' });
|
22
|
+
expect(linkButton).not.toBeDisabled();
|
23
|
+
fireEvent.click(linkButton);
|
24
|
+
expect(mockOnClick).toHaveBeenCalled();
|
25
|
+
});
|
26
|
+
});
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import ToolbarButton from '../../../ui/ToolbarButton/ToolbarButton';
|
3
|
+
import InsertLinkRoundedIcon from '@mui/icons-material/InsertLinkRounded';
|
4
|
+
|
5
|
+
type LinkButtonProps = {
|
6
|
+
onClick: () => void;
|
7
|
+
enabled: boolean;
|
8
|
+
showModal: boolean;
|
9
|
+
};
|
10
|
+
|
11
|
+
const LinkButton = ({ onClick, showModal, enabled }: LinkButtonProps) => {
|
12
|
+
const active = showModal;
|
13
|
+
|
14
|
+
return (
|
15
|
+
<ToolbarButton
|
16
|
+
handleOnClick={onClick}
|
17
|
+
isDisabled={!enabled}
|
18
|
+
isActive={active}
|
19
|
+
icon={<InsertLinkRoundedIcon />}
|
20
|
+
label={`Link`}
|
21
|
+
/>
|
22
|
+
);
|
23
|
+
};
|
24
|
+
|
25
|
+
export default LinkButton;
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import '@testing-library/jest-dom';
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
3
|
+
import React from 'react';
|
4
|
+
import TriggerLinkModal from './TriggerLinkModal';
|
5
|
+
|
6
|
+
describe('TriggerLinkModal', () => {
|
7
|
+
it('Renders the trigger button', () => {
|
8
|
+
render(<TriggerLinkModal />);
|
9
|
+
expect(screen.getByRole('button', { name: 'Link' })).toBeInTheDocument();
|
10
|
+
});
|
11
|
+
|
12
|
+
it('Displays the modal when clicking on the trigger button', () => {
|
13
|
+
render(<TriggerLinkModal />);
|
14
|
+
|
15
|
+
// Open the modal
|
16
|
+
const triggerButton = screen.getByRole('button', { name: 'Link' });
|
17
|
+
fireEvent.click(triggerButton);
|
18
|
+
|
19
|
+
const modalHeading = screen.getByRole('heading', {
|
20
|
+
name: /Link/i,
|
21
|
+
});
|
22
|
+
expect(modalHeading).toBeInTheDocument();
|
23
|
+
});
|
24
|
+
|
25
|
+
it('Closes the modal when clicking on the cancel button', () => {
|
26
|
+
render(<TriggerLinkModal />);
|
27
|
+
// Open the modal
|
28
|
+
const triggerButton = screen.getByRole('button', { name: 'Link' });
|
29
|
+
fireEvent.click(triggerButton);
|
30
|
+
const modalHeading = screen.getByRole('heading', {
|
31
|
+
name: /Link/i,
|
32
|
+
});
|
33
|
+
expect(modalHeading).toBeInTheDocument();
|
34
|
+
// Check that the cancel button renders
|
35
|
+
const cancelButton = screen.getByRole('button', { name: 'Cancel' });
|
36
|
+
expect(cancelButton).toBeInTheDocument();
|
37
|
+
// check that firing the cancel button fires the cancel function
|
38
|
+
fireEvent.click(cancelButton);
|
39
|
+
expect(modalHeading).not.toBeInTheDocument();
|
40
|
+
});
|
41
|
+
});
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import React, { ReactElement, useState } from 'react';
|
2
|
+
import LinkButton from '../LinkButton';
|
3
|
+
import Modal from '../../../../ui/Modal/Modal';
|
4
|
+
import LinkForm from '../Form/LinkForm';
|
5
|
+
|
6
|
+
const TriggerLinkModal = (): ReactElement => {
|
7
|
+
const [showModal, setShowModal] = useState(false);
|
8
|
+
|
9
|
+
return (
|
10
|
+
<>
|
11
|
+
<LinkButton showModal={showModal} onClick={() => setShowModal(true)} enabled />
|
12
|
+
{showModal ? (
|
13
|
+
<Modal onCancel={() => setShowModal(false)} onSubmit={() => console.log('Sumbmit form')}>
|
14
|
+
<LinkForm />
|
15
|
+
</Modal>
|
16
|
+
) : null}
|
17
|
+
</>
|
18
|
+
);
|
19
|
+
};
|
20
|
+
|
21
|
+
export default TriggerLinkModal;
|
@@ -11,7 +11,7 @@ describe('Underline button', () => {
|
|
11
11
|
|
12
12
|
it('Activates the button if clicked', () => {
|
13
13
|
render(<Editor />);
|
14
|
-
expect(screen.getByRole('button', { name: 'Underline (cmd+U)' }).classList.contains('btn')).toBeTruthy();
|
14
|
+
expect(screen.getByRole('button', { name: 'Underline (cmd+U)' }).classList.contains('sds-btn')).toBeTruthy();
|
15
15
|
const underline = screen.getByRole('button', { name: 'Underline (cmd+U)' });
|
16
16
|
fireEvent.click(underline);
|
17
17
|
expect(underline.classList.contains('is-active')).toBeTruthy();
|
package/src/index.scss
CHANGED
@@ -3,8 +3,9 @@
|
|
3
3
|
@import 'tailwindcss/components';
|
4
4
|
@import 'tailwindcss/utilities';
|
5
5
|
|
6
|
-
/*
|
7
|
-
@import '
|
6
|
+
/* Global */
|
7
|
+
@import './ui/forms';
|
8
|
+
@import './ui/buttons';
|
8
9
|
|
9
10
|
/* Components */
|
10
11
|
@import './Editor/editor';
|
@@ -13,3 +14,5 @@
|
|
13
14
|
@import './ui/ToolbarButton/toolbar-button';
|
14
15
|
@import './ui/ToolbarDropdown/toolbar-dropdown';
|
15
16
|
@import './ui/DropdownButton/dropdown-button';
|
17
|
+
|
18
|
+
@import './ui/Modal/modal';
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import '@testing-library/jest-dom';
|
2
|
+
import { render, screen } from '@testing-library/react';
|
3
|
+
import React from 'react';
|
4
|
+
import { Select } from './Select';
|
5
|
+
|
6
|
+
describe('Select field', () => {
|
7
|
+
const selectOptions = [
|
8
|
+
{ id: 1, name: 'Chicken', key: 'chicken' },
|
9
|
+
{ id: 2, name: 'Egg', key: 'egg' },
|
10
|
+
];
|
11
|
+
|
12
|
+
it('Renders the label', () => {
|
13
|
+
render(<Select id="select-field" label="Which came first?" value="Chicken" options={selectOptions} />);
|
14
|
+
// Check that the supplied label renders
|
15
|
+
const inputLabel = screen.getByLabelText('Which came first?');
|
16
|
+
expect(inputLabel).toBeInTheDocument();
|
17
|
+
});
|
18
|
+
|
19
|
+
it('Renders the default value', () => {
|
20
|
+
render(<Select id="select-field" label="Which came first?" value="Chicken" options={selectOptions} />);
|
21
|
+
// Check that default value supplied renders
|
22
|
+
expect(screen.getByDisplayValue('Chicken')).toBeInTheDocument();
|
23
|
+
});
|
24
|
+
});
|
@@ -0,0 +1,67 @@
|
|
1
|
+
import { Listbox } from '@headlessui/react';
|
2
|
+
import React, { useState } from 'react';
|
3
|
+
import ExpandMoreRoundedIcon from '@mui/icons-material/ExpandMoreRounded';
|
4
|
+
import ExpandLessRoundedIcon from '@mui/icons-material/ExpandLessRounded';
|
5
|
+
import clsx from 'clsx';
|
6
|
+
type SelectProps = {
|
7
|
+
id: string;
|
8
|
+
label?: string;
|
9
|
+
value: any;
|
10
|
+
options: {
|
11
|
+
id: number;
|
12
|
+
name: string;
|
13
|
+
key: string;
|
14
|
+
}[];
|
15
|
+
};
|
16
|
+
|
17
|
+
export const Select = ({ id, label, value, options }: SelectProps) => {
|
18
|
+
const [selectedOption, setSelectedOption] = useState(value);
|
19
|
+
|
20
|
+
return (
|
21
|
+
<>
|
22
|
+
{label && (
|
23
|
+
<label htmlFor={id} className="sds-form-label">
|
24
|
+
{label}
|
25
|
+
</label>
|
26
|
+
)}
|
27
|
+
<div className="relative text-md text-gray-800">
|
28
|
+
<Listbox value={selectedOption} onChange={setSelectedOption} name={id} aria-labelledby={id}>
|
29
|
+
{({ open }) => (
|
30
|
+
<>
|
31
|
+
<Listbox.Label className="sr-only">{label}</Listbox.Label>
|
32
|
+
<Listbox.Button className="relative w-full cursor-default rounded border-2 border-gray-300 bg-white py-2 pl-3 pr-10 focus:border-blue-300 focus:outline-none">
|
33
|
+
<span className="flex items-center">
|
34
|
+
<span className="block truncate">{selectedOption.name}</span>
|
35
|
+
</span>
|
36
|
+
<span className="pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2">
|
37
|
+
{!open ? (
|
38
|
+
<ExpandMoreRoundedIcon className="text-gray-500" aria-hidden="true" />
|
39
|
+
) : (
|
40
|
+
<ExpandLessRoundedIcon className="text-gray-500" aria-hidden="true" />
|
41
|
+
)}
|
42
|
+
</span>
|
43
|
+
</Listbox.Button>
|
44
|
+
|
45
|
+
<Listbox.Options className="absolute z-10 mt-1 w-full overflow-auto rounded border-2 border-gray-300 bg-white">
|
46
|
+
{options.map((option) => (
|
47
|
+
<Listbox.Option
|
48
|
+
key={option.id}
|
49
|
+
className={({ active }) =>
|
50
|
+
clsx(
|
51
|
+
active ? 'bg-gray-100' : 'bg-white',
|
52
|
+
'relative cursor-default select-none flex hover:bg-gray-100 py-2 px-3',
|
53
|
+
)
|
54
|
+
}
|
55
|
+
value={option}
|
56
|
+
>
|
57
|
+
<span className="block truncate">{option.name}</span>
|
58
|
+
</Listbox.Option>
|
59
|
+
))}
|
60
|
+
</Listbox.Options>
|
61
|
+
</>
|
62
|
+
)}
|
63
|
+
</Listbox>
|
64
|
+
</div>
|
65
|
+
</>
|
66
|
+
);
|
67
|
+
};
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import '@testing-library/jest-dom';
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
3
|
+
import React from 'react';
|
4
|
+
import { TextInput } from './TextInput';
|
5
|
+
|
6
|
+
describe('Text input', () => {
|
7
|
+
const mockOnChange = jest.fn();
|
8
|
+
|
9
|
+
const TextInputComponent = () => {
|
10
|
+
return <TextInput id="text-input" value="Water" label="Text input" onChange={mockOnChange} />;
|
11
|
+
};
|
12
|
+
|
13
|
+
it('Renders the label', () => {
|
14
|
+
render(<TextInputComponent />);
|
15
|
+
// Check that the supplied label renders
|
16
|
+
const inputLabel = screen.getByLabelText('Text input');
|
17
|
+
expect(inputLabel).toBeInTheDocument();
|
18
|
+
});
|
19
|
+
|
20
|
+
it('Renders the default value', () => {
|
21
|
+
render(<TextInputComponent />);
|
22
|
+
// Check that default value supplied renders
|
23
|
+
expect(screen.getByDisplayValue('Water')).toBeInTheDocument();
|
24
|
+
});
|
25
|
+
|
26
|
+
it('Changes the value when new value entered', () => {
|
27
|
+
render(<TextInputComponent />);
|
28
|
+
const input = screen.getByLabelText('Text input') as HTMLInputElement;
|
29
|
+
// Check that default value supplied renders
|
30
|
+
expect(input.value).toBe('Water');
|
31
|
+
fireEvent.change(input, { target: { value: 'Wine' } });
|
32
|
+
expect(input.value).toBe('Wine');
|
33
|
+
});
|
34
|
+
|
35
|
+
it('Fires the change function when new value entered', () => {
|
36
|
+
render(<TextInputComponent />);
|
37
|
+
const input = screen.getByLabelText('Text input') as HTMLInputElement;
|
38
|
+
// Check that default value supplied renders
|
39
|
+
expect(input.value).toBe('Water');
|
40
|
+
fireEvent.change(input, { target: { value: 'Wine' } });
|
41
|
+
expect(mockOnChange).toHaveBeenCalled();
|
42
|
+
});
|
43
|
+
});
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
|
3
|
+
type TextInputProps = {
|
4
|
+
id: string;
|
5
|
+
label?: string;
|
6
|
+
value: string;
|
7
|
+
onChange: any;
|
8
|
+
};
|
9
|
+
export const TextInput = ({ id, label, value, ...rest }: TextInputProps) => {
|
10
|
+
return (
|
11
|
+
<>
|
12
|
+
{label && (
|
13
|
+
<label htmlFor={id} className="sds-form-label">
|
14
|
+
{label}
|
15
|
+
</label>
|
16
|
+
)}
|
17
|
+
<input name={id} id={id} type="text" className="sds-form-control" defaultValue={value} {...rest} />
|
18
|
+
</>
|
19
|
+
);
|
20
|
+
};
|