Fix theme input scrollbar line, jumbo emoji, zalgo clip, and Windows autocorrect dupe.
Make twilight/mocha scrollbar tracks transparent so the editor no longer shows a vertical strip, size standalone emoji to 100px, loosen zalgo clipping with padded overflow, and handle insertReplacementText via onDOMBeforeInput so Slate does not insert the correction twice.
This commit is contained in:
@@ -6,12 +6,11 @@ import React, {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { Box, Scroll, Text } from 'folds';
|
||||
import { Descendant, Editor, createEditor, Transforms, Range, Element as SlateElement, Text as SlateText, Point } from 'slate';
|
||||
import {
|
||||
ReactEditor,
|
||||
Slate,
|
||||
Editable,
|
||||
withReact,
|
||||
@@ -172,48 +171,41 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
|
||||
[editor, onKeyDown]
|
||||
);
|
||||
|
||||
const handleBeforeInput = useCallback(
|
||||
(event: Event) => {
|
||||
const inputEvent = event as InputEvent;
|
||||
|
||||
// Handle autocorrect replacement that causes text duplication
|
||||
if (inputEvent.inputType === 'insertReplacementText' ||
|
||||
inputEvent.inputType === 'insertFromComposition') {
|
||||
const { selection } = editor;
|
||||
if (!selection) return;
|
||||
/**
|
||||
* Spellcheck / right-click autocorrect (insertReplacementText).
|
||||
* Must run as Editable's onDOMBeforeInput and return true so Slate skips its
|
||||
* own insert — slate-react ignores event.defaultPrevented when this prop is unset,
|
||||
* which previously duplicated the corrected word (especially on Windows Electron).
|
||||
*/
|
||||
const handleDOMBeforeInput = useCallback(
|
||||
(event: InputEvent) => {
|
||||
if (event.inputType !== 'insertReplacementText') return false;
|
||||
|
||||
// Get the data being inserted
|
||||
const data = inputEvent.data || inputEvent.dataTransfer?.getData('text/plain');
|
||||
|
||||
if (data) {
|
||||
event.preventDefault();
|
||||
|
||||
// If there's selected text, delete it first
|
||||
if (selection && !Range.isCollapsed(selection)) {
|
||||
Transforms.delete(editor, { at: selection });
|
||||
}
|
||||
|
||||
// Insert the replacement text
|
||||
editor.insertText(data);
|
||||
const data =
|
||||
event.dataTransfer?.getData('text/plain') || event.data || '';
|
||||
if (!data) return false;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const [targetRange] = event.getTargetRanges();
|
||||
if (targetRange) {
|
||||
const slateRange = ReactEditor.toSlateRange(editor, targetRange, {
|
||||
exactMatch: false,
|
||||
suppressThrow: true,
|
||||
});
|
||||
if (slateRange) {
|
||||
Transforms.select(editor, slateRange);
|
||||
}
|
||||
} else if (editor.selection && !Range.isCollapsed(editor.selection)) {
|
||||
Transforms.delete(editor, { at: editor.selection });
|
||||
}
|
||||
|
||||
Transforms.insertText(editor, data);
|
||||
return true;
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
const editableRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const editableElement = editableRef.current?.querySelector('[data-slate-editor="true"]');
|
||||
if (!editableElement) return;
|
||||
|
||||
editableElement.addEventListener('beforeinput', handleBeforeInput, { capture: true });
|
||||
|
||||
return () => {
|
||||
editableElement.removeEventListener('beforeinput', handleBeforeInput, { capture: true });
|
||||
};
|
||||
}, [handleBeforeInput]);
|
||||
|
||||
const renderPlaceholder = useCallback(
|
||||
({ attributes, children }: RenderPlaceholderProps) => (
|
||||
<span {...attributes} className={css.EditorPlaceholderContainer}>
|
||||
@@ -259,7 +251,6 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
|
||||
size="300"
|
||||
visibility="Hover"
|
||||
hideTrack
|
||||
ref={editableRef}
|
||||
>
|
||||
<Editable
|
||||
data-editable-name={editableName}
|
||||
@@ -269,6 +260,7 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
|
||||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
decorate={decorate}
|
||||
onDOMBeforeInput={handleDOMBeforeInput}
|
||||
onKeyDown={handleKeydown}
|
||||
onKeyUp={onKeyUp}
|
||||
onPaste={onPaste}
|
||||
|
||||
@@ -35,6 +35,8 @@ export const ReplyContent = style({
|
||||
overflow: 'clip',
|
||||
overflowY: 'clip',
|
||||
lineHeight: '1.5',
|
||||
paddingBlock: toRem(4),
|
||||
marginBlock: toRem(-4),
|
||||
selectors: {
|
||||
[`${Reply}:hover &`]: {
|
||||
opacity: config.opacity.P500,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createVar, keyframes, style, styleVariants } from '@vanilla-extract/css';
|
||||
import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
|
||||
import { DefaultReset, color, config, toRem } from 'folds';
|
||||
import * as htmlCss from '../../../styles/CustomHtml.css';
|
||||
|
||||
export const StickySection = style({
|
||||
position: 'sticky',
|
||||
@@ -212,9 +213,12 @@ export const UsernameBold = style({
|
||||
export const MessageTextBody = recipe({
|
||||
base: {
|
||||
wordBreak: 'break-word',
|
||||
// Clip zalgo/combining marks with room so normal text + emoji aren't shaved
|
||||
overflow: 'clip',
|
||||
overflowY: 'clip',
|
||||
lineHeight: '1.5',
|
||||
padding: toRem(20),
|
||||
margin: toRem(-20),
|
||||
userSelect: 'text',
|
||||
WebkitUserSelect: 'text',
|
||||
},
|
||||
@@ -226,8 +230,27 @@ export const MessageTextBody = recipe({
|
||||
},
|
||||
jumboEmoji: {
|
||||
true: {
|
||||
fontSize: '1.504em',
|
||||
lineHeight: '1.4962em',
|
||||
fontSize: toRem(100),
|
||||
lineHeight: toRem(100),
|
||||
selectors: {
|
||||
[`& .${htmlCss.EmoticonBase}`]: {
|
||||
height: toRem(100),
|
||||
padding: 0,
|
||||
verticalAlign: 'bottom',
|
||||
},
|
||||
[`& .${htmlCss.Emoticon.classNames.base}`]: {
|
||||
fontSize: toRem(100),
|
||||
height: toRem(100),
|
||||
minWidth: toRem(100),
|
||||
lineHeight: toRem(100),
|
||||
top: 0,
|
||||
},
|
||||
[`& .${htmlCss.EmoticonImg}`]: {
|
||||
height: toRem(100),
|
||||
width: toRem(100),
|
||||
objectFit: 'contain',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
emote: {
|
||||
|
||||
@@ -19,7 +19,7 @@ export const MarginSpaced = style({
|
||||
export const Paragraph = style([
|
||||
DefaultReset,
|
||||
{
|
||||
overflow: 'hidden',
|
||||
// Zalgo clipping is handled by MessageTextBody padding + overflow
|
||||
lineHeight: '1.5',
|
||||
},
|
||||
]);
|
||||
@@ -29,7 +29,6 @@ export const Heading = style([
|
||||
MarginSpaced,
|
||||
{
|
||||
marginTop: config.space.S400,
|
||||
overflow: 'hidden',
|
||||
lineHeight: '1.5',
|
||||
selectors: {
|
||||
'&:first-child': {
|
||||
@@ -46,7 +45,6 @@ export const BlockQuote = style([
|
||||
paddingLeft: config.space.S200,
|
||||
borderLeft: `${config.borderWidth.B700} solid ${color.SurfaceVariant.ContainerLine}`,
|
||||
fontStyle: 'italic',
|
||||
overflow: 'hidden',
|
||||
lineHeight: '1.5',
|
||||
},
|
||||
]);
|
||||
@@ -73,7 +71,6 @@ export const Code = style([
|
||||
{
|
||||
padding: `0 ${config.space.S100}`,
|
||||
display: 'inline',
|
||||
overflow: 'hidden',
|
||||
lineHeight: '1.5',
|
||||
},
|
||||
]);
|
||||
@@ -120,7 +117,6 @@ export const Spoiler = recipe({
|
||||
padding: `0 ${config.space.S100}`,
|
||||
backgroundColor: color.SurfaceVariant.ContainerActive,
|
||||
borderRadius: config.radii.R300,
|
||||
overflow: 'hidden',
|
||||
lineHeight: '1.5',
|
||||
selectors: {
|
||||
'&[aria-pressed=true]': {
|
||||
@@ -182,7 +178,6 @@ export const List = style([
|
||||
{
|
||||
padding: `0 ${config.space.S100}`,
|
||||
paddingLeft: config.space.S600,
|
||||
overflow: 'hidden',
|
||||
lineHeight: '1.5',
|
||||
},
|
||||
]);
|
||||
@@ -211,7 +206,6 @@ export const Mention = recipe({
|
||||
padding: `0 ${toRem(2)}`,
|
||||
borderRadius: config.radii.R300,
|
||||
fontWeight: config.fontWeight.W500,
|
||||
overflow: 'hidden',
|
||||
lineHeight: '1.5',
|
||||
},
|
||||
],
|
||||
@@ -238,7 +232,6 @@ export const Command = recipe({
|
||||
padding: `0 ${toRem(2)}`,
|
||||
borderRadius: config.radii.R300,
|
||||
fontWeight: config.fontWeight.W500,
|
||||
overflow: 'hidden',
|
||||
lineHeight: '1.5',
|
||||
},
|
||||
],
|
||||
@@ -308,7 +301,6 @@ export const highlightText = style([
|
||||
{
|
||||
backgroundColor: 'yellow',
|
||||
color: 'black',
|
||||
overflow: 'hidden',
|
||||
lineHeight: '1.5',
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -257,7 +257,7 @@ body.mocha-theme {
|
||||
}
|
||||
|
||||
.twilight-theme ::-webkit-scrollbar-track {
|
||||
background: rgba(20, 18, 31, 0.5);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.twilight-theme ::-webkit-scrollbar-thumb {
|
||||
@@ -355,7 +355,7 @@ body.mocha-theme {
|
||||
}
|
||||
|
||||
.mocha-theme ::-webkit-scrollbar-track {
|
||||
background: rgba(26, 22, 20, 0.5);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.mocha-theme ::-webkit-scrollbar-thumb {
|
||||
|
||||
Reference in New Issue
Block a user