fix: Document titles in RTL script not correctly aligned

This commit is contained in:
Tom Moor
2021-12-17 11:29:40 -08:00
parent 93efedb912
commit 8b73f98b9a
3 changed files with 155 additions and 139 deletions

View File

@@ -20,78 +20,85 @@ type Props = Omit<React.HTMLAttributes<HTMLSpanElement>, "ref" | "onChange"> & {
* Defines a content editable component with the same interface as a native
* HTMLInputElement (or, as close as we can get).
*/
function ContentEditable({
disabled,
onChange,
onInput,
onBlur,
onKeyDown,
value,
children,
className,
maxLength,
autoFocus,
placeholder,
readOnly,
...rest
}: Props) {
const ref = React.useRef<HTMLSpanElement>(null);
const [innerHTML, setInnerHTML] = React.useState<string>(value);
const lastValue = React.useRef("");
const ContentEditable = React.forwardRef(
(
{
disabled,
onChange,
onInput,
onBlur,
onKeyDown,
value,
children,
className,
maxLength,
autoFocus,
placeholder,
readOnly,
dir,
...rest
}: Props,
forwardedRef: React.RefObject<HTMLSpanElement>
) => {
const innerRef = React.useRef<HTMLSpanElement>(null);
const ref = forwardedRef || innerRef;
const [innerHTML, setInnerHTML] = React.useState<string>(value);
const lastValue = React.useRef("");
const wrappedEvent = (
callback:
| React.FocusEventHandler<HTMLSpanElement>
| React.FormEventHandler<HTMLSpanElement>
| React.KeyboardEventHandler<HTMLSpanElement>
| undefined
) => (event: any) => {
const text = ref.current?.innerText || "";
const wrappedEvent = (
callback:
| React.FocusEventHandler<HTMLSpanElement>
| React.FormEventHandler<HTMLSpanElement>
| React.KeyboardEventHandler<HTMLSpanElement>
| undefined
) => (event: any) => {
const text = ref.current?.innerText || "";
if (maxLength && isPrintableKeyEvent(event) && text.length >= maxLength) {
event?.preventDefault();
return;
}
if (maxLength && isPrintableKeyEvent(event) && text.length >= maxLength) {
event?.preventDefault();
return;
}
if (text !== lastValue.current) {
lastValue.current = text;
onChange && onChange(text);
}
if (text !== lastValue.current) {
lastValue.current = text;
onChange && onChange(text);
}
callback?.(event);
};
callback?.(event);
};
React.useLayoutEffect(() => {
if (autoFocus) {
ref.current?.focus();
}
});
React.useLayoutEffect(() => {
if (autoFocus) {
ref.current?.focus();
}
});
React.useEffect(() => {
if (value !== ref.current?.innerText) {
setInnerHTML(value);
}
}, [value]);
React.useEffect(() => {
if (value !== ref.current?.innerText) {
setInnerHTML(value);
}
}, [value, ref]);
return (
<div className={className}>
<Content
ref={ref}
contentEditable={!disabled && !readOnly}
onInput={wrappedEvent(onInput)}
onBlur={wrappedEvent(onBlur)}
onKeyDown={wrappedEvent(onKeyDown)}
data-placeholder={placeholder}
role="textbox"
dangerouslySetInnerHTML={{
__html: innerHTML,
}}
{...rest}
/>
{children}
</div>
);
}
return (
<div className={className} dir={dir}>
<Content
ref={ref}
contentEditable={!disabled && !readOnly}
onInput={wrappedEvent(onInput)}
onBlur={wrappedEvent(onBlur)}
onKeyDown={wrappedEvent(onKeyDown)}
data-placeholder={placeholder}
role="textbox"
dangerouslySetInnerHTML={{
__html: innerHTML,
}}
{...rest}
/>
{children}
</div>
);
}
);
const Content = styled.span`
&:empty {
@@ -108,4 +115,4 @@ const Content = styled.span`
}
`;
export default React.memo<Props>(ContentEditable);
export default ContentEditable;

View File

@@ -27,86 +27,92 @@ type Props = {
onSave?: (options: { publish?: boolean; done?: boolean }) => void;
};
function EditableTitle({
value,
document,
readOnly,
onChange,
onSave,
onGoToNextInput,
starrable,
}: Props) {
const { policies } = useStores();
const { t } = useTranslation();
const can = policies.abilities(document.id);
const { emoji } = parseTitle(value);
const startsWithEmojiAndSpace = !!(emoji && value.startsWith(`${emoji} `));
const normalizedTitle =
!value && readOnly ? document.titleWithDefault : value;
const EditableTitle = React.forwardRef(
(
{
value,
document,
readOnly,
onChange,
onSave,
onGoToNextInput,
starrable,
}: Props,
ref: React.RefObject<HTMLSpanElement>
) => {
const { policies } = useStores();
const { t } = useTranslation();
const can = policies.abilities(document.id);
const { emoji } = parseTitle(value);
const startsWithEmojiAndSpace = !!(emoji && value.startsWith(`${emoji} `));
const normalizedTitle =
!value && readOnly ? document.titleWithDefault : value;
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent) => {
if (event.key === "Enter") {
event.preventDefault();
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent) => {
if (event.key === "Enter") {
event.preventDefault();
if (isModKey(event)) {
if (isModKey(event)) {
onSave?.({
done: true,
});
return;
}
onGoToNextInput(true);
return;
}
if (event.key === "Tab" || event.key === "ArrowDown") {
event.preventDefault();
onGoToNextInput();
return;
}
if (event.key === "p" && isModKey(event) && event.shiftKey) {
event.preventDefault();
onSave?.({
publish: true,
done: true,
});
return;
}
onGoToNextInput(true);
return;
}
if (event.key === "s" && isModKey(event)) {
event.preventDefault();
onSave?.({});
return;
}
},
[onGoToNextInput, onSave]
);
if (event.key === "Tab" || event.key === "ArrowDown") {
event.preventDefault();
onGoToNextInput();
return;
}
if (event.key === "p" && isModKey(event) && event.shiftKey) {
event.preventDefault();
onSave?.({
publish: true,
done: true,
});
return;
}
if (event.key === "s" && isModKey(event)) {
event.preventDefault();
onSave?.({});
return;
}
},
[onGoToNextInput, onSave]
);
return (
<Title
onChange={onChange}
onKeyDown={handleKeyDown}
placeholder={
document.isTemplate
? t("Start your template…")
: t("Start with a title…")
}
value={normalizedTitle}
$startsWithEmojiAndSpace={startsWithEmojiAndSpace}
$isStarred={document.isStarred}
autoFocus={!value}
maxLength={MAX_TITLE_LENGTH}
readOnly={readOnly}
dir="auto"
>
{(can.star || can.unstar) && starrable !== false && (
<StarButton document={document} size={32} />
)}
</Title>
);
}
return (
<Title
onChange={onChange}
onKeyDown={handleKeyDown}
placeholder={
document.isTemplate
? t("Start your template…")
: t("Start with a title…")
}
value={normalizedTitle}
$startsWithEmojiAndSpace={startsWithEmojiAndSpace}
$isStarred={document.isStarred}
autoFocus={!value}
maxLength={MAX_TITLE_LENGTH}
readOnly={readOnly}
dir="auto"
ref={ref}
>
{(can.star || can.unstar) && starrable !== false && (
<StarButton document={document} size={32} />
)}
</Title>
);
}
);
const StarButton = styled(Star)`
position: relative;

View File

@@ -37,6 +37,7 @@ class DocumentEditor extends React.Component<Props> {
activeLinkEvent: MouseEvent | null | undefined;
ref = React.createRef<HTMLDivElement | HTMLInputElement>();
titleRef = React.createRef<HTMLSpanElement>();
focusAtStart = () => {
if (this.props.innerRef.current) {
@@ -94,6 +95,7 @@ class DocumentEditor extends React.Component<Props> {
return (
<Flex auto column>
<EditableTitle
ref={this.titleRef}
value={title}
readOnly={readOnly}
document={document}
@@ -107,8 +109,9 @@ class DocumentEditor extends React.Component<Props> {
document={document}
to={documentHistoryUrl(document)}
rtl={
this.ref.current
? window.getComputedStyle(this.ref.current).direction === "rtl"
this.titleRef.current
? window.getComputedStyle(this.titleRef.current).direction ===
"rtl"
: false
}
/>