fix: More selection toolbar fixes around link selection (#11408)

This commit is contained in:
Tom Moor
2026-02-10 21:26:11 -05:00
committed by GitHub
parent 7252701e9b
commit 22556b2121
4 changed files with 51 additions and 35 deletions
+3 -3
View File
@@ -95,7 +95,7 @@ const IconWrapper = styled.span`
export const Outline = styled(Flex)<{
margin?: string | number;
hasError?: boolean;
focused?: boolean;
$focused?: boolean;
}>`
flex: 1;
margin: ${(props) =>
@@ -106,7 +106,7 @@ export const Outline = styled(Flex)<{
border-color: ${(props) =>
props.hasError
? props.theme.danger
: props.focused
: props.$focused
? props.theme.inputBorderFocused
: props.theme.inputBorder};
border-radius: 4px;
@@ -224,7 +224,7 @@ function Input(
) : (
wrappedLabel
))}
<Outline focused={focused} margin={margin}>
<Outline $focused={focused} margin={margin}>
{prefix}
{icon && <IconWrapper>{icon}</IconWrapper>}
{type === "textarea" ? (
+3 -1
View File
@@ -33,6 +33,7 @@ type Props = {
mark?: Mark;
dictionary: Dictionary;
view: EditorView;
autoFocus?: boolean;
onLinkAdd: () => void;
onLinkUpdate: () => void;
onLinkRemove: () => void;
@@ -45,6 +46,7 @@ const LinkEditor: React.FC<Props> = ({
mark,
dictionary,
view,
autoFocus,
onLinkAdd,
onLinkUpdate,
onLinkRemove,
@@ -209,7 +211,7 @@ const LinkEditor: React.FC<Props> = ({
onKeyDown={handleKeyDown}
onChange={handleSearch}
onFocus={handleSearch}
autoFocus={getHref() === ""}
autoFocus={autoFocus}
readOnly={!view.editable}
/>
{actions.map((action, index) => {
+44 -30
View File
@@ -87,25 +87,29 @@ export function SelectionToolbar(props: Props) {
const isMobile = useMobile();
const isActive = props.isActive || isMobile;
const { state } = view;
const [autoFocusLinkInput, setAutoFocusLinkInput] = React.useState(false);
const isDragging = useIsDragging(state);
const { selection } = state;
const [activeToolbar, setActiveToolbar] = React.useState<Toolbar | null>(
null
);
React.useEffect(() => {
const { selection } = state;
const linkMark =
selection instanceof NodeSelection
? getMarkRangeNodeSelection(selection, state.schema.marks.link)
: getMarkRange(selection.$from, state.schema.marks.link);
const linkMark =
selection instanceof NodeSelection
? getMarkRangeNodeSelection(selection, state.schema.marks.link)
: getMarkRange(selection.$from, state.schema.marks.link);
const isEmbedSelection =
selection instanceof NodeSelection &&
selection.node.type.name === "embed";
const isEmbedSelection =
selection instanceof NodeSelection && selection.node.type.name === "embed";
const isCodeSelection = isInCode(state, { onlyBlock: true });
const isNoticeSelection = isInNotice(state);
const isCodeSelection = isInCode(state, { onlyBlock: true });
const isNoticeSelection = isInNotice(state);
React.useLayoutEffect(() => {
if (!isActive) {
setActiveToolbar(null);
return;
}
if (isEmbedSelection && !readOnly) {
setActiveToolbar(Toolbar.Media);
@@ -124,22 +128,37 @@ export function SelectionToolbar(props: Props) {
} else if (selection.empty) {
setActiveToolbar(null);
}
}, [readOnly, selection]);
}, [
readOnly,
isActive,
selection,
linkMark,
isEmbedSelection,
isCodeSelection,
isNoticeSelection,
]);
React.useLayoutEffect(() => {
if (autoFocusLinkInput && activeToolbar !== Toolbar.Link) {
setAutoFocusLinkInput(false);
}
}, [activeToolbar]);
// Refocus the editor when the link toolbar closes to prevent focus loss
const prevActiveToolbar = React.useRef(activeToolbar);
React.useEffect(() => {
React.useLayoutEffect(() => {
if (
prevActiveToolbar.current === Toolbar.Link &&
activeToolbar !== Toolbar.Link &&
!readOnly
!readOnly &&
isActive
) {
view.focus();
}
prevActiveToolbar.current = activeToolbar;
}, [activeToolbar, readOnly, view]);
}, [activeToolbar, readOnly, isActive, view]);
React.useEffect(() => {
React.useLayoutEffect(() => {
const handleClickOutside = (ev: MouseEvent): void => {
if (
ev.target instanceof HTMLElement &&
@@ -193,11 +212,10 @@ export function SelectionToolbar(props: Props) {
) {
ev.preventDefault();
ev.stopPropagation();
if (activeToolbar === Toolbar.Link) {
setActiveToolbar(Toolbar.Menu);
} else if (activeToolbar === Toolbar.Menu) {
setActiveToolbar(Toolbar.Link);
}
setAutoFocusLinkInput(true);
setActiveToolbar(
activeToolbar === Toolbar.Link ? Toolbar.Menu : Toolbar.Link
);
}
},
view.dom,
@@ -218,12 +236,6 @@ export function SelectionToolbar(props: Props) {
const isAttachmentSelection =
selection instanceof NodeSelection &&
selection.node.type.name === "attachment";
const isCodeSelection = isInCode(state, { onlyBlock: true });
const isNoticeSelection = isInNotice(state);
const link =
selection instanceof NodeSelection
? getMarkRangeNodeSelection(selection, state.schema.marks.link)
: getMarkRange(selection.$from, state.schema.marks.link);
let items: MenuItem[] = [];
let align: "center" | "start" | "end" = "center";
@@ -289,6 +301,7 @@ export function SelectionToolbar(props: Props) {
if (item.name === "linkOnImage" || item.name === "addLink") {
item.onClick = () => {
setAutoFocusLinkInput(true);
setActiveToolbar(Toolbar.Link);
};
}
@@ -315,10 +328,11 @@ export function SelectionToolbar(props: Props) {
>
{activeToolbar === Toolbar.Link ? (
<LinkEditor
key={`${selection.from}-${selection.to}`}
key={`link-${selection.anchor}`}
dictionary={dictionary}
autoFocus={autoFocusLinkInput}
view={view}
mark={link ? link.mark : undefined}
mark={linkMark ? linkMark.mark : undefined}
onLinkAdd={() => setActiveToolbar(null)}
onLinkUpdate={() => setActiveToolbar(null)}
onLinkRemove={() => setActiveToolbar(null)}
@@ -328,7 +342,7 @@ export function SelectionToolbar(props: Props) {
/>
) : activeToolbar === Toolbar.Media ? (
<MediaLinkEditor
key={`embed-${selection.from}`}
key={`embed-${selection.anchor}`}
node={
"node" in selection ? (selection as NodeSelection).node : undefined
}
+1 -1
View File
@@ -887,7 +887,7 @@ export class Editor extends React.PureComponent<
images={this.getLightboxImages()}
activeImage={this.state.activeLightboxImage}
onUpdate={this.updateActiveLightboxImage}
onClose={this.view.focus}
onClose={this.view.focus.bind(this.view)}
/>
)}
</EditorContext.Provider>