Add ESC btn to toolbar to quickly exit formatting (#1283)

* Add ESC btn to toolbar to quickly exit formatting

* add horizontal scroll to toolbar item

* make editor toolbar usable in touch device

* fix editor hotkeys not working in window

* remove unused import
This commit is contained in:
Ajay Bura 2023-06-16 11:09:09 +10:00 committed by GitHub
parent 2883b4c35b
commit bc5e7445d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 210 additions and 108 deletions

View file

@ -43,6 +43,7 @@ export const EditorPlaceholder = style([
{ {
position: 'absolute', position: 'absolute',
zIndex: 1, zIndex: 1,
width: '100%',
opacity: config.opacity.Placeholder, opacity: config.opacity.Placeholder,
pointerEvents: 'none', pointerEvents: 'none',
userSelect: 'none', userSelect: 'none',
@ -55,9 +56,10 @@ export const EditorPlaceholder = style([
}, },
]); ]);
export const EditorToolbar = style([ export const EditorToolbarBase = style({
DefaultReset, padding: `0 ${config.borderWidth.B300}`,
{ });
padding: config.space.S100,
}, export const EditorToolbar = style({
]); padding: config.space.S100,
});

View file

@ -104,7 +104,13 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { style, ...props } = attributes; const { style, ...props } = attributes;
return ( return (
<Text as="span" {...props} className={css.EditorPlaceholder} contentEditable={false}> <Text
as="span"
{...props}
className={css.EditorPlaceholder}
contentEditable={false}
truncate
>
{children} {children}
</Text> </Text>
); );

View file

@ -10,6 +10,7 @@ import {
Line, Line,
Menu, Menu,
PopOut, PopOut,
Scroll,
Text, Text,
Tooltip, Tooltip,
TooltipProvider, TooltipProvider,
@ -17,7 +18,14 @@ import {
} from 'folds'; } from 'folds';
import React, { ReactNode, useState } from 'react'; import React, { ReactNode, useState } from 'react';
import { ReactEditor, useSlate } from 'slate-react'; import { ReactEditor, useSlate } from 'slate-react';
import { isBlockActive, isMarkActive, toggleBlock, toggleMark } from './common'; import {
isAnyMarkActive,
isBlockActive,
isMarkActive,
removeAllMark,
toggleBlock,
toggleMark,
} from './common';
import * as css from './Editor.css'; import * as css from './Editor.css';
import { BlockType, MarkType } from './Elements'; import { BlockType, MarkType } from './Elements';
import { HeadingLevel } from './slate'; import { HeadingLevel } from './slate';
@ -44,6 +52,11 @@ function BtnTooltip({ text, shortCode }: { text: string; shortCode?: string }) {
type MarkButtonProps = { format: MarkType; icon: IconSrc; tooltip: ReactNode }; type MarkButtonProps = { format: MarkType; icon: IconSrc; tooltip: ReactNode };
export function MarkButton({ format, icon, tooltip }: MarkButtonProps) { export function MarkButton({ format, icon, tooltip }: MarkButtonProps) {
const editor = useSlate(); const editor = useSlate();
const disableInline = isBlockActive(editor, BlockType.CodeBlock);
if (disableInline) {
removeAllMark(editor);
}
const handleClick = () => { const handleClick = () => {
toggleMark(editor, format); toggleMark(editor, format);
@ -58,10 +71,11 @@ export function MarkButton({ format, icon, tooltip }: MarkButtonProps) {
variant="SurfaceVariant" variant="SurfaceVariant"
onClick={handleClick} onClick={handleClick}
aria-pressed={isMarkActive(editor, format)} aria-pressed={isMarkActive(editor, format)}
size="300" size="400"
radii="300" radii="300"
disabled={disableInline}
> >
<Icon size="50" src={icon} /> <Icon size="200" src={icon} />
</IconButton> </IconButton>
)} )}
</TooltipProvider> </TooltipProvider>
@ -89,10 +103,10 @@ export function BlockButton({ format, icon, tooltip }: BlockButtonProps) {
variant="SurfaceVariant" variant="SurfaceVariant"
onClick={handleClick} onClick={handleClick}
aria-pressed={isBlockActive(editor, format)} aria-pressed={isBlockActive(editor, format)}
size="300" size="400"
radii="300" radii="300"
> >
<Icon size="50" src={icon} /> <Icon size="200" src={icon} />
</IconButton> </IconButton>
)} )}
</TooltipProvider> </TooltipProvider>
@ -115,6 +129,7 @@ export function HeadingBlockButton() {
return ( return (
<PopOut <PopOut
open={open} open={open}
offset={5}
align="Start" align="Start"
position="Top" position="Top"
content={ content={
@ -130,14 +145,14 @@ export function HeadingBlockButton() {
> >
<Menu style={{ padding: config.space.S100 }}> <Menu style={{ padding: config.space.S100 }}>
<Box gap="100"> <Box gap="100">
<IconButton onClick={() => handleMenuSelect(1)} size="300" radii="300"> <IconButton onClick={() => handleMenuSelect(1)} size="400" radii="300">
<Icon size="100" src={Icons.Heading1} /> <Icon size="200" src={Icons.Heading1} />
</IconButton> </IconButton>
<IconButton onClick={() => handleMenuSelect(2)} size="300" radii="300"> <IconButton onClick={() => handleMenuSelect(2)} size="400" radii="300">
<Icon size="100" src={Icons.Heading2} /> <Icon size="200" src={Icons.Heading2} />
</IconButton> </IconButton>
<IconButton onClick={() => handleMenuSelect(3)} size="300" radii="300"> <IconButton onClick={() => handleMenuSelect(3)} size="400" radii="300">
<Icon size="100" src={Icons.Heading3} /> <Icon size="200" src={Icons.Heading3} />
</IconButton> </IconButton>
</Box> </Box>
</Menu> </Menu>
@ -151,97 +166,143 @@ export function HeadingBlockButton() {
variant="SurfaceVariant" variant="SurfaceVariant"
onClick={() => (isActive ? toggleBlock(editor, BlockType.Heading) : setOpen(!open))} onClick={() => (isActive ? toggleBlock(editor, BlockType.Heading) : setOpen(!open))}
aria-pressed={isActive} aria-pressed={isActive}
size="300" size="400"
radii="300" radii="300"
> >
<Icon size="50" src={Icons[`Heading${level}`]} /> <Icon size="200" src={Icons[`Heading${level}`]} />
<Icon size="50" src={isActive ? Icons.Cross : Icons.ChevronBottom} /> <Icon size="200" src={isActive ? Icons.Cross : Icons.ChevronBottom} />
</IconButton> </IconButton>
)} )}
</PopOut> </PopOut>
); );
} }
export function Toolbar() { type ExitFormattingProps = { tooltip: ReactNode };
export function ExitFormatting({ tooltip }: ExitFormattingProps) {
const editor = useSlate(); const editor = useSlate();
const allowInline = !isBlockActive(editor, BlockType.CodeBlock);
const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl'; const handleClick = () => {
if (isAnyMarkActive(editor)) {
removeAllMark(editor);
} else if (!isBlockActive(editor, BlockType.Paragraph)) {
toggleBlock(editor, BlockType.Paragraph);
}
ReactEditor.focus(editor);
};
return ( return (
<Box className={css.EditorToolbar} alignItems="Center" gap="300"> <TooltipProvider tooltip={tooltip} delay={500}>
<Box gap="100"> {(triggerRef) => (
<HeadingBlockButton /> <IconButton
<BlockButton ref={triggerRef}
format={BlockType.OrderedList} variant="SurfaceVariant"
icon={Icons.OrderList} onClick={handleClick}
tooltip={ size="400"
<BtnTooltip text="Ordered List" shortCode={`${modKey} + ${KeySymbol.Shift} + 0`} /> radii="300"
} >
/> <Text size="B400">{`Exit ${KeySymbol.Hyper}`}</Text>
<BlockButton </IconButton>
format={BlockType.UnorderedList} )}
icon={Icons.UnorderList} </TooltipProvider>
tooltip={ );
<BtnTooltip text="Unordered List" shortCode={`${modKey} + ${KeySymbol.Shift} + 8`} /> }
}
/> export function Toolbar() {
<BlockButton const editor = useSlate();
format={BlockType.BlockQuote} const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl';
icon={Icons.BlockQuote}
tooltip={ const canEscape = isAnyMarkActive(editor) || !isBlockActive(editor, BlockType.Paragraph);
<BtnTooltip text="Block Quote" shortCode={`${modKey} + ${KeySymbol.Shift} + '`} />
} return (
/> <Box className={css.EditorToolbarBase}>
<BlockButton <Scroll direction="Horizontal" size="0">
format={BlockType.CodeBlock} <Box className={css.EditorToolbar} alignItems="Center" gap="300">
icon={Icons.BlockCode} <>
tooltip={ <Box shrink="No" gap="100">
<BtnTooltip text="Block Code" shortCode={`${modKey} + ${KeySymbol.Shift} + ;`} /> <MarkButton
} format={MarkType.Bold}
/> icon={Icons.Bold}
</Box> tooltip={<BtnTooltip text="Bold" shortCode={`${modKey} + B`} />}
{allowInline && ( />
<> <MarkButton
<Line variant="SurfaceVariant" direction="Vertical" style={{ height: toRem(12) }} /> format={MarkType.Italic}
<Box gap="100"> icon={Icons.Italic}
<MarkButton tooltip={<BtnTooltip text="Italic" shortCode={`${modKey} + I`} />}
format={MarkType.Bold} />
icon={Icons.Bold} <MarkButton
tooltip={<BtnTooltip text="Bold" shortCode={`${modKey} + B`} />} format={MarkType.Underline}
icon={Icons.Underline}
tooltip={<BtnTooltip text="Underline" shortCode={`${modKey} + U`} />}
/>
<MarkButton
format={MarkType.StrikeThrough}
icon={Icons.Strike}
tooltip={
<BtnTooltip
text="Strike Through"
shortCode={`${modKey} + ${KeySymbol.Shift} + U`}
/>
}
/>
<MarkButton
format={MarkType.Code}
icon={Icons.Code}
tooltip={<BtnTooltip text="Inline Code" shortCode={`${modKey} + [`} />}
/>
<MarkButton
format={MarkType.Spoiler}
icon={Icons.EyeBlind}
tooltip={<BtnTooltip text="Spoiler" shortCode={`${modKey} + H`} />}
/>
</Box>
<Line variant="SurfaceVariant" direction="Vertical" style={{ height: toRem(12) }} />
</>
<Box shrink="No" gap="100">
<BlockButton
format={BlockType.BlockQuote}
icon={Icons.BlockQuote}
tooltip={
<BtnTooltip text="Block Quote" shortCode={`${modKey} + ${KeySymbol.Shift} + '`} />
}
/> />
<MarkButton <BlockButton
format={MarkType.Italic} format={BlockType.CodeBlock}
icon={Icons.Italic} icon={Icons.BlockCode}
tooltip={<BtnTooltip text="Italic" shortCode={`${modKey} + I`} />} tooltip={
<BtnTooltip text="Block Code" shortCode={`${modKey} + ${KeySymbol.Shift} + ;`} />
}
/> />
<MarkButton <BlockButton
format={MarkType.Underline} format={BlockType.OrderedList}
icon={Icons.Underline} icon={Icons.OrderList}
tooltip={<BtnTooltip text="Underline" shortCode={`${modKey} + U`} />} tooltip={
<BtnTooltip text="Ordered List" shortCode={`${modKey} + ${KeySymbol.Shift} + 7`} />
}
/> />
<MarkButton <BlockButton
format={MarkType.StrikeThrough} format={BlockType.UnorderedList}
icon={Icons.Strike} icon={Icons.UnorderList}
tooltip={ tooltip={
<BtnTooltip <BtnTooltip
text="Strike Through" text="Unordered List"
shortCode={`${modKey} + ${KeySymbol.Shift} + U`} shortCode={`${modKey} + ${KeySymbol.Shift} + 8`}
/> />
} }
/> />
<MarkButton <HeadingBlockButton />
format={MarkType.Code}
icon={Icons.Code}
tooltip={<BtnTooltip text="Inline Code" shortCode={`${modKey} + [`} />}
/>
<MarkButton
format={MarkType.Spoiler}
icon={Icons.EyeBlind}
tooltip={<BtnTooltip text="Spoiler" shortCode={`${modKey} + H`} />}
/>
</Box> </Box>
</> {canEscape && (
)} <>
<Line variant="SurfaceVariant" direction="Vertical" style={{ height: toRem(12) }} />
<Box shrink="No" gap="100">
<ExitFormatting
tooltip={<BtnTooltip text="Exit Formatting" shortCode={`${modKey} + E`} />}
/>
</Box>
</>
)}
</Box>
</Scroll>
</Box> </Box>
); );
} }

View file

@ -32,7 +32,9 @@ export const toggleMark = (editor: Editor, format: MarkType) => {
}; };
export const removeAllMark = (editor: Editor) => { export const removeAllMark = (editor: Editor) => {
ALL_MARK_TYPE.forEach((mark) => Editor.removeMark(editor, mark)); ALL_MARK_TYPE.forEach((mark) => {
if (isMarkActive(editor, mark)) Editor.removeMark(editor, mark);
});
}; };
export const isBlockActive = (editor: Editor, format: BlockType) => { export const isBlockActive = (editor: Editor, format: BlockType) => {

View file

@ -15,7 +15,7 @@ export const INLINE_HOTKEYS: Record<string, MarkType> = {
const INLINE_KEYS = Object.keys(INLINE_HOTKEYS); const INLINE_KEYS = Object.keys(INLINE_HOTKEYS);
export const BLOCK_HOTKEYS: Record<string, BlockType> = { export const BLOCK_HOTKEYS: Record<string, BlockType> = {
'mod+shift+0': BlockType.OrderedList, 'mod+shift+7': BlockType.OrderedList,
'mod+shift+8': BlockType.UnorderedList, 'mod+shift+8': BlockType.UnorderedList,
"mod+shift+'": BlockType.BlockQuote, "mod+shift+'": BlockType.BlockQuote,
'mod+shift+;': BlockType.CodeBlock, 'mod+shift+;': BlockType.CodeBlock,
@ -26,12 +26,12 @@ const BLOCK_KEYS = Object.keys(BLOCK_HOTKEYS);
* @return boolean true if shortcut is toggled. * @return boolean true if shortcut is toggled.
*/ */
export const toggleKeyboardShortcut = (editor: Editor, event: KeyboardEvent<Element>): boolean => { export const toggleKeyboardShortcut = (editor: Editor, event: KeyboardEvent<Element>): boolean => {
if (isHotkey('escape', event)) { if (isHotkey('mod+e', event)) {
if (isAnyMarkActive(editor)) { if (isAnyMarkActive(editor)) {
removeAllMark(editor); removeAllMark(editor);
return true; return true;
} }
console.log(isBlockActive(editor, BlockType.Paragraph));
if (!isBlockActive(editor, BlockType.Paragraph)) { if (!isBlockActive(editor, BlockType.Paragraph)) {
toggleBlock(editor, BlockType.Paragraph); toggleBlock(editor, BlockType.Paragraph);
return true; return true;

View file

@ -19,6 +19,7 @@ import {
Icon, Icon,
IconButton, IconButton,
Icons, Icons,
Line,
Overlay, Overlay,
OverlayBackdrop, OverlayBackdrop,
OverlayCenter, OverlayCenter,
@ -95,6 +96,7 @@ import { MessageReply } from '../../molecules/message/Message';
import colorMXID from '../../../util/colorMXID'; import colorMXID from '../../../util/colorMXID';
import { parseReplyBody, parseReplyFormattedBody } from '../../utils/room'; import { parseReplyBody, parseReplyFormattedBody } from '../../utils/room';
import { sanitizeText } from '../../utils/sanitize'; import { sanitizeText } from '../../utils/sanitize';
import { getResizeObserverEntry, useResizeObserver } from '../../hooks/useResizeObserver';
interface RoomInputProps { interface RoomInputProps {
roomViewRef: RefObject<HTMLElement>; roomViewRef: RefObject<HTMLElement>;
@ -158,6 +160,16 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
const handlePaste = useFilePasteHandler(handleFiles); const handlePaste = useFilePasteHandler(handleFiles);
const dropZoneVisible = useFileDropZone(roomViewRef, handleFiles); const dropZoneVisible = useFileDropZone(roomViewRef, handleFiles);
const [mobile, setMobile] = useState(document.body.clientWidth < 500);
useResizeObserver(
document.body,
useCallback((entries) => {
const bodyEntry = getResizeObserverEntry(document.body, entries);
if (bodyEntry && bodyEntry.contentRect.width < 500) setMobile(true);
else setMobile(false);
}, [])
);
useEffect(() => { useEffect(() => {
Transforms.insertFragment(editor, msgDraft); Transforms.insertFragment(editor, msgDraft);
}, [editor, msgDraft]); }, [editor, msgDraft]);
@ -500,27 +512,36 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
> >
{(anchorRef) => ( {(anchorRef) => (
<> <>
<IconButton {!mobile && (
aria-pressed={emojiBoardTab === EmojiBoardTab.Sticker} <IconButton
onClick={() => setEmojiBoardTab(EmojiBoardTab.Sticker)} aria-pressed={emojiBoardTab === EmojiBoardTab.Sticker}
variant="SurfaceVariant" onClick={() => setEmojiBoardTab(EmojiBoardTab.Sticker)}
size="300" variant="SurfaceVariant"
radii="300" size="300"
> radii="300"
<Icon >
src={Icons.Sticker} <Icon
filled={emojiBoardTab === EmojiBoardTab.Sticker} src={Icons.Sticker}
/> filled={emojiBoardTab === EmojiBoardTab.Sticker}
</IconButton> />
</IconButton>
)}
<IconButton <IconButton
ref={anchorRef} ref={anchorRef}
aria-pressed={emojiBoardTab === EmojiBoardTab.Emoji} aria-pressed={
mobile ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji
}
onClick={() => setEmojiBoardTab(EmojiBoardTab.Emoji)} onClick={() => setEmojiBoardTab(EmojiBoardTab.Emoji)}
variant="SurfaceVariant" variant="SurfaceVariant"
size="300" size="300"
radii="300" radii="300"
> >
<Icon src={Icons.Smile} filled={emojiBoardTab === EmojiBoardTab.Emoji} /> <Icon
src={Icons.Smile}
filled={
mobile ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji
}
/>
</IconButton> </IconButton>
</> </>
)} )}
@ -532,7 +553,14 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
</IconButton> </IconButton>
</> </>
} }
bottom={toolbar && <Toolbar />} bottom={
toolbar && (
<div>
<Line variant="SurfaceVariant" size="300" />
<Toolbar />
</div>
)
}
/> />
</div> </div>
); );

View file

@ -3,4 +3,7 @@ export enum KeySymbol {
Shift = '⇧', Shift = '⇧',
Option = '⌥', Option = '⌥',
Control = '⌃', Control = '⌃',
Hyper = '✦',
Super = '❖',
Escape = '⎋',
} }