Skip to content

Commit

Permalink
feat: integrate default value
Browse files Browse the repository at this point in the history
  • Loading branch information
sashuk committed Jun 27, 2023
1 parent b43658d commit f91c86d
Show file tree
Hide file tree
Showing 14 changed files with 249 additions and 9 deletions.
18 changes: 16 additions & 2 deletions packages/picasso-forms/src/RichTextEditor/RichTextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useForm } from 'react-final-form'
import type { FieldProps } from '../FieldWrapper'
import InputField from '../InputField'
import FieldLabel from '../FieldLabel'
import { useEnforceHighlightAutofill } from './hooks'

type OverriddenProps = {
defaultValue?: ASTType
Expand All @@ -21,7 +22,10 @@ export type Props = RichTextEditorProps &
type InternalProps = RichTextEditorProps & { value: string }

export const RichTextEditor = (props: Props) => {
const { onChange, defaultValue, label, titleCase, ...rest } = props
const { onChange, onFocus, defaultValue, label, titleCase, ...rest } = props

const { enforceHighlightAutofill, registerChangeOrFocus } =
useEnforceHighlightAutofill()
const [value, setValue] = useState('')
const {
mutators: { setHasMultilineCounter },
Expand All @@ -31,17 +35,26 @@ export const RichTextEditor = (props: Props) => {
// as an compatibility layer between final-form
const handleOnChange = useCallback(
(newVal: string) => {
registerChangeOrFocus()
setValue(newVal)
onChange?.(newVal)
},
[onChange, setValue]
[onChange, setValue, registerChangeOrFocus]
)

const handleOnFocus = useCallback(() => {
registerChangeOrFocus()

onFocus?.()
}, [onFocus, registerChangeOrFocus])

const hiddenInputId = `${props.id}-hidden-input`

return (
<InputField<InternalProps>
value={value}
onChange={handleOnChange}
onFocus={handleOnFocus}
label={
label ? (
<FieldLabel
Expand All @@ -59,6 +72,7 @@ export const RichTextEditor = (props: Props) => {
<PicassoRichTextEditor
defaultValue={defaultValue}
hiddenInputId={hiddenInputId}
highlight={enforceHighlightAutofill ? 'autofill' : undefined}
{...inputProps}
/>
)}
Expand Down
1 change: 1 addition & 0 deletions packages/picasso-forms/src/RichTextEditor/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useEnforceHighlightAutofill } from './use-enforce-highlight-autofill'
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react'
import { render, fireEvent } from '@toptal/picasso/test-utils'

import { useEnforceHighlightAutofill } from './use-enforce-highlight-autofill'
import { useFormConfig } from '../../FormConfig'

jest.mock('../../FormConfig', () => ({
useFormConfig: jest.fn(),
}))

const mockedUseFormConfig = useFormConfig as jest.MockedFunction<
typeof useFormConfig
>

const TestComponent = () => {
const { enforceHighlightAutofill, registerChangeOrFocus } =
useEnforceHighlightAutofill()

return (
<>
<button onClick={registerChangeOrFocus}>trigger</button>
<div data-testid='enforce-highlight-autofill-status'>
{enforceHighlightAutofill ? 'true' : 'false'}
</div>
</>
)
}

describe('useEnforceHighlightAutofill', () => {
describe('when form does not highlight autofilled fields', () => {
it('does not enforce highlighting', () => {
mockedUseFormConfig.mockReturnValue({ highlightAutofill: false })
const { getByText, getByTestId } = render(<TestComponent />)
const button = getByText('trigger')
const autofillStatusContainer = getByTestId(
'enforce-highlight-autofill-status'
)

fireEvent.click(button)

// After first update
expect(autofillStatusContainer).toHaveTextContent('false')
})
})

describe('when form highlights autofilled fields', () => {
it('highlighting is enforced after first update and not enforced after following updates', async () => {
mockedUseFormConfig.mockReturnValue({ highlightAutofill: true })
const { getByText, getByTestId } = render(<TestComponent />)

const button = getByText('trigger')
const autofillStatusContainer = getByTestId(
'enforce-highlight-autofill-status'
)

fireEvent.click(button)

// After first update
expect(autofillStatusContainer).toHaveTextContent('true')

fireEvent.click(button)

// After second update
expect(autofillStatusContainer).toHaveTextContent('false')
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useCallback, useMemo, useState } from 'react'

import { useFormConfig } from '../../FormConfig'

const TRACKED_EVENTS_LIMIT = 3

export const useEnforceHighlightAutofill = () => {
const { highlightAutofill } = useFormConfig()
const [timesChangeOrFocusTriggered, setTimesChangeOrFocusTriggered] =
useState(0)

const enforceHighlightAutofill = useMemo(() => {
if (!highlightAutofill) {
return false
}

return timesChangeOrFocusTriggered < 2
}, [highlightAutofill, timesChangeOrFocusTriggered])

const registerChangeOrFocus = useCallback(() => {
if (!highlightAutofill) {
return
}

if (timesChangeOrFocusTriggered < TRACKED_EVENTS_LIMIT) {
setTimesChangeOrFocusTriggered(timesChangeOrFocusTriggered + 1)
}
}, [timesChangeOrFocusTriggered])

return {
enforceHighlightAutofill,
registerChangeOrFocus,
}
}
3 changes: 2 additions & 1 deletion packages/picasso/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
"hast-util-from-dom": "^3.0.0",
"hast-util-sanitize": "^3.0.2",
"hast-util-to-html": "^7.1.3",
"lexical": "^0.9.1",
"hast-util-to-dom": "^3.1.1",
"lexical": "^0.9.2",
"quill": "^1.3.7",
"quill-emoji": "^0.2.0",
"quill-paste-smart": "^1.4.9",
Expand Down
19 changes: 15 additions & 4 deletions packages/picasso/src/LexicalEditor/LexicalEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@ import { makeStyles } from '@material-ui/core/styles'
import { LexicalComposer } from '@lexical/react/LexicalComposer'
import type { InitialConfigType } from '@lexical/react/LexicalComposer'
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'
import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin'
import { ContentEditable } from '@lexical/react/LexicalContentEditable'
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary'
import { HeadingNode } from '@lexical/rich-text'
import { $generateHtmlFromNodes } from '@lexical/html'
import { ListItemNode, ListNode } from '@lexical/list'
import { $isRootTextContentEmpty } from '@lexical/text'
import type { LexicalEditor as LexicalEditorType } from 'lexical'
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'

import { createLexicalTheme } from './utils'
import { TriggerInitialOnChangePlugin } from './plugins'
import { createLexicalTheme, setEditorValue } from './utils'
import noop from '../utils/noop'
import Container from '../Container'
import Typography from '../Typography'
Expand All @@ -25,6 +27,7 @@ import ToolbarPlugin from '../LexicalEditorToolbarPlugin'
import LexicalTextLengthPlugin from '../LexicalTextLengthPlugin'
import LexicalListPlugin from '../LexicalListPlugin'
import LexicalHeadingsReplacementPlugin from '../LexicalHeadingsReplacementPlugin'
import type { ASTType } from '../RichText'

const useStyles = makeStyles<Theme>(styles, {
name: 'LexicalEditor',
Expand All @@ -38,7 +41,7 @@ export type Props = BaseProps & {
/** Indicates that an element is to be focused on page load */
autoFocus?: boolean
/** Default value in [HAST](https://github.com/syntax-tree/hast) format */
// defaultValue?: ASTType
defaultValue?: ASTType
/**
* This Boolean attribute indicates that the user cannot interact with the control.
*/
Expand Down Expand Up @@ -85,7 +88,7 @@ const LexicalEditor = forwardRef<HTMLDivElement, Props>(function LexicalEditor(
const {
// plugins,
autoFocus = false,
// defaultValue,
defaultValue,
disabled = false,
id,
onChange = noop,
Expand Down Expand Up @@ -130,6 +133,11 @@ const LexicalEditor = forwardRef<HTMLDivElement, Props>(function LexicalEditor(

const editorConfig: InitialConfigType = useMemo(
() => ({
editorState: (editor: LexicalEditorType) => {
if (defaultValue) {
setEditorValue(editor, defaultValue)
}
},
theme,
onError(error: Error) {
throw error
Expand Down Expand Up @@ -171,6 +179,9 @@ const LexicalEditor = forwardRef<HTMLDivElement, Props>(function LexicalEditor(
// remount Toolbar when disabled
key={`${disabled || !isFocused}`}
/>
{defaultValue ? (
<TriggerInitialOnChangePlugin onChange={handleChange} />
) : null}
<OnChangePlugin ignoreSelectionChange onChange={handleChange} />
{autoFocus && <AutoFocusPlugin />}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { useIsomorphicLayoutEffect } from '@toptal/picasso-shared'
import type { EditorState, LexicalEditor } from 'lexical'

const TriggerInitialOnChangePlugin = ({
onChange,
}: {
onChange: (
editorState: EditorState,
editor: LexicalEditor,
tags: Set<string>
) => void
}) => {
const [editor] = useLexicalComposerContext()

useIsomorphicLayoutEffect(() => {
if (onChange) {
return editor.registerUpdateListener(
({ editorState, prevEditorState, tags }) => {
if (prevEditorState.isEmpty()) {
onChange(editorState, editor, tags)
}
}
)
}
}, [editor, onChange])

return null
}

export default TriggerInitialOnChangePlugin
1 change: 1 addition & 0 deletions packages/picasso/src/LexicalEditor/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as TriggerInitialOnChangePlugin } from './TriggerInitialOnChangePlugin'
18 changes: 18 additions & 0 deletions packages/picasso/src/LexicalEditor/utils/getDomValue.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { ASTType } from '../../RichText'
import { getDomValue } from './getDomValue'

describe('getDomValue', () => {
it('returns DOM structure from AST', () => {
const inputValue: ASTType = {
type: 'root',
children: [{ type: 'text', value: 'Example of default text' }],
}

const result = getDomValue(inputValue)
const serializer = new XMLSerializer()

expect(serializer.serializeToString(result)).toBe(
'<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>Example of default text</body></html>'
)
})
})
10 changes: 10 additions & 0 deletions packages/picasso/src/LexicalEditor/utils/getDomValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { HastNode } from 'hast-util-to-dom/lib'
import toHtml from 'hast-util-to-html'

import type { ASTType } from '../../RichText'

export const getDomValue = (value: ASTType) => {
const parser = new DOMParser()

return parser.parseFromString(toHtml(value as HastNode), 'text/html')
}
2 changes: 2 additions & 0 deletions packages/picasso/src/LexicalEditor/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export { synchronizeToolbarState } from './synchronizeToolbarState'
export { toolbarStateReducer } from './toolbarState'
export { getLexicalNode } from './getLexicalNode'
export { createLexicalTheme } from './createLexicalTheme'
export { getDomValue } from './getDomValue'
export { setEditorValue } from './setEditorValue'
36 changes: 36 additions & 0 deletions packages/picasso/src/LexicalEditor/utils/setEditorValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { LexicalEditor as LexicalEditorType } from 'lexical'
import {
$createParagraphNode,
$isLineBreakNode,
$getRoot,
$isDecoratorNode,
$isElementNode,
} from 'lexical'
import { $generateNodesFromDOM } from '@lexical/html'

import type { ASTType } from '../../RichText'
import { getDomValue } from './getDomValue'

export const setEditorValue = (editor: LexicalEditorType, value: ASTType) => {
const domValue = getDomValue(value)

editor.update(() => {
const root = $getRoot()
const lexicalValueNodes = $generateNodesFromDOM(editor, domValue)

lexicalValueNodes.forEach(node => {
if ($isElementNode(node) || $isDecoratorNode(node)) {
root.append(node)
} else if ($isLineBreakNode(node)) {
const paragraphNode = $createParagraphNode()

root.append(paragraphNode)
} else {
const paragraphNode = $createParagraphNode()

paragraphNode.append(node)
root.append(paragraphNode)
}
})
})
}
3 changes: 2 additions & 1 deletion packages/picasso/src/RichTextEditor/RichTextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const RichTextEditor = forwardRef<HTMLDivElement, Props>(
// plugins,
autoFocus = false,
className,
// defaultValue,
defaultValue,
disabled,
id,
onChange = noop,
Expand Down Expand Up @@ -188,6 +188,7 @@ export const RichTextEditor = forwardRef<HTMLDivElement, Props>(
testIds={testIds}
disabled={disabled}
autoFocus={autoFocus}
defaultValue={defaultValue}
/>
{hiddenInputId && (
// Native `for` attribute on label does not work for div target
Expand Down
15 changes: 14 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12921,6 +12921,14 @@ hast-util-sanitize@^3.0.2:
dependencies:
xtend "^4.0.0"

hast-util-to-dom@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/hast-util-to-dom/-/hast-util-to-dom-3.1.1.tgz#7cd9f925b0cff8ec2c95474ef896b1865fcefd62"
integrity sha512-hDiYqOapuWzLPDMADCD5z6re/07OQOpQuT2YO5hxPjaxWTtgcbjqCjlv4KtyMuEQiW4wKTIPoK+japvbZ5zqxg==
dependencies:
property-information "^6.0.0"
web-namespaces "^2.0.0"

hast-util-to-html@^7.1.3:
version "7.1.3"
resolved "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-7.1.3.tgz"
Expand Down Expand Up @@ -15421,7 +15429,7 @@ levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"

lexical@^0.9.1:
lexical@^0.9.2:
version "0.9.2"
resolved "https://registry.yarnpkg.com/lexical/-/lexical-0.9.2.tgz#b4e910322270a7ad06fc7e625066ffa43fa1d4fb"
integrity sha512-8S3fNd053/QD5DJ4wePlAY4Rc5k2LFvg2lBcmKLpPUZ+1/1tr35Kdyr5wyFWJKy1rbEUqjP74Hvly/lcXSipag==
Expand Down Expand Up @@ -23604,6 +23612,11 @@ web-namespaces@^1.0.0:
resolved "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.4.tgz"
integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==

web-namespaces@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692"
integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==

webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz"
Expand Down

0 comments on commit f91c86d

Please sign in to comment.