Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 40 additions & 31 deletions src/components/ArcOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -303,40 +303,49 @@ export function ArcOverlay({
);
const willCreateFreeTokens = arcSplitFreeRefs !== undefined;
return (
<Button
// The centering offset sits on this wrapper rather than on the button: the platform
// button nudges itself on `:active` through the same translate custom property, so a
// button that centered itself would slide out from under the cursor mid-press and
// drop the click.
<span
key={`split-arc-${phraseId}-${d}`}
aria-label={splitHereLabel}
className={`tw:absolute tw:-translate-x-1/2 tw:-translate-y-1/2 tw:inline-flex tw:h-auto tw:items-center tw:justify-center tw:rounded tw:border tw:bg-background tw:p-px ${buttonZClass} ${buttonColorClass}${willCreateFreeTokens ? ' tw:hover:border-destructive tw:hover:text-destructive' : ''}`}
data-testid="split-arc-btn"
className={`tw:absolute tw:inline-flex tw:-translate-x-1/2 tw:-translate-y-1/2 ${buttonZClass}`}
style={{ left: midX, top: midY }}
tabIndex={-1}
type="button"
variant="ghost"
onClick={() => {
// Clear the split-hover state synchronously with the click. The button is removed
// from the DOM by the resulting re-render, so no mouseLeave fires — without this
// the red "would become free" border would linger until the next mouse move.
setSplitHoveredArc(undefined);
onSplitHoverChange(new Set());
// Also clear the phrase highlight applied for non-freeing splits, for the same
// reason: the button unmounts on click so its mouseLeave never fires.
if (!willCreateFreeTokens) onHoverPhrase(undefined);
onArcSplit(phraseId, splitAfterTokenRef);
}}
onMouseEnter={() => {
if (willCreateFreeTokens) {
handleSplitHoverEnter(phraseId, splitAfterTokenRef, arcSplitFreeRefs);
} else {
handleReshapeHoverEnter(phraseId, splitAfterTokenRef);
}
}}
onMouseLeave={() => {
if (willCreateFreeTokens) handleSplitHoverLeave();
else handleReshapeHoverLeave();
}}
>
<Link2Off className="tw:size-2.5" />
</Button>
<Button
aria-label={splitHereLabel}
className={`tw:inline-flex tw:h-auto tw:items-center tw:justify-center tw:rounded tw:border tw:bg-background tw:p-px ${buttonColorClass}${willCreateFreeTokens ? ' tw:hover:border-destructive tw:hover:text-destructive' : ''}`}
data-testid="split-arc-btn"
tabIndex={-1}
type="button"
variant="ghost"
onClick={() => {
// Clear the split-hover state synchronously with the click. The button is
// removed from the DOM by the resulting re-render, so no mouseLeave fires —
// without this the red "would become free" border would linger until the next
// mouse move.
setSplitHoveredArc(undefined);
onSplitHoverChange(new Set());
// Also clear the phrase highlight applied for non-freeing splits, for the same
// reason: the button unmounts on click so its mouseLeave never fires.
if (!willCreateFreeTokens) onHoverPhrase(undefined);
onArcSplit(phraseId, splitAfterTokenRef);
}}
onMouseEnter={() => {
if (willCreateFreeTokens) {
handleSplitHoverEnter(phraseId, splitAfterTokenRef, arcSplitFreeRefs);
} else {
handleReshapeHoverEnter(phraseId, splitAfterTokenRef);
}
}}
onMouseLeave={() => {
if (willCreateFreeTokens) handleSplitHoverLeave();
else handleReshapeHoverLeave();
}}
>
<Link2Off className="tw:size-2.5" />
</Button>
</span>
);
})}
</>
Expand Down
53 changes: 30 additions & 23 deletions src/components/TokenChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -531,29 +531,36 @@ export function TokenChip({
type="text"
/>
{hasMultipleSuggestions && (
<Button
aria-controls={dropdownShown ? listboxId : undefined}
aria-expanded={dropdownShown}
aria-hidden={!addVisible}
aria-label={formatReplacementString(
localizedStrings['%interlinearizer_tokenChip_showSuggestions%'],
{ token: token.surfaceText },
)}
// Absolutely positioned inside the input's reserved end-padding so it never
// affects layout; we toggle only opacity, fading the button in on focus/hover.
// When hidden it is also made non-interactive so an invisible button can't swallow
// clicks.
className={`tw:absolute tw:right-0.5 tw:top-1/2 tw:flex tw:h-2.5 tw:w-2.5 tw:-translate-y-1/2 tw:items-center tw:justify-center tw:rounded tw:p-0 tw:text-muted-foreground tw:cursor-pointer tw:transition-opacity tw:hover:bg-accent${addVisible ? '' : ' tw:pointer-events-none tw:opacity-0'}`}
data-testid="suggestion-add"
tabIndex={-1}
type="button"
variant="ghost"
onClick={handleAddClick}
// Suppress the mouse-down focus shift so clicking the button never blurs the input.
onMouseDown={(e) => e.preventDefault()}
>
<Plus className="tw:size-2.5" />
</Button>
// Absolutely positioned inside the input's reserved end-padding so it never affects
// layout. The centering offset sits here rather than on the button: the platform
// button nudges itself on `:active` through the same translate custom property, so a
// button that centered itself would slide out from under the cursor mid-press and
// drop the click.
<span className="tw:pointer-events-none tw:absolute tw:right-0.5 tw:top-1/2 tw:flex tw:-translate-y-1/2">
<Button
aria-controls={dropdownShown ? listboxId : undefined}
aria-expanded={dropdownShown}
aria-hidden={!addVisible}
aria-label={formatReplacementString(
localizedStrings['%interlinearizer_tokenChip_showSuggestions%'],
{ token: token.surfaceText },
)}
// Fades in on focus/hover, taking pointer events back from the wrapper only when
// visible so that neither it nor the wrapper's box swallows a click meant for
// the input's end padding.
className={`tw:flex tw:h-2.5 tw:w-2.5 tw:items-center tw:justify-center tw:rounded tw:p-0 tw:text-muted-foreground tw:cursor-pointer tw:transition-opacity tw:hover:bg-accent${addVisible ? ' tw:pointer-events-auto' : ' tw:pointer-events-none tw:opacity-0'}`}
data-testid="suggestion-add"
tabIndex={-1}
type="button"
variant="ghost"
onClick={handleAddClick}
// Suppress the mouse-down focus shift so clicking the button never blurs the
// input.
onMouseDown={(e) => e.preventDefault()}
>
<Plus className="tw:size-2.5" />
</Button>
</span>
)}
</span>
</PopoverAnchor>
Expand Down
Loading