Skip to content

Commit

Permalink
Add Rich text editor (#294)
Browse files Browse the repository at this point in the history
* copy all files from paragraph to use as the basis for the rich text component.

* update icons for rich text component

* resolve conflicts

* wip on text formatting

* get ready for setvalue

* Refactor rich text handling in form components

Updated RichTextPatternEdit to sync editor content with form state. Improved FormSummary rendering and enhanced form context with setValue function.

* code formatting

* add custom toolbar modules

* formatting

* add types to handlechange

* add small format

* pare down editor features

* hidden field to mirror editor contents

* install react-quill and increaes concurrency

* fix type

* remove logging

* remove logging

* remove logging

* allow br tag in editor

* formatting and label updates

* swap quill for tiptap

* fix build error with failing checks in form package

* Rename and update RichTextPatternEdit components

* Refactor editor button actions into a reusable array

Consolidated individual editor button action handlers into a reusable editorActions array. This refactor simplifies the button creation process and enhances maintainability by reducing repeated code.

* Add tests for lists

* Dry out test a little

* dry out test even more.

* code formatting

* Add CSS styles for RichText components

Introduced new CSS modules to standardize the margin behavior of list items within RichText components. Updated the corresponding JSX files to apply these styles for consistent appearance.

* Remove react-quill

* remove prop from type for rich text

* use patternvalue instead of string to correct typing issue

* update styles of texteditor

* exclude test that relies on browser feature from vitest runner

* cleanup vitest config

* remove save on blur for rich-text fields

* change button type

* remove top margin from editor content first child

* add focus to editor when initialized

* fix css selector

* remove richtext check onblur

* remove dbs from git

* update form handling submission

Check for rich text handler in the form blur event. Manage editor saving onChange

* Add debounce for improved performance in RichTextPatternEdit

Implemented debounce to optimize editor updates and prevent unnecessary re-renders. Memoized toolbar component.

* Add optional validation rule to form fields

Updated the form field registration to accept optional validation rules through the `register` method.

* Add optional validation rule to form fields

Updated the form field registration to accept optional validation rules through the `register` method.

* add adr

* Refactor MenuBar for enhanced editor functionality

Remove `React.memo` from `MenuBar` component and add more fine-grained control with `disabled` property for button actions. Introduce new heading levels and update related stories to reflect these changes.

* add types and style editor for long blocks of text.

Adds new styles, change default heading hierarchy, and update tests

* turn off spellcheck. squigglies were problematic for long documents.

* match styles from edit view to preview.

* move label

* use vitest browser and install deps

* update test config to try running browser tests in CI

* install playwright browsers in ci

* install dependencies

* update install command

* fix failing tests in e2e

* formatting code

* update test commands for watch and dev scenarios

* update documentation

* add aria-live for contents that change with soft nav

* added css file extensions to prettier config

* update deprecated command

* Rich Text Component Toolbar buttons & API updates (#300)

* pull latest from rich text branch and clean up

* ignore cdktf type errors

* install tf providers to pass typecheck

---------

Co-authored-by: Daniel Naab <dan@crushingpennies.com>
Co-authored-by: Jim Moffet <jim.moffet@gsa.gov>
  • Loading branch information
3 people authored Sep 5, 2024
1 parent 13b5d2f commit 6b53414
Show file tree
Hide file tree
Showing 49 changed files with 3,392 additions and 2,400 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/_validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ jobs:
- name: Install dependencies
run: pnpm install

- name: Install playwright
run: pnpm dlx playwright install --with-deps

# While most of the test suite is self-contained, the tests for the demo
# servers require a prod build of @atj/server.
- name: Build
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ packages/form-service
/e2e/playwright-report/
/e2e/blob-report/
/e2e/playwright/.cache/
__screenshots__/
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ This project uses [pnpm workspaces](https://pnpm.io/workspaces). To work with th
pnpm install
```

To run the complete test suite, with coverage metrics generated:
To install the browsers needed for the Storybook testing with `@vitest/browser`, you need to do a one-time install with `pnpm dlx install playwright --with-deps`. To run the complete test suite, with coverage metrics generated:

```bash
pnpm test
Expand Down
1 change: 1 addition & 0 deletions apps/server-doj/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
dist/
*.db
1 change: 1 addition & 0 deletions apps/server-kansas/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
dist/
*.db
21 changes: 21 additions & 0 deletions documents/adr/0012-rich-text-editor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 12. Rich Text Editor

Date: 2024-08-12

## Status

Approved

## Context

In user testing, there was an expressed interest to be able to apply a limited set of rich text formatting to imported forms and new builds from scratch.

## Decision

TipTap was selected to provide the rich text formatting features after evaluating several alternatives (QuillJS, Prose, TinyMCE).

Quill was originally installed, but there was a known issue in being able to execute a line break inside a bulleted list, which caused an issue with reformatting imported text when it was edited for the first time. TipTap was installed as an alternative.

## Consequences

TipTap is a popular package that works outside the React ecosystem, so it is portable if there is ever a need to move to a different JS framework. The plugin does operate on a freemium model with the subset of features being free with additional, more advanced features having a cost. However, based on the features we use at the time of this ADR, the free features are robust enough to current requirements as well as the most likely requirements in the future.
8 changes: 4 additions & 4 deletions e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,22 @@ First, make sure the script is executable:

```bash
# from the project root
chmod +x ./e2e/scripts/end-to-end.sh
chmod +x ./e2e/script/end-to-end.sh
```

Examples:

```bash
# builds the test container (also will run the tests)
./e2e/scripts/end-to-end.sh -f build_container -t test
./e2e/script/end-to-end.sh -f build_container -t test
```

```bash
# builds the serve container and start it
./e2e/scripts/end-to-end.sh -f build_container -t serve -f run_container
./e2e/script/end-to-end.sh -f build_container -t serve -f run_container
```

```bash
# Runs the default tasks `end_to_end` and `interaction`
./e2e/scripts/end-to-end.sh
./e2e/script/end-to-end.sh
```
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
"scripts": {
"build": "turbo run build --filter=!@atj/infra-cdktf",
"clean": "turbo run clean",
"dev": "turbo run dev --concurrency 18",
"format": "prettier --write \"packages/*/src/**/*.{js,jsx,ts,tsx,scss}\"",
"dev": "turbo run dev --concurrency 20",
"format": "prettier --write \"packages/*/src/**/*.{js,jsx,ts,tsx,scss,css}\"",
"lint": "turbo run lint",
"pages": "rm -rf node_modules && npm i -g pnpm turbo && pnpm i && pnpm build && ln -sf ./apps/spotlight/dist _site",
"test": "vitest run",
"test:ci": "vitest run # --coverage.enabled --coverage.provider=v8 --coverage.reporter=text --coverage.reporter=json-summary --coverage.reporter=json --coverage.reportOnFailure",
"test": "vitest run && pnpm --filter @atj/design test:ci",
"test:ci": "vitest run && pnpm --filter @atj/design test:ci # --coverage.enabled --coverage.provider=v8 --coverage.reporter=text --coverage.reporter=json-summary --coverage.reporter=json --coverage.reportOnFailure",
"test:infra": "turbo run --filter=infra-cdktf test",
"typecheck": "tsc --build",
"prepare": "husky"
Expand Down
5 changes: 5 additions & 0 deletions packages/common/src/locales/en/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ export const en = {
displayName: 'Paragraph',
errorTextMustContainChar: 'String must contain at least 1 character(s)',
},
richText: {
fieldLabel: 'Rich text',
displayName: 'Rich text',
errorTextMustContainChar: 'String must contain at least 1 character(s)',
},
radioGroup: {
...defaults,
displayName: 'Radio group label',
Expand Down
2 changes: 2 additions & 0 deletions packages/design/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ See relevant ADRs:

- [documents/adr/0007-initial-css-strategy](../../documents/adr/0007-initial-css-strategy.md)
- [documents/adr/0009-design-assets-workflow.md](../../documents/adr/0009-design-assets-workflow.md)

This package as a special watch task. If your dev server is running already (`pnpm dev`), you can open a separate terminal and run `pnpm test:watch` and any changes to the *.{ts,tsx} files in this package will run the test suite. If you'd like to run from the project root directory, you would run `pnpm --filter @atj/design test:watch`.
12 changes: 11 additions & 1 deletion packages/design/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
"dev:styles": "gulp watch",
"lint": "eslint --ext ts,tsx --report-unused-disable-directives --max-warnings 0 src",
"test:url": "test-storybook --url http://127.0.0.1:9009 --config-dir .storybook",
"test:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"pnpm build:storybook --quiet && pnpm http-server storybook-static --port 9009 --silent\" \"wait-on tcp:127.0.0.1:9009 && pnpm test:url --maxWorkers=2\""
"test:dev": "wait-on tcp:127.0.0.1:9009 && pnpm test:url --maxWorkers=2 # assumes dev server is running",
"test:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"pnpm build:storybook --quiet && pnpm http-server storybook-static --port 9009 --silent\" \"pnpm test:dev\"",
"test:watch": "pnpm onchange './**/*.{tsx,ts}' -- pnpm test:url"
},
"files": [
"dist/**/*"
Expand Down Expand Up @@ -45,17 +47,21 @@
"@typescript-eslint/parser": "^7.18.0",
"@uswds/compile": "1.1.0",
"@vitejs/plugin-react": "^4.3.1",
"@vitest/browser": "^2.0.5",
"concurrently": "^8.2.2",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.35.0",
"gulp": "^5.0.0",
"http-server": "^14.1.1",
"install": "^0.13.0",
"jsdom": "^24.1.1",
"onchange": "^7.1.0",
"playwright": "^1.46.1",
"prop-types": "^15.8.1",
"react-dom": "^18.3.1",
"vite": "^5.4.0",
"vite-plugin-dts": "^4.0.1",
"vitest": "^2.0.5",
"wait-on": "^7.2.0"
},
"dependencies": {
Expand All @@ -64,8 +70,12 @@
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@tiptap/core": "^2.6.2",
"@tiptap/react": "^2.6.2",
"@tiptap/starter-kit": "^2.6.2",
"@uswds/uswds": "^3.8.1",
"classnames": "^2.5.1",
"debounce": "^2.1.0",
"deep-equal": "^2.2.3",
"react": "^18.3.1",
"react-hook-form": "^7.52.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
.sideNavWrapper {
top: 6rem;
top: 6rem;
}

.sideNav a:not(.usa-button):not(.usa-current) {
color: #005ea2;
color: #005ea2;
}

.sideNav a:not(.usa-button):hover {
background-color: #d9e8f6;
background-color: #d9e8f6;
}
5 changes: 4 additions & 1 deletion packages/design/src/Form/components/PageSet/PageSet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ const PageSet: PatternComponent<PageSetProps> = props => {
})}
/>
</nav>
<div className="tablet:grid-col-9 tablet:padding-left-4 padding-left-0 padding-bottom-3 padding-top-3 tablet:border-left tablet:border-base-lighter contentWrapper">
<div
className="tablet:grid-col-9 tablet:padding-left-4 padding-left-0 padding-bottom-3 padding-top-3 tablet:border-left tablet:border-base-lighter contentWrapper"
aria-live="polite"
>
{props.children}
<ActionBar actions={props.actions} />
</div>
Expand Down
14 changes: 14 additions & 0 deletions packages/design/src/Form/components/RichText/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';

import { type RichTextProps } from '@atj/forms';
import { type PatternComponent } from '../../../Form/index.js';
import styles from './richTextStyles.module.css';

const FormSummary: PatternComponent<RichTextProps> = props => {
return (
<div className={`${styles.richTextEditorWrapper}`}>
<div dangerouslySetInnerHTML={{ __html: props.text }} />
</div>
);
};
export default FormSummary;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.richTextEditorWrapper li > p {
margin-top: 0;
margin-bottom: 0;
padding-top: 0;
padding-bottom: 0;
}
2 changes: 2 additions & 0 deletions packages/design/src/Form/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Page from './Page/index.js';
import PageSet from './PageSet/index.js';
import Paragraph from './Paragraph/index.js';
import RadioGroup from './RadioGroup/index.js';
import RichText from './RichText/index.js';
import Sequence from './Sequence/index.js';
import SubmissionConfirmation from './SubmissionConfirmation/index.js';
import TextInput from './TextInput/index.js';
Expand All @@ -22,6 +23,7 @@ export const defaultPatternComponents: ComponentForPattern = {
'page-set': PageSet as PatternComponent,
paragraph: Paragraph as PatternComponent,
'radio-group': RadioGroup as PatternComponent,
'rich-text': RichText as PatternComponent,
sequence: Sequence as PatternComponent,
'submission-confirmation': SubmissionConfirmation as PatternComponent,
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import type { Meta, StoryObj } from '@storybook/react';

import DocumentImporter from '.';
import { createTwoPatternTestForm } from '../../../test-form';
import DocumentImporter from './index.js';
import { createTwoPatternTestForm } from '../../../test-form.js';

const meta: Meta<typeof DocumentImporter> = {
title: 'FormManager/DocumentImporter',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @vitest-environment jsdom
*/
import { describeStories } from '../../../test-helper';
import meta, * as stories from './DocumentImporter.stories';
import { describeStories } from '../../../test-helper.js';
import meta, * as stories from './DocumentImporter.stories.js';

describeStories(meta, stories);
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import checkboxIcon from './images/checkbox-icon.svg';
import dateIcon from './images/date-icon.svg';
import dropDownIcon from './images/dropdown-icon.svg';
import dropDownOptionIcon from './images/dropdownoption-icon.svg';
import richTextIcon from './images/richtext-icon.svg';
import longanswerIcon from './images/longanswer-icon.svg';
import pageIcon from './images/page-icon.svg';
import shortanswerIcon from './images/shortanswer-icon.svg';
Expand All @@ -24,6 +25,7 @@ const icons: Record<string, string | any> = {
'date-icon.svg.svg': dateIcon,
'dropdown-icon.svg': dropDownIcon,
'dropdownoption-icon.svg': dropDownOptionIcon,
'richtext-icon.svg': richTextIcon,
'longanswer-icon.svg': longanswerIcon,
'page-icon.svg': pageIcon,
'shortanswer-icon.svg': shortanswerIcon,
Expand Down Expand Up @@ -89,12 +91,14 @@ const sidebarPatterns: DropdownPattern[] = [
['fieldset', defaultFormConfig.patterns['fieldset']],
['input', defaultFormConfig.patterns['input']],
['paragraph', defaultFormConfig.patterns['paragraph']],
['rich-text', defaultFormConfig.patterns['rich-text']],
['radio-group', defaultFormConfig.patterns['radio-group']],
] as const;
export const fieldsetPatterns: DropdownPattern[] = [
['form-summary', defaultFormConfig.patterns['form-summary']],
['input', defaultFormConfig.patterns['input']],
['paragraph', defaultFormConfig.patterns['paragraph']],
['rich-text', defaultFormConfig.patterns['rich-text']],
['radio-group', defaultFormConfig.patterns['radio-group']],
] as const;

Expand Down
20 changes: 13 additions & 7 deletions packages/design/src/FormManager/FormEdit/FormEdit.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,14 @@ export const FormEditAddPattern: StoryObj<typeof FormEdit> = {
await userEvent.click(canvas.getByText('Pattern 1'));
//await userEvent.selectOptions(select, 'Text input');

select.forEach(async element => {
await userEvent.click(element);
});
await Promise.all(
select.map(async element => {
return await userEvent.click(element);
})
);

const finalCount = (await canvas.findAllByRole('textbox')).length;
expect(finalCount).toBeGreaterThan(initialCount);
await expect(finalCount).toBeGreaterThan(initialCount);
},
};

Expand All @@ -82,9 +84,13 @@ const editFieldLabel = async (
await userEvent.type(input, updatedLabel);
//await userEvent.click(canvas.getByText('Add Element'));

select.forEach(async element => {
await userEvent.click(element);
});
await Promise.all(
select.map(async element => {
return await userEvent.click(element);
})
);

await userEvent.click(canvas.getByText(/save and close/i));

waitFor(
async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ const PageSetEdit: PatternEditComponent<PageSetProps> = ({ previewProps }) => {
})}
/>
</nav>
<div className="tablet:grid-col-9 tablet:padding-left-4 padding-left-0 padding-bottom-3 padding-top-3 tablet:border-left tablet:border-base-lighter contentWrapper">
<div
className="tablet:grid-col-9 tablet:padding-left-4 padding-left-0 padding-bottom-3 padding-top-3 tablet:border-left tablet:border-base-lighter contentWrapper"
aria-live="polite"
>
{previewProps.children}
<ActionBar actions={previewProps.actions} />
</div>
Expand Down
Loading

0 comments on commit 6b53414

Please sign in to comment.