Skip to content

feat(Link): update link#1154

Open
ReFFaT wants to merge 3 commits into
mainfrom
feat-redisign-link
Open

feat(Link): update link#1154
ReFFaT wants to merge 3 commits into
mainfrom
feat-redisign-link

Conversation

@ReFFaT

@ReFFaT ReFFaT commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

refactor(link): rework link creation popup UX

Redesigned the link creation popup (triggered from cursor position) to match the existing link edit tooltip UX.

Changes:

  • LinkForm.tsx — removed Submit/Cancel buttons; replaced with two compact inline inputs (URL with clear + open button, and text with clear); added onUrlChange / onTextChange callbacks
  • LinkForm.scss — new stylesheet matching TooltipView.scss style
  • PlaceholderWidget/widget.tsx — moved submit logic from form to widget: outside click with a filled URL field creates the link; outside click with empty URL cancels; Escape cancels without creating

added saving when changing text in the input

Summary by Sourcery

Rework the link creation and editing UI to use a unified compact two-row form and tooltip-style controls, and update link handling behavior on popup close and outside clicks.

New Features:

  • Introduce a reusable Link form component with URL and link text inputs, clear actions, and an open-link button for use across link creation UIs.
  • Add inline link remove and open actions to the link edit tooltip.

Bug Fixes:

  • Ensure edits to the link text input are saved when submitting or closing the link creation popup.

Enhancements:

  • Align the link edit tooltip styling with the new compact link form design.
  • Change link placeholder behavior so that closing the popup with a non-empty URL creates the link, while closing with an empty URL cancels it.
  • Track the current URL in the selection tooltip to allow saving changes when the popup is closed via outside click or toolbar interactions.

@sourcery-ai

sourcery-ai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Reviewer's Guide

Refactors link creation/editing UX by replacing the old form-based link component with compact inline link inputs (for both tooltip and placeholder popup), adds a reusable Link form component with URL/text handling, and moves link creation/cancellation logic from the form into the surrounding editor plugins/widgets, including new behaviors for outside clicks, escape, undo/redo, and text saving.

File-Level Changes

Change Details Files
Refactor link tooltip view to a compact inline URL editor with open/remove actions and state lifted into the plugin.
  • Replace form-based LinkForm with a simple Link component that renders a single TextInputFixed and open/remove icon buttons
  • Change enter handling from onKeyPress to onKeyDown using enterKeyHandler
  • Track current URL in the SelectionTooltip plugin and update link attributes or remove link on outside click instead of via form submit
  • Handle escape key and undo/redo toolbar clicks distinctly in onOpenChange to cancel or hide tooltip appropriately
  • Wire new onRemove and onUrlChange callbacks through SelectionTooltipView into the plugin logic
  • Add TooltipView.scss with layout for the inline tooltip editor
packages/editor/src/extensions/markdown/Link/plugins/LinkTooltipPlugin/TooltipView.tsx
packages/editor/src/extensions/markdown/Link/plugins/LinkTooltipPlugin/index.tsx
packages/editor/src/extensions/markdown/Link/plugins/LinkTooltipPlugin/TooltipView.scss
Introduce a reusable Link form component for link creation with URL and text fields and updated popup behavior in the placeholder widget.
  • Create a new forms/Link component that manages url/text local state, exposes onSubmit/onUrlChange/onTextChange, and provides compact inline URL and text inputs with an open-link button
  • Add Link.scss styling for the new Link form layout
  • Update LinkPlaceholderWidget to use the new Link component instead of LinkForm, tracking current URL/text via refs
  • Move submit/cancel behavior into LinkPlaceholderWidget: escape cancels, outside click with non-empty URL submits with current text, outside click with empty URL cancels
  • Ensure that typing in the text input is saved by propagating changes via onTextChange
packages/editor/src/extensions/markdown/Link/PlaceholderWidget/widget.tsx
packages/editor/src/forms/Link.tsx
packages/editor/src/forms/Link.scss
Remove deprecated link form and URL row components and adjust i18n as needed.
  • Delete old LinkForm and UrlInputRow components and their styles, since the new Link component replaces them
  • Update forms i18n JSON files to support new link-related helper texts and placeholders (e.g., link_open_help, link_text)
packages/editor/src/forms/LinkForm.tsx
packages/editor/src/forms/UrlInputRow.scss
packages/editor/src/forms/UrlInputRow.tsx
packages/editor/src/i18n/forms/en.json
packages/editor/src/i18n/forms/ru.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • In SelectionTooltip, clicking the unlink button calls removeLink, but the subsequent onOpenChangeonOutsideClick path will still use currentUrl and can immediately recreate the link; consider short‑circuiting onOpenChange when manualHidden is true or when the close was triggered by the unlink button.
  • The .g-popup_open selector in TooltipView.scss globally changes the border radius for all popups; if the intention is to style only the link tooltip, consider scoping this rule to the tooltip popup (e.g., via a more specific container class) to avoid affecting unrelated popups.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `SelectionTooltip`, clicking the unlink button calls `removeLink`, but the subsequent `onOpenChange``onOutsideClick` path will still use `currentUrl` and can immediately recreate the link; consider short‑circuiting `onOpenChange` when `manualHidden` is true or when the close was triggered by the unlink button.
- The `.g-popup_open` selector in `TooltipView.scss` globally changes the border radius for all popups; if the intention is to style only the link tooltip, consider scoping this rule to the tooltip popup (e.g., via a more specific container class) to avoid affecting unrelated popups.

## Individual Comments

### Comment 1
<location path="packages/editor/src/extensions/markdown/Link/plugins/LinkTooltipPlugin/TooltipView.tsx" line_range="36-38" />
<code_context>
-    const inputEnterKeyHandler: TextInputProps['onKeyPress'] = enterKeyHandler(handleSubmit);
+    const inputEnterKeyHandler: TextInputProps['onKeyDown'] = enterKeyHandler(handleSubmit);
+
+    useEffect(() => {
+        onUrlChange?.(href);
+    }, [href, onUrlChange]);

     return (
</code_context>
<issue_to_address>
**issue (bug_risk):** The local `url` state is not updated when `href` changes, which can desync the input from the selected link.

`useState(href)` only uses `href` as the initial value. If this tooltip is reused for another link or attrs change, `href` will update but the input will still show the old URL. Also update `url` when `href` changes, e.g.

```ts
useEffect(() => {
  setUrl(href);
  onUrlChange?.(href);
}, [href, onUrlChange]);
```
</issue_to_address>

### Comment 2
<location path="packages/editor/src/extensions/markdown/Link/plugins/LinkTooltipPlugin/TooltipView.tsx" line_range="60-64" />
<code_context>
+                </ActionTooltip>
+            )}
+            <ActionTooltip title={i18n('link_open_help')}>
+                <Button className={b('button')} view="flat" size="m" href={url} target="_blank">
+                    <Icon data={LinkIcon} size={16} />
+                </Button>
</code_context>
<issue_to_address>
**🚨 suggestion (security):** Opening links in a new tab should include `rel` attributes to avoid `window.opener` security issues.

Because this renders an anchor with `target="_blank"`, please also add `rel="noopener noreferrer"` (or confirm the UI kit applies it automatically) to prevent `window.opener` abuse:

```tsx
<Button
    className={b('button')}
    view="flat"
    size="m"
    href={url}
    target="_blank"
    rel="noopener noreferrer"
>
    <Icon data={LinkIcon} size={16} />
</Button>
```

```suggestion
            <ActionTooltip title={i18n('link_open_help')}>
                <Button
                    className={b('button')}
                    view="flat"
                    size="m"
                    href={url}
                    target="_blank"
                    rel="noopener noreferrer"
                >
                    <Icon data={LinkIcon} size={16} />
                </Button>
            </ActionTooltip>
```
</issue_to_address>

### Comment 3
<location path="packages/editor/src/extensions/markdown/Link/plugins/LinkTooltipPlugin/index.tsx" line_range="195-185" />
<code_context>
+        }
+
+        const url = currentUrlRef.current.trim();
+        if (url) {
+            onSubmit({url, text: currentTextRef.current});
+        } else {
</code_context>
<issue_to_address>
**suggestion:** `removeLink` and `onOutsideClick` both hide the tooltip and set `manualHidden`, causing redundant calls.

When `url` is empty, `onOutsideClick` calls `removeLink()`, which already sets `manualHidden = true` and calls `hideTooltip()`, and then `onOutsideClick` repeats both. While harmless, it’s confusing. Consider either:

- Letting `onOutsideClick` only choose between `changeAttrs` and `removeLink`, and have those handle hiding, or
- Centralizing all hiding/manual state in `onOutsideClick` and keeping `removeLink` focused on document changes.

Suggested implementation:

```typescript
    private onOutsideClick = () => {
        const url = this.currentUrl.trim();

        if (!url) {
            this.removeLink();
            return;
        }

        const target = (event as MouseEvent | undefined)?.target as HTMLElement | null;
        const isUndoRedoClick = Boolean(
            target?.closest('[data-toolbar-item="undo"], [data-toolbar-item="redo"]'),
        );

```

This change ensures that when `url` is empty, `onOutsideClick` delegates to `removeLink()` and returns early, so `manualHidden` and `hideTooltip` are not also called in `onOutsideClick`. This removes the redundant behavior specifically in the empty-URL path that you highlighted, assuming `removeLink()` already handles hiding and `manualHidden`. No further changes are required unless `removeLink()` currently does *not* manage those responsibilities, in which case that method should be updated accordingly.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@gravity-ui

gravity-ui Bot commented Jun 22, 2026

Copy link
Copy Markdown

Storybook Deployed

@gravity-ui

gravity-ui Bot commented Jun 22, 2026

Copy link
Copy Markdown

🎭 Playwright Report

@ReFFaT ReFFaT force-pushed the feat-redisign-link branch from 9a66a32 to 9379f4c Compare June 22, 2026 07:41
@ReFFaT ReFFaT force-pushed the feat-redisign-link branch from 282ffc3 to d908949 Compare June 22, 2026 08:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant