mirror of
https://github.com/facebook/lexical.git
synced 2025-08-26 10:40:47 +08:00
Add UI components to Lexical Playground (#1211)
* Add UI components to Lexical Playground * Add autofocus plugin * Fix test
This commit is contained in:

committed by
acywatson

parent
1b6e5d254e
commit
fa107c93fb
@ -17,6 +17,10 @@ describe('Selection', () => {
|
|||||||
return document.activeElement === editorElement;
|
return document.activeElement === editorElement;
|
||||||
});
|
});
|
||||||
const {page} = e2e;
|
const {page} = e2e;
|
||||||
|
await evaluate(page, () => {
|
||||||
|
const editorElement = document.querySelector('div[contenteditable="true"]');
|
||||||
|
return editorElement.blur();
|
||||||
|
});
|
||||||
expect(await editorHasFocus()).toEqual(false);
|
expect(await editorHasFocus()).toEqual(false);
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
expect(await editorHasFocus()).toEqual(false);
|
expect(await editorHasFocus()).toEqual(false);
|
||||||
|
@ -39,6 +39,7 @@ import ContentEditable from './ui/ContentEditable';
|
|||||||
import AutoLinkPlugin from './plugins/AutoLinkPlugin';
|
import AutoLinkPlugin from './plugins/AutoLinkPlugin';
|
||||||
import PollPlugin from './plugins/PollPlugin';
|
import PollPlugin from './plugins/PollPlugin';
|
||||||
import {useSettings} from './context/SettingsContext';
|
import {useSettings} from './context/SettingsContext';
|
||||||
|
import AutoFocusPlugin from './plugins/AutoFocusPlugin';
|
||||||
|
|
||||||
const skipCollaborationInit =
|
const skipCollaborationInit =
|
||||||
window.parent != null && window.parent.frames.right === window;
|
window.parent != null && window.parent.frames.right === window;
|
||||||
@ -69,6 +70,7 @@ export default function Editor(): React$Node {
|
|||||||
className={`editor-container ${showTreeView ? 'tree-view' : ''} ${
|
className={`editor-container ${showTreeView ? 'tree-view' : ''} ${
|
||||||
!isRichText ? 'plain-text' : ''
|
!isRichText ? 'plain-text' : ''
|
||||||
}`}>
|
}`}>
|
||||||
|
<AutoFocusPlugin />
|
||||||
<StickyPlugin />
|
<StickyPlugin />
|
||||||
<MentionsPlugin />
|
<MentionsPlugin />
|
||||||
<EmojisPlugin />
|
<EmojisPlugin />
|
||||||
|
48
packages/lexical-playground/src/hooks/useModal.js
Normal file
48
packages/lexical-playground/src/hooks/useModal.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @flow strict
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Modal from '../ui/Modal';
|
||||||
|
|
||||||
|
import {useCallback, useMemo, useState} from 'react';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
export default function useModal(): [
|
||||||
|
React$Node,
|
||||||
|
(string, (() => void) => React$Node) => void,
|
||||||
|
] {
|
||||||
|
const [modalContent, setModalContent] = useState<null | {
|
||||||
|
content: React$Node,
|
||||||
|
title: string,
|
||||||
|
}>(null);
|
||||||
|
|
||||||
|
const onClose = useCallback(() => {
|
||||||
|
setModalContent(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const modal = useMemo(() => {
|
||||||
|
if (modalContent === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const {title, content} = modalContent;
|
||||||
|
return (
|
||||||
|
<Modal onClose={onClose} title={title}>
|
||||||
|
{content}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}, [modalContent, onClose]);
|
||||||
|
|
||||||
|
const showModal = useCallback(
|
||||||
|
(title, getContent: (() => void) => React$Node) => {
|
||||||
|
setModalContent({title, content: getContent(onClose)});
|
||||||
|
},
|
||||||
|
[onClose],
|
||||||
|
);
|
||||||
|
|
||||||
|
return [modal, showModal];
|
||||||
|
}
|
@ -35,7 +35,7 @@ header h1 {
|
|||||||
.editor-shell {
|
.editor-shell {
|
||||||
margin: 20px auto 20px auto;
|
margin: 20px auto 20px auto;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
width: 940px;
|
width: 960px;
|
||||||
color: #000;
|
color: #000;
|
||||||
position: relative;
|
position: relative;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
@ -62,7 +62,7 @@ header h1 {
|
|||||||
|
|
||||||
.test-recorder-output {
|
.test-recorder-output {
|
||||||
margin: 20px auto 20px auto;
|
margin: 20px auto 20px auto;
|
||||||
width: 940px;
|
width: 960px;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
|
22
packages/lexical-playground/src/plugins/AutoFocusPlugin.js
Normal file
22
packages/lexical-playground/src/plugins/AutoFocusPlugin.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @flow strict
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||||
|
|
||||||
|
import {useEffect} from 'react';
|
||||||
|
|
||||||
|
export default function AutoFocusPlugin(): null {
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
editor.focus();
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
@ -40,6 +40,19 @@ import {getCodeLanguages, getDefaultCodeLanguage} from './CodeHighlightPlugin';
|
|||||||
import {$getNearestNodeOfType} from '@lexical/helpers/nodes';
|
import {$getNearestNodeOfType} from '@lexical/helpers/nodes';
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
import {createPortal} from 'react-dom';
|
import {createPortal} from 'react-dom';
|
||||||
|
import useModal from '../hooks/useModal';
|
||||||
|
import Input from '../ui/Input';
|
||||||
|
import Button from '../ui/Button';
|
||||||
|
import stylex from 'stylex';
|
||||||
|
|
||||||
|
const styles = stylex.create({
|
||||||
|
dialogActions: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'right',
|
||||||
|
marginTop: 20,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const LowPriority: CommandListenerLowPriority = 1;
|
const LowPriority: CommandListenerLowPriority = 1;
|
||||||
|
|
||||||
@ -221,6 +234,32 @@ function FloatingLinkEditor({editor}: {editor: LexicalEditor}): React$Node {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function InsertTableDialog({
|
||||||
|
activeEditor,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
activeEditor: LexicalEditor,
|
||||||
|
onClose: () => void,
|
||||||
|
}): React$Node {
|
||||||
|
const [rows, setRows] = useState('3');
|
||||||
|
const [columns, setColumns] = useState('3');
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
activeEditor.execCommand('insertTable', {rows, columns});
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Input label="No of rows" onChange={setRows} value={rows} />
|
||||||
|
<Input label="No of columns" onChange={setColumns} value={columns} />
|
||||||
|
<div className={stylex(styles.dialogActions)}>
|
||||||
|
<Button onClick={onClick}>Confirm</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function BlockOptionsDropdownList({
|
function BlockOptionsDropdownList({
|
||||||
editor,
|
editor,
|
||||||
blockType,
|
blockType,
|
||||||
@ -438,6 +477,7 @@ export default function ToolbarPlugin(): React$Node {
|
|||||||
const [isCode, setIsCode] = useState(false);
|
const [isCode, setIsCode] = useState(false);
|
||||||
const [canUndo, setCanUndo] = useState(false);
|
const [canUndo, setCanUndo] = useState(false);
|
||||||
const [canRedo, setCanRedo] = useState(false);
|
const [canRedo, setCanRedo] = useState(false);
|
||||||
|
const [modal, showModal] = useModal();
|
||||||
const [showBlockOptionsDropDown, setShowBlockOptionsDropDown] =
|
const [showBlockOptionsDropDown, setShowBlockOptionsDropDown] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [codeLanguage, setCodeLanguage] = useState<string>('');
|
const [codeLanguage, setCodeLanguage] = useState<string>('');
|
||||||
@ -740,7 +780,12 @@ export default function ToolbarPlugin(): React$Node {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
activeEditor.execCommand('insertTable');
|
showModal('Insert Table', (onClose) => (
|
||||||
|
<InsertTableDialog
|
||||||
|
activeEditor={activeEditor}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
));
|
||||||
}}
|
}}
|
||||||
className="toolbar-item"
|
className="toolbar-item"
|
||||||
aria-label="Insert Table">
|
aria-label="Insert Table">
|
||||||
@ -807,6 +852,7 @@ export default function ToolbarPlugin(): React$Node {
|
|||||||
aria-label="Indent">
|
aria-label="Indent">
|
||||||
<i className="format indent" />
|
<i className="format indent" />
|
||||||
</button>
|
</button>
|
||||||
|
{modal}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
41
packages/lexical-playground/src/ui/Button.js
Normal file
41
packages/lexical-playground/src/ui/Button.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @flow strict
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import stylex from 'stylex';
|
||||||
|
|
||||||
|
const styles = stylex.create({
|
||||||
|
root: {
|
||||||
|
paddingTop: 10,
|
||||||
|
paddingBottom: 10,
|
||||||
|
paddingLeft: 15,
|
||||||
|
paddingRight: 15,
|
||||||
|
border: 0,
|
||||||
|
backgroundColor: '#eee',
|
||||||
|
borderRadius: 5,
|
||||||
|
cursor: 'pointer',
|
||||||
|
':hover': {
|
||||||
|
backgroundColor: '#ddd',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function Button({
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
children: React$Node,
|
||||||
|
onClick: () => void,
|
||||||
|
}): React$Node {
|
||||||
|
return (
|
||||||
|
<button className={stylex(styles.root)} onClick={onClick}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
60
packages/lexical-playground/src/ui/Input.js
Normal file
60
packages/lexical-playground/src/ui/Input.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @flow strict
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import stylex from 'stylex';
|
||||||
|
|
||||||
|
const styles = stylex.create({
|
||||||
|
wrapper: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
display: 'flex',
|
||||||
|
flex: 1,
|
||||||
|
color: '#666',
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
display: 'flex',
|
||||||
|
flex: 2,
|
||||||
|
border: '1px solid #999',
|
||||||
|
paddingTop: 7,
|
||||||
|
paddingBottom: 7,
|
||||||
|
paddingLeft: 10,
|
||||||
|
paddingRight: 10,
|
||||||
|
fontSize: 16,
|
||||||
|
borderRadius: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function Input({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string,
|
||||||
|
onChange: (string) => void,
|
||||||
|
value: string,
|
||||||
|
}): React$Node {
|
||||||
|
return (
|
||||||
|
<div className={stylex(styles.wrapper)}>
|
||||||
|
<label className={stylex(styles.label)}>{label}</label>
|
||||||
|
<input
|
||||||
|
className={stylex(styles.input)}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
140
packages/lexical-playground/src/ui/Modal.js
Normal file
140
packages/lexical-playground/src/ui/Modal.js
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @flow strict
|
||||||
|
*/
|
||||||
|
|
||||||
|
// $FlowFixMe
|
||||||
|
import {createPortal} from 'react-dom';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import {useEffect, useRef} from 'react';
|
||||||
|
import stylex from 'stylex';
|
||||||
|
|
||||||
|
const styles = stylex.create({
|
||||||
|
overlay: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
position: 'fixed',
|
||||||
|
flexDirection: 'column',
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
backgroundColor: 'rgba(40, 40, 40, 0.6)',
|
||||||
|
flexGrow: 0,
|
||||||
|
flexShrink: 1,
|
||||||
|
zIndex: 100,
|
||||||
|
},
|
||||||
|
modal: {
|
||||||
|
padding: 20,
|
||||||
|
minHeight: 100,
|
||||||
|
minWidth: 400,
|
||||||
|
display: 'flex',
|
||||||
|
flexGrow: 0,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
maxHeight: 300,
|
||||||
|
flexDirection: 'column',
|
||||||
|
position: 'relative',
|
||||||
|
boxShadow: '0 0 20px 0 #444',
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
color: '#444',
|
||||||
|
margin: 0,
|
||||||
|
paddingBottom: 10,
|
||||||
|
borderBottom: '1px solid #ccc',
|
||||||
|
},
|
||||||
|
closeButton: {
|
||||||
|
border: 0,
|
||||||
|
position: 'absolute',
|
||||||
|
right: 20,
|
||||||
|
borderRadius: 20,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
textAlign: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
backgroundColor: '#eee',
|
||||||
|
':hover': {
|
||||||
|
backgroundColor: '#ddd',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
paddingTop: 20,
|
||||||
|
paddingBottom: 20,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function PortalImpl({
|
||||||
|
onClose,
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
}: {
|
||||||
|
onClose: () => void,
|
||||||
|
children: React$Node,
|
||||||
|
title: string,
|
||||||
|
}) {
|
||||||
|
const modalRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (modalRef.current !== null) {
|
||||||
|
modalRef.current.focus();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (event) => {
|
||||||
|
if (event.keyCode === 27) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handler);
|
||||||
|
};
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={stylex(styles.overlay)} onClick={() => onClose()}>
|
||||||
|
<div
|
||||||
|
className={stylex(styles.modal)}
|
||||||
|
tabIndex={-1}
|
||||||
|
ref={modalRef}
|
||||||
|
onClick={(e) => e.stopPropagation()}>
|
||||||
|
<h2 className={stylex(styles.title)}>{title}</h2>
|
||||||
|
<button
|
||||||
|
className={stylex(styles.closeButton)}
|
||||||
|
aria-label="Close modal"
|
||||||
|
onClick={() => onClose()}>
|
||||||
|
X
|
||||||
|
</button>
|
||||||
|
<div className={stylex(styles.content)}>{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Modal({
|
||||||
|
onClose,
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
}: {
|
||||||
|
onClose: () => void,
|
||||||
|
children: React$Node,
|
||||||
|
title: string,
|
||||||
|
}): React$Node {
|
||||||
|
return createPortal(
|
||||||
|
<PortalImpl onClose={onClose} title={title}>
|
||||||
|
{children}
|
||||||
|
</PortalImpl>,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
}
|
@ -26,8 +26,9 @@ export default function TablePlugin(): React$Node {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const removeCommandListener = editor.addListener(
|
const removeCommandListener = editor.addListener(
|
||||||
'command',
|
'command',
|
||||||
(type) => {
|
(type, payload) => {
|
||||||
if (type === 'insertTable') {
|
if (type === 'insertTable') {
|
||||||
|
const {columns, rows} = payload;
|
||||||
$log('handleAddTable');
|
$log('handleAddTable');
|
||||||
const selection = $getSelection();
|
const selection = $getSelection();
|
||||||
if (selection === null) {
|
if (selection === null) {
|
||||||
@ -37,7 +38,7 @@ export default function TablePlugin(): React$Node {
|
|||||||
|
|
||||||
if (focusNode !== null) {
|
if (focusNode !== null) {
|
||||||
const topLevelNode = focusNode.getTopLevelElementOrThrow();
|
const topLevelNode = focusNode.getTopLevelElementOrThrow();
|
||||||
const tableNode = $createTableNodeWithDimensions(3, 3);
|
const tableNode = $createTableNodeWithDimensions(rows, columns);
|
||||||
topLevelNode.insertAfter(tableNode);
|
topLevelNode.insertAfter(tableNode);
|
||||||
tableNode.insertAfter($createParagraphNode());
|
tableNode.insertAfter($createParagraphNode());
|
||||||
const firstCell = tableNode
|
const firstCell = tableNode
|
||||||
|
Reference in New Issue
Block a user