authorgravatar for 97427849+flamboh@users.noreply.github.comoliver <97427849+flamboh@users.noreply.github.com> 2026-10-13 10:34:46-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-13 12:34:46-05:00
log6a68bee34f5ecfd478b6e2bd0af2ce39c0c32583
tree28345b4ef30acd42e802a15d5f3bd7a7e21b47c6
parent05e4f888a0d8591c790d036140080092cf024d30
signature Signed by PGP key B5690EEEBB952194

fix(web): merge button fits its label and animates width changes (#18478)


2 files changed, 53 insertions(+), 14 deletions(-)

apps/web/src/components/AnimatedWidth.tsx created+39
......@@ -0,0 +1,39 @@
1import { type ReactNode, useLayoutEffect, useRef, useState } from "react";
2
3import { observeResize } from "~/lib/observeResize";
4import { cn } from "~/lib/utils";
5
6export function AnimatedWidth({
7 children,
8 className,
9}: {
10 readonly children: ReactNode;
11 readonly className?: string;
12}) {
13 const contentRef = useRef<HTMLSpanElement>(null);
14 const [width, setWidth] = useState<number | null>(null);
15
16 useLayoutEffect(() => {
17 const element = contentRef.current;
18 if (!element) return;
19 return observeResize(element, () => {
20 const nextWidth = Math.ceil(element.getBoundingClientRect().width);
21 if (nextWidth > 0) setWidth(nextWidth);
22 });
23 }, []);
24
25 return (
26 <span
27 data-slot="animated-width"
28 className={cn(
29 "inline-block overflow-hidden transition-[width] duration-150 ease-out motion-reduce:transition-none",
30 className,
31 )}
32 style={width === null ? undefined : { width }}
33 >
34 <span ref={contentRef} className="block w-max whitespace-nowrap">
35 {children}
36 </span>
37 </span>
38 );
39}
apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx+14-14
......@@ -2,6 +2,7 @@ import { parseChangeRequestUrl } from "@t3tools/shared/changeRequestUrl";
22import { useAtomValue } from "@effect/atom-react";
33import { usePullRequestStack } from "~/state/usePullRequestStack";
44import { RefreshIcon } from "~/components/ui/refresh-icon";
5import { AnimatedWidth } from "~/components/AnimatedWidth";
56import {
67 scopedThreadKey,
78 scopeProjectRef,
......@@ -2122,21 +2123,20 @@ export function PullRequestDetailPanel({
21222123 ) : (
21232124 <PullRequestGlyph.merged aria-hidden className="size-3.5" />
21242125 )}
2125 <span className="inline-grid @max-[30rem]/pr-header:hidden">
2126 <span aria-hidden className="invisible col-start-1 row-start-1">
2127 {selectedMergeMethodLabel}
2128 </span>
2129 <span aria-hidden className="invisible col-start-1 row-start-1">
2130 Confirm merge?
2126 <AnimatedWidth className="@max-[30rem]/pr-header:hidden">
2127 <span className="grid">
2128 <span aria-hidden className="invisible col-start-1 row-start-1">
2129 {selectedMergeMethodLabel}
2130 </span>
2131 <span className="col-start-1 row-start-1">
2132 {pendingAction === "merge"
2133 ? "Merging..."
2134 : inlineConfirm.isArmed("merge")
2135 ? "Confirm merge?"
2136 : selectedMergeMethodLabel}
2137 </span>
21312138 </span>
2132 <span className="col-start-1 row-start-1">
2133 {pendingAction === "merge"
2134 ? "Merging..."
2135 : inlineConfirm.isArmed("merge")
2136 ? "Confirm merge?"
2137 : selectedMergeMethodLabel}
2138 </span>
2139 </span>
2139 </AnimatedWidth>
21402140 </Button>
21412141 </span>
21422142 }