@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.
Files changed (43) hide show
  1. package/jest.config.ts +2 -0
  2. package/lib/EditorToolbar/EditorToolbar.js +3 -1
  3. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.d.ts +3 -0
  4. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +29 -0
  5. package/lib/EditorToolbar/Tools/Link/LinkButton.d.ts +7 -0
  6. package/lib/EditorToolbar/Tools/Link/LinkButton.js +8 -0
  7. package/lib/EditorToolbar/Tools/Link/Trigger/TriggerLinkModal.d.ts +3 -0
  8. package/lib/EditorToolbar/Tools/Link/Trigger/TriggerLinkModal.js +12 -0
  9. package/lib/index.css +401 -3744
  10. package/lib/ui/Inputs/Select/Select.d.ts +12 -0
  11. package/lib/ui/Inputs/Select/Select.js +19 -0
  12. package/lib/ui/Inputs/Text/TextInput.d.ts +8 -0
  13. package/lib/ui/Inputs/Text/TextInput.js +6 -0
  14. package/lib/ui/Modal/Modal.d.ts +9 -0
  15. package/lib/ui/Modal/Modal.js +30 -0
  16. package/lib/ui/ToolbarButton/ToolbarButton.js +1 -1
  17. package/package.json +10 -7
  18. package/src/Editor/_editor.scss +16 -9
  19. package/src/EditorToolbar/EditorToolbar.tsx +2 -0
  20. package/src/EditorToolbar/Tools/Bold/BoldButton.spec.tsx +1 -1
  21. package/src/EditorToolbar/Tools/Italic/ItalicButton.spec.tsx +1 -1
  22. package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +21 -0
  23. package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +41 -0
  24. package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +26 -0
  25. package/src/EditorToolbar/Tools/Link/LinkButton.tsx +25 -0
  26. package/src/EditorToolbar/Tools/Link/Trigger/TriggerLinkModal.spec.tsx +41 -0
  27. package/src/EditorToolbar/Tools/Link/Trigger/TriggerLinkModal.tsx +21 -0
  28. package/src/EditorToolbar/Tools/Underline/Underline.spec.tsx +1 -1
  29. package/src/EditorToolbar/_editor-toolbar.scss +1 -1
  30. package/src/index.scss +5 -2
  31. package/src/ui/DropdownButton/_dropdown-button.scss +3 -2
  32. package/src/ui/Inputs/Select/Select.spec.tsx +24 -0
  33. package/src/ui/Inputs/Select/Select.tsx +67 -0
  34. package/src/ui/Inputs/Text/TextInput.spec.tsx +43 -0
  35. package/src/ui/Inputs/Text/TextInput.tsx +20 -0
  36. package/src/ui/Modal/Modal.spec.tsx +75 -0
  37. package/src/ui/Modal/Modal.tsx +70 -0
  38. package/src/ui/Modal/_modal.scss +24 -0
  39. package/src/ui/ToolbarButton/ToolbarButton.tsx +1 -1
  40. package/src/ui/ToolbarButton/_toolbar-button.scss +5 -25
  41. package/src/ui/_buttons.scss +19 -0
  42. package/src/ui/_forms.scss +16 -0
  43. package/tailwind.config.cjs +23 -0
@@ -0,0 +1,12 @@
1
+ type SelectProps = {
2
+ id: string;
3
+ label?: string;
4
+ value: any;
5
+ options: {
6
+ id: number;
7
+ name: string;
8
+ key: string;
9
+ }[];
10
+ };
11
+ export declare const Select: ({ id, label, value, options }: SelectProps) => JSX.Element;
12
+ export {};
@@ -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,8 @@
1
+ type TextInputProps = {
2
+ id: string;
3
+ label?: string;
4
+ value: string;
5
+ onChange: any;
6
+ };
7
+ export declare const TextInput: ({ id, label, value, ...rest }: TextInputProps) => JSX.Element;
8
+ export {};
@@ -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.40",
3
+ "version": "1.12.0-alpha.42",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "scripts": {
7
- "dev": "vite",
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:watch": "jest --watch",
16
- "test:e2e": "vite build && vite preview --port 8080 & cypress open"
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": "12.1.5",
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": "8db852ade2813dc297b7d0fa4975e0c5a57ac83e"
67
+ "gitHead": "e6a1ba4e3c9f944bd0006ed59673996dc5dccbfa"
65
68
  }
@@ -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
- :focus {
17
- /* some focus state */
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
- /* remove existing style */
28
- .remirror-theme .ProseMirror {
29
- &:active,
30
- &:focus {
31
- @apply shadow-none;
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();
@@ -1,5 +1,5 @@
1
1
  .formatted-text-editor .editor-toolbar {
2
- @apply bg-white rounded border-gray-200 border-2 border-solid p-1;
2
+ @apply bg-white border-gray-200 border-b-2 border-solid p-1;
3
3
 
4
4
  display: flex;
5
5
  justify-items: center;
package/src/index.scss CHANGED
@@ -3,8 +3,9 @@
3
3
  @import 'tailwindcss/components';
4
4
  @import 'tailwindcss/utilities';
5
5
 
6
- /* Remirror base */
7
- @import 'remirror/styles/all.css';
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';
@@ -1,7 +1,8 @@
1
1
  .dropdown-button {
2
- padding: 4px 8px;
3
- color: #707070;
4
2
  height: 40px;
5
3
  width: 100%;
6
4
  justify-content: space-between;
5
+ align-items: center;
6
+ display: flex;
7
+ @apply px-2 py-1 text-gray-600;
7
8
  }
@@ -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
+ };