Skip to content

Commit 07dbd68

Browse files
authored
avoid account button auth flash (#73)
* avoid account button auth flash * use fade-in account button loading state * hide invisible account button from focus * tidy account button fade state
1 parent 74bf2aa commit 07dbd68

2 files changed

Lines changed: 115 additions & 5 deletions

File tree

e2e/home.spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect, test } from "@playwright/test";
2+
import { HOST_EMAIL, signIn } from "./helpers";
23

34
test("homepage hydrates without chat date mismatches", async ({
45
browser,
@@ -108,3 +109,57 @@ test("homepage drop-off only shows curated featured hosts", async ({
108109
page.getByTestId("homepage-featured-host-demo-newtown-worm-farm")
109110
).toHaveCount(0);
110111
});
112+
113+
test("homepage account button stays hidden while signed-in profile state loads", async ({
114+
page,
115+
}) => {
116+
await signIn(page, { email: HOST_EMAIL, redirectTo: "/profile" });
117+
118+
let resolveProfileRequest = () => {};
119+
const profileRequestStarted = new Promise<void>((resolve) => {
120+
resolveProfileRequest = resolve;
121+
});
122+
let continueProfileRequest = () => {};
123+
const profileRequestCanContinue = new Promise<void>((resolve) => {
124+
continueProfileRequest = resolve;
125+
});
126+
127+
await page.route(/\/rest\/v1\/profiles(?:\?|$)/, async (route) => {
128+
const request = route.request();
129+
130+
if (
131+
request.method() === "GET" &&
132+
request.url().includes("select=first_name")
133+
) {
134+
resolveProfileRequest();
135+
await profileRequestCanContinue;
136+
}
137+
138+
await route.continue();
139+
});
140+
141+
await page.goto("/", { waitUntil: "domcontentloaded" });
142+
await profileRequestStarted;
143+
144+
try {
145+
await expect(page.getByTestId("account-button-profile")).toHaveCount(0);
146+
await expect(page.getByTestId("account-button-sign-in")).toHaveCount(0);
147+
} finally {
148+
continueProfileRequest();
149+
}
150+
151+
const profileAccountButton = page.getByTestId("account-button-profile");
152+
153+
await expect(profileAccountButton).toHaveAttribute("href", "/profile");
154+
await expect(profileAccountButton).toHaveCSS("opacity", "1");
155+
});
156+
157+
test("homepage account button links guests to sign in", async ({ page }) => {
158+
await page.goto("/", { waitUntil: "domcontentloaded" });
159+
160+
await expect(page.getByTestId("account-button-sign-in")).toHaveAttribute(
161+
"href",
162+
"/sign-in"
163+
);
164+
await expect(page.getByTestId("account-button-profile")).toHaveCount(0);
165+
});
Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"use client";
22

3+
import { useEffect, useState } from "react";
34
import { useTranslations } from "next-intl";
5+
import { styled } from "next-yak";
46
import Button from "@/components/Button";
57
import { useAuthUser } from "@/hooks/useAuthUser";
68
import type { LinkButtonProps } from "@/components/Button";
@@ -13,16 +15,69 @@ type AccountButtonProps = Omit<
1315
export default function AccountButton({ ...props }: AccountButtonProps) {
1416
const t = useTranslations("Actions");
1517
const tCommon = useTranslations("Common");
16-
const { user, profileFirstName } = useAuthUser({ includeProfile: true });
18+
const { user, profileFirstName, isLoading } = useAuthUser({
19+
includeProfile: true,
20+
});
21+
const [isReadyToShow, setIsReadyToShow] = useState(false);
1722
const label = profileFirstName?.trim() || tCommon("account");
1823

24+
useEffect(() => {
25+
if (isLoading) {
26+
setIsReadyToShow(false);
27+
return;
28+
}
29+
30+
const animationFrame = window.requestAnimationFrame(() => {
31+
setIsReadyToShow(true);
32+
});
33+
34+
return () => {
35+
window.cancelAnimationFrame(animationFrame);
36+
};
37+
}, [isLoading]);
38+
39+
if (isLoading) {
40+
return null;
41+
}
42+
43+
const hiddenButtonProps = !isReadyToShow
44+
? ({
45+
"aria-hidden": true,
46+
tabIndex: -1,
47+
} satisfies Pick<LinkButtonProps, "aria-hidden" | "tabIndex">)
48+
: {};
49+
1950
return user ? (
20-
<Button href="/profile" variant="secondary" {...props}>
51+
<FadingAccountButton
52+
href="/profile"
53+
variant="secondary"
54+
{...props}
55+
{...hiddenButtonProps}
56+
$visible={isReadyToShow}
57+
data-testid="account-button-profile"
58+
>
2159
{label}
22-
</Button>
60+
</FadingAccountButton>
2361
) : (
24-
<Button href="/sign-in" variant="secondary" {...props}>
62+
<FadingAccountButton
63+
href="/sign-in"
64+
variant="secondary"
65+
{...props}
66+
{...hiddenButtonProps}
67+
$visible={isReadyToShow}
68+
data-testid="account-button-sign-in"
69+
>
2570
{t("signIn")}
26-
</Button>
71+
</FadingAccountButton>
2772
);
2873
}
74+
75+
const FadingAccountButton = styled(Button)<{ $visible: boolean }>`
76+
opacity: ${({ $visible }) => ($visible ? 1 : 0)};
77+
pointer-events: ${({ $visible }) => ($visible ? "auto" : "none")};
78+
transition: opacity 160ms ease-out;
79+
80+
@media (prefers-reduced-motion: reduce) {
81+
transition: none;
82+
}
83+
`;

0 commit comments

Comments
 (0)