Add ESC btn to toolbar to quickly exit formatting (#1283)
authorAjay Bura <32841439+ajbura@users.noreply.github.com>
Fri, 16 Jun 2023 01:09:09 +0000 (11:09 +1000)
committerGitHub <noreply@github.com>
Fri, 16 Jun 2023 01:09:09 +0000 (11:09 +1000)
* 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

src/app/components/editor/Editor.css.ts
src/app/components/editor/Editor.tsx
src/app/components/editor/Toolbar.tsx
src/app/components/editor/common.ts
src/app/components/editor/keyboard.ts
src/app/organisms/room/RoomInput.tsx
src/app/utils/key-symbol.ts

index 034ded79a3a8b99c471d6eb23fd21ca781fc483d..9ec8cfaf80b963a2f90b7a2078fa3d4efbfca88c 100644 (file)
@@ -43,6 +43,7 @@ export const EditorPlaceholder = style([
   {
     position: 'absolute',
     zIndex: 1,
+    width: '100%',
     opacity: config.opacity.Placeholder,
     pointerEvents: 'none',
     userSelect: 'none',
@@ -55,9 +56,10 @@ export const EditorPlaceholder = style([
   },
 ]);
 
-export const EditorToolbar = style([
-  DefaultReset,
-  {
-    padding: config.space.S100,
-  },
-]);
+export const EditorToolbarBase = style({
+  padding: `0 ${config.borderWidth.B300}`,
+});
+
+export const EditorToolbar = style({
+  padding: config.space.S100,
+});
index 2657b2135b0c7d23551bc8c54f54982d209012c4..3f048a6f88c43fb9db4d035f5ea453157ed957b2 100644 (file)
@@ -104,7 +104,13 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
       // eslint-disable-next-line @typescript-eslint/no-unused-vars
       const { style, ...props } = attributes;
       return (
-        <Text as="span" {...props} className={css.EditorPlaceholder} contentEditable={false}>
+        <Text
+          as="span"
+          {...props}
+          className={css.EditorPlaceholder}
+          contentEditable={false}
+          truncate
+        >
           {children}
         </Text>
       );
index a84fca2297efd2683b746556d16aba779054633d..72e2c38cfb84340af604e817c730943a31708798 100644 (file)
@@ -10,6 +10,7 @@ import {
   Line,
   Menu,
   PopOut,
+  Scroll,
   Text,
   Tooltip,
   TooltipProvider,
@@ -17,7 +18,14 @@ import {
 } from 'folds';
 import React, { ReactNode, useState } from '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 { BlockType, MarkType } from './Elements';
 import { HeadingLevel } from './slate';
@@ -44,6 +52,11 @@ function BtnTooltip({ text, shortCode }: { text: string; shortCode?: string }) {
 type MarkButtonProps = { format: MarkType; icon: IconSrc; tooltip: ReactNode };
 export function MarkButton({ format, icon, tooltip }: MarkButtonProps) {
   const editor = useSlate();
+  const disableInline = isBlockActive(editor, BlockType.CodeBlock);
+
+  if (disableInline) {
+    removeAllMark(editor);
+  }
 
   const handleClick = () => {
     toggleMark(editor, format);
@@ -58,10 +71,11 @@ export function MarkButton({ format, icon, tooltip }: MarkButtonProps) {
           variant="SurfaceVariant"
           onClick={handleClick}
           aria-pressed={isMarkActive(editor, format)}
-          size="300"
+          size="400"
           radii="300"
+          disabled={disableInline}
         >
-          <Icon size="50" src={icon} />
+          <Icon size="200" src={icon} />
         </IconButton>
       )}
     </TooltipProvider>
@@ -89,10 +103,10 @@ export function BlockButton({ format, icon, tooltip }: BlockButtonProps) {
           variant="SurfaceVariant"
           onClick={handleClick}
           aria-pressed={isBlockActive(editor, format)}
-          size="300"
+          size="400"
           radii="300"
         >
-          <Icon size="50" src={icon} />
+          <Icon size="200" src={icon} />
         </IconButton>
       )}
     </TooltipProvider>
@@ -115,6 +129,7 @@ export function HeadingBlockButton() {
   return (
     <PopOut
       open={open}
+      offset={5}
       align="Start"
       position="Top"
       content={
@@ -130,14 +145,14 @@ export function HeadingBlockButton() {
         >
           <Menu style={{ padding: config.space.S100 }}>
             <Box gap="100">
-              <IconButton onClick={() => handleMenuSelect(1)} size="300" radii="300">
-                <Icon size="100" src={Icons.Heading1} />
+              <IconButton onClick={() => handleMenuSelect(1)} size="400" radii="300">
+                <Icon size="200" src={Icons.Heading1} />
               </IconButton>
-              <IconButton onClick={() => handleMenuSelect(2)} size="300" radii="300">
-                <Icon size="100" src={Icons.Heading2} />
+              <IconButton onClick={() => handleMenuSelect(2)} size="400" radii="300">
+                <Icon size="200" src={Icons.Heading2} />
               </IconButton>
-              <IconButton onClick={() => handleMenuSelect(3)} size="300" radii="300">
-                <Icon size="100" src={Icons.Heading3} />
+              <IconButton onClick={() => handleMenuSelect(3)} size="400" radii="300">
+                <Icon size="200" src={Icons.Heading3} />
               </IconButton>
             </Box>
           </Menu>
@@ -151,97 +166,143 @@ export function HeadingBlockButton() {
           variant="SurfaceVariant"
           onClick={() => (isActive ? toggleBlock(editor, BlockType.Heading) : setOpen(!open))}
           aria-pressed={isActive}
-          size="300"
+          size="400"
           radii="300"
         >
-          <Icon size="50" src={Icons[`Heading${level}`]} />
-          <Icon size="50" src={isActive ? Icons.Cross : Icons.ChevronBottom} />
+          <Icon size="200" src={Icons[`Heading${level}`]} />
+          <Icon size="200" src={isActive ? Icons.Cross : Icons.ChevronBottom} />
         </IconButton>
       )}
     </PopOut>
   );
 }
 
+type ExitFormattingProps = { tooltip: ReactNode };
+export function ExitFormatting({ tooltip }: ExitFormattingProps) {
+  const editor = useSlate();
+
+  const handleClick = () => {
+    if (isAnyMarkActive(editor)) {
+      removeAllMark(editor);
+    } else if (!isBlockActive(editor, BlockType.Paragraph)) {
+      toggleBlock(editor, BlockType.Paragraph);
+    }
+    ReactEditor.focus(editor);
+  };
+
+  return (
+    <TooltipProvider tooltip={tooltip} delay={500}>
+      {(triggerRef) => (
+        <IconButton
+          ref={triggerRef}
+          variant="SurfaceVariant"
+          onClick={handleClick}
+          size="400"
+          radii="300"
+        >
+          <Text size="B400">{`Exit ${KeySymbol.Hyper}`}</Text>
+        </IconButton>
+      )}
+    </TooltipProvider>
+  );
+}
+
 export function Toolbar() {
   const editor = useSlate();
-  const allowInline = !isBlockActive(editor, BlockType.CodeBlock);
   const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl';
 
+  const canEscape = isAnyMarkActive(editor) || !isBlockActive(editor, BlockType.Paragraph);
+
   return (
-    <Box className={css.EditorToolbar} alignItems="Center" gap="300">
-      <Box gap="100">
-        <HeadingBlockButton />
-        <BlockButton
-          format={BlockType.OrderedList}
-          icon={Icons.OrderList}
-          tooltip={
-            <BtnTooltip text="Ordered List" shortCode={`${modKey} + ${KeySymbol.Shift} + 0`} />
-          }
-        />
-        <BlockButton
-          format={BlockType.UnorderedList}
-          icon={Icons.UnorderList}
-          tooltip={
-            <BtnTooltip text="Unordered List" shortCode={`${modKey} + ${KeySymbol.Shift} + 8`} />
-          }
-        />
-        <BlockButton
-          format={BlockType.BlockQuote}
-          icon={Icons.BlockQuote}
-          tooltip={
-            <BtnTooltip text="Block Quote" shortCode={`${modKey} + ${KeySymbol.Shift} + '`} />
-          }
-        />
-        <BlockButton
-          format={BlockType.CodeBlock}
-          icon={Icons.BlockCode}
-          tooltip={
-            <BtnTooltip text="Block Code" shortCode={`${modKey} + ${KeySymbol.Shift} + ;`} />
-          }
-        />
-      </Box>
-      {allowInline && (
-        <>
-          <Line variant="SurfaceVariant" direction="Vertical" style={{ height: toRem(12) }} />
-          <Box gap="100">
-            <MarkButton
-              format={MarkType.Bold}
-              icon={Icons.Bold}
-              tooltip={<BtnTooltip text="Bold" shortCode={`${modKey} + B`} />}
+    <Box className={css.EditorToolbarBase}>
+      <Scroll direction="Horizontal" size="0">
+        <Box className={css.EditorToolbar} alignItems="Center" gap="300">
+          <>
+            <Box shrink="No" gap="100">
+              <MarkButton
+                format={MarkType.Bold}
+                icon={Icons.Bold}
+                tooltip={<BtnTooltip text="Bold" shortCode={`${modKey} + B`} />}
+              />
+              <MarkButton
+                format={MarkType.Italic}
+                icon={Icons.Italic}
+                tooltip={<BtnTooltip text="Italic" shortCode={`${modKey} + I`} />}
+              />
+              <MarkButton
+                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
-              format={MarkType.Italic}
-              icon={Icons.Italic}
-              tooltip={<BtnTooltip text="Italic" shortCode={`${modKey} + I`} />}
+            <BlockButton
+              format={BlockType.CodeBlock}
+              icon={Icons.BlockCode}
+              tooltip={
+                <BtnTooltip text="Block Code" shortCode={`${modKey} + ${KeySymbol.Shift} + ;`} />
+              }
             />
-            <MarkButton
-              format={MarkType.Underline}
-              icon={Icons.Underline}
-              tooltip={<BtnTooltip text="Underline" shortCode={`${modKey} + U`} />}
+            <BlockButton
+              format={BlockType.OrderedList}
+              icon={Icons.OrderList}
+              tooltip={
+                <BtnTooltip text="Ordered List" shortCode={`${modKey} + ${KeySymbol.Shift} + 7`} />
+              }
             />
-            <MarkButton
-              format={MarkType.StrikeThrough}
-              icon={Icons.Strike}
+            <BlockButton
+              format={BlockType.UnorderedList}
+              icon={Icons.UnorderList}
               tooltip={
                 <BtnTooltip
-                  text="Strike Through"
-                  shortCode={`${modKey} + ${KeySymbol.Shift} + U`}
+                  text="Unordered List"
+                  shortCode={`${modKey} + ${KeySymbol.Shift} + 8`}
                 />
               }
             />
-            <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`} />}
-            />
+            <HeadingBlockButton />
           </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>
   );
 }
index 619a1bfee3ab97a8c04455a817f7353b2c7ae283..2f20790de8bdc23f620be4a69ae3d80fbd6fcf84 100644 (file)
@@ -32,7 +32,9 @@ export const toggleMark = (editor: Editor, format: MarkType) => {
 };
 
 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) => {
index 3fbe5363d5335757e6fc7cf4debc9b19c3791cb1..b6e1c3f4fdcba3afd0c88de348eb5efc289dfe21 100644 (file)
@@ -15,7 +15,7 @@ export const INLINE_HOTKEYS: Record<string, MarkType> = {
 const INLINE_KEYS = Object.keys(INLINE_HOTKEYS);
 
 export const BLOCK_HOTKEYS: Record<string, BlockType> = {
-  'mod+shift+0': BlockType.OrderedList,
+  'mod+shift+7': BlockType.OrderedList,
   'mod+shift+8': BlockType.UnorderedList,
   "mod+shift+'": BlockType.BlockQuote,
   'mod+shift+;': BlockType.CodeBlock,
@@ -26,12 +26,12 @@ const BLOCK_KEYS = Object.keys(BLOCK_HOTKEYS);
  * @return boolean true if shortcut is toggled.
  */
 export const toggleKeyboardShortcut = (editor: Editor, event: KeyboardEvent<Element>): boolean => {
-  if (isHotkey('escape', event)) {
+  if (isHotkey('mod+e', event)) {
     if (isAnyMarkActive(editor)) {
       removeAllMark(editor);
       return true;
     }
-    console.log(isBlockActive(editor, BlockType.Paragraph));
+
     if (!isBlockActive(editor, BlockType.Paragraph)) {
       toggleBlock(editor, BlockType.Paragraph);
       return true;
index e79f4883b2a31f7b44b9ae914ca4bacdc15548ca..3a22b57f83568626e2f55e1a646d25ed151a1265 100644 (file)
@@ -19,6 +19,7 @@ import {
   Icon,
   IconButton,
   Icons,
+  Line,
   Overlay,
   OverlayBackdrop,
   OverlayCenter,
@@ -95,6 +96,7 @@ import { MessageReply } from '../../molecules/message/Message';
 import colorMXID from '../../../util/colorMXID';
 import { parseReplyBody, parseReplyFormattedBody } from '../../utils/room';
 import { sanitizeText } from '../../utils/sanitize';
+import { getResizeObserverEntry, useResizeObserver } from '../../hooks/useResizeObserver';
 
 interface RoomInputProps {
   roomViewRef: RefObject<HTMLElement>;
@@ -158,6 +160,16 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
     const handlePaste = useFilePasteHandler(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(() => {
       Transforms.insertFragment(editor, msgDraft);
     }, [editor, msgDraft]);
@@ -500,27 +512,36 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
                   >
                     {(anchorRef) => (
                       <>
-                        <IconButton
-                          aria-pressed={emojiBoardTab === EmojiBoardTab.Sticker}
-                          onClick={() => setEmojiBoardTab(EmojiBoardTab.Sticker)}
-                          variant="SurfaceVariant"
-                          size="300"
-                          radii="300"
-                        >
-                          <Icon
-                            src={Icons.Sticker}
-                            filled={emojiBoardTab === EmojiBoardTab.Sticker}
-                          />
-                        </IconButton>
+                        {!mobile && (
+                          <IconButton
+                            aria-pressed={emojiBoardTab === EmojiBoardTab.Sticker}
+                            onClick={() => setEmojiBoardTab(EmojiBoardTab.Sticker)}
+                            variant="SurfaceVariant"
+                            size="300"
+                            radii="300"
+                          >
+                            <Icon
+                              src={Icons.Sticker}
+                              filled={emojiBoardTab === EmojiBoardTab.Sticker}
+                            />
+                          </IconButton>
+                        )}
                         <IconButton
                           ref={anchorRef}
-                          aria-pressed={emojiBoardTab === EmojiBoardTab.Emoji}
+                          aria-pressed={
+                            mobile ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji
+                          }
                           onClick={() => setEmojiBoardTab(EmojiBoardTab.Emoji)}
                           variant="SurfaceVariant"
                           size="300"
                           radii="300"
                         >
-                          <Icon src={Icons.Smile} filled={emojiBoardTab === EmojiBoardTab.Emoji} />
+                          <Icon
+                            src={Icons.Smile}
+                            filled={
+                              mobile ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji
+                            }
+                          />
                         </IconButton>
                       </>
                     )}
@@ -532,7 +553,14 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
               </IconButton>
             </>
           }
-          bottom={toolbar && <Toolbar />}
+          bottom={
+            toolbar && (
+              <div>
+                <Line variant="SurfaceVariant" size="300" />
+                <Toolbar />
+              </div>
+            )
+          }
         />
       </div>
     );
index 7e758fd9477b92a401df6f63fc9d4e7746e3081a..00dd1042a657539f1952cc11310c72ce09b2a2e0 100644 (file)
@@ -3,4 +3,7 @@ export enum KeySymbol {
   Shift = '⇧',
   Option = '⌥',
   Control = '⌃',
+  Hyper = '✦',
+  Super = '❖',
+  Escape = '⎋',
 }