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
10 changes: 7 additions & 3 deletions desktop/src/features/profile/ui/ProfileAvatarEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,11 @@ export function ProfileAvatarEditor({
) : (
<div className="relative grid content-start gap-3">
<div
className="buzz-emoji-mart relative z-0 h-[316px] overflow-hidden rounded-xl bg-muted transition-colors duration-[250ms] ease-out"
className={cn(
"buzz-emoji-mart relative z-0 overflow-hidden rounded-xl bg-muted transition-colors duration-[250ms] ease-out",
isOnboardingModal ? "h-[316px]" : "h-[384px]",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P1 — Keep the onboarding CTA clear of the taller picker. AvatarStep uses the default presentation, so this branch increases its picker from 316px to 384px as well as adding the 68px search row. At the 1280×720 viewport in the PR's own onboarding screenshot, the picker ends around y=562 while the Next button starts around y=558; the picker visibly paints over the top of the CTA. That is a user-facing regression on a supported desktop-sized viewport, and the new E2E test currently locks in 384px without asserting that the actions remain visible and non-overlapping. Please make the height/layout responsive (or give this onboarding surface an appropriate presentation-specific height) and add a bounding-box/non-overlap assertion for the actions.

)}
data-testid={`${testIdPrefix}-emoji-picker`}
ref={emojiPickerContainerRef}
style={emojiMartThemeVars}
>
Expand Down Expand Up @@ -788,9 +792,9 @@ export function ProfileAvatarEditor({
applyEmojiAvatar(emoji.native, nextColor);
}}
previewPosition="none"
searchPosition="none"
searchPosition="sticky"
set="native"
skinTonePosition="none"
skinTonePosition="search"
theme={emojiPickerTheme}
/>
</div>
Expand Down
48 changes: 44 additions & 4 deletions desktop/src/features/profile/ui/ProfileAvatarEditor.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const EMOJI_MART_SHADOW_CSS = `

#root {
--padding: var(--buzz-emoji-picker-padding, 16px);
--buzz-emoji-picker-search-control-height: 48px;
--sidebar-width: 0px;
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -127,6 +128,47 @@ const EMOJI_MART_SHADOW_CSS = `
display: none;
}

/* Match the app's member-search controls: a distinct resting surface and
* border make both the emoji search and its adjacent skin-tone control easy
* to find before either receives focus. */
.search input[type="search"],
.search + .flex {
background-color: rgb(var(--em-rgb-input));
box-shadow: inset 0 0 0 1px rgba(var(--em-rgb-color), 0.16);
}

.search input[type="search"] {
border-radius: 12px;
height: var(--buzz-emoji-picker-search-control-height);
padding-bottom: 0;
padding-top: 0;
}

.search input[type="search"]:focus {
box-shadow: inset 0 0 0 1px rgb(var(--em-rgb-accent));
}

.search + .flex {
border-radius: 12px;
flex: 0 0 auto;
height: var(--buzz-emoji-picker-search-control-height) !important;
margin-left: 8px;
width: var(--buzz-emoji-picker-search-control-height) !important;
}

.skin-tone-button {
background-color: transparent !important;
border: 0 !important;
border-radius: 8px;
box-shadow: none !important;
height: calc(var(--buzz-emoji-picker-search-control-height) - 8px) !important;
width: calc(var(--buzz-emoji-picker-search-control-height) - 8px) !important;
}

.skin-tone-button[aria-selected] {
background-color: transparent !important;
}

.category button .background {
background-color: transparent;
transition: background-color var(--duration) var(--easing);
Expand Down Expand Up @@ -593,14 +635,12 @@ export function useEmojiMartThemeVars() {
const foreground = hslToRgbString(
styles.getPropertyValue("--foreground"),
);
const background = hslToRgbString(
styles.getPropertyValue("--background"),
);
const input = hslToRgbString(styles.getPropertyValue("--input"));

setThemeVars({
"--buzz-emoji-picker-rgb-background": muted ?? "54, 58, 79",
"--buzz-emoji-picker-rgb-color": foreground ?? "245, 247, 255",
"--buzz-emoji-picker-rgb-input": background ?? "47, 51, 68",
"--buzz-emoji-picker-rgb-input": input ?? "47, 51, 68",
} as React.CSSProperties);
};

Expand Down
37 changes: 37 additions & 0 deletions desktop/tests/e2e/onboarding-avatar-skip.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { expect, test } from "@playwright/test";

import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
import { waitForAnimations } from "../helpers/animations";
import { expectEmojiMartStylesInstalled } from "../helpers/css";
import { seedActiveIdentity } from "../helpers/onboarding";

const BLANK_TYLER_IDENTITY = {
Expand Down Expand Up @@ -38,6 +39,42 @@ test("avatar step always shows Skip for now button without an error", async ({
});
});

test("avatar step shares the profile emoji picker controls", async ({
page,
}) => {
await seedActiveIdentity(page, BLANK_TYLER_IDENTITY);
await installMockBridge(page, undefined, { skipOnboardingSeed: true });
await page.goto("/");

await page.getByTestId("onboarding-display-name").fill("Morty QA");
await page.getByTestId("onboarding-next").click();
await page.getByRole("tab", { name: "Emoji" }).click();

const picker = page.locator("em-emoji-picker");
await expect(picker.locator("input[type='search']")).toBeVisible();
await expectEmojiMartStylesInstalled(picker);
await expect(page.getByTestId("onboarding-avatar-emoji-picker")).toHaveCSS(
"height",
"384px",
Comment thread
klopez4212 marked this conversation as resolved.
);

const controlHeights = await picker.evaluate((element) => {
const input = element.shadowRoot?.querySelector<HTMLInputElement>(
'input[type="search"]',
);
const toneControl =
element.shadowRoot?.querySelector<HTMLElement>(".search + .flex");
if (!input || !toneControl) {
throw new Error("Onboarding emoji picker controls did not render.");
}
return {
input: input.getBoundingClientRect().height,
tone: toneControl.getBoundingClientRect().height,
};
});
expect(controlHeights).toEqual({ input: 48, tone: 48 });
});

test("avatar step skip button completes community profile setup", async ({
page,
}) => {
Expand Down
76 changes: 76 additions & 0 deletions desktop/tests/e2e/profile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
installMockBridge,
TEST_IDENTITIES,
} from "../helpers/bridge";
import { expectEmojiMartStylesInstalled } from "../helpers/css";
import { openProfileMenu, openSettings } from "../helpers/settings";

async function expectHomeView(page: import("@playwright/test").Page) {
Expand Down Expand Up @@ -585,6 +586,81 @@ test("renders emoji avatars with a static background layer", async ({
);
});

test("offers emoji search and skin-tone controls for profile avatars", async ({
page,
}) => {
await page.goto("/");

await openSettings(page, "profile");
await page.getByTestId("profile-avatar-edit").click();
await page.getByRole("tab", { name: "Emoji" }).click();

const picker = page.locator("em-emoji-picker");
const searchInput = picker.locator("input[type='search']");
await expect(searchInput).toBeVisible();
await expectEmojiMartStylesInstalled(picker);
await expect(page.getByTestId("profile-avatar-emoji-picker")).toHaveCSS(
"height",
"384px",
);

// Emoji Mart renders the tone selector alongside its search field when
// `skinTonePosition` is `search`. Keeping it in the same shadow-DOM region
// ensures profile avatars match the regular in-app emoji picker.
const hasSkinToneControl = await picker.evaluate((element) => {
const controls = element.shadowRoot?.querySelectorAll("button") ?? [];
return Array.from(controls).some((button) =>
/skin tone/i.test(button.getAttribute("aria-label") ?? ""),
);
});
expect(hasSkinToneControl).toBe(true);

const controlColors = await picker.evaluate((element) => {
const root = element.shadowRoot?.querySelector<HTMLElement>("#root");
const input = element.shadowRoot?.querySelector<HTMLInputElement>(
'input[type="search"]',
);
const toneControl =
element.shadowRoot?.querySelector<HTMLElement>(".search + .flex");
if (!root || !input || !toneControl) {
throw new Error("Profile emoji picker controls did not render.");
}
return {
input: getComputedStyle(input).backgroundColor,
picker: getComputedStyle(root).backgroundColor,
tone: getComputedStyle(toneControl).backgroundColor,
};
});
expect(controlColors.input).not.toBe(controlColors.picker);
expect(controlColors.tone).not.toBe(controlColors.picker);

const controlHeights = await picker.evaluate((element) => {
const input = element.shadowRoot?.querySelector<HTMLInputElement>(
'input[type="search"]',
);
const toneControl =
element.shadowRoot?.querySelector<HTMLElement>(".search + .flex");
const toneButton =
element.shadowRoot?.querySelector<HTMLElement>(".skin-tone-button");
if (!input || !toneControl || !toneButton) {
throw new Error("Profile emoji picker controls did not render.");
}
input.focus();
return {
inputHeight: input.getBoundingClientRect().height,
inputShadow: getComputedStyle(input).boxShadow,
toneButtonBorder: getComputedStyle(toneButton).borderTopWidth,
toneButtonShadow: getComputedStyle(toneButton).boxShadow,
toneHeight: toneControl.getBoundingClientRect().height,
};
});
expect(controlHeights.inputHeight).toBe(48);
expect(controlHeights.toneHeight).toBe(48);
expect(controlHeights.inputShadow).toMatch(/inset$/);
expect(controlHeights.toneButtonBorder).toBe("0px");
expect(controlHeights.toneButtonShadow).toBe("none");
});

test("reveals emoji background colors only after choosing an emoji", async ({
page,
}) => {
Expand Down
20 changes: 20 additions & 0 deletions desktop/tests/helpers/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,23 @@ export async function expectSmoothCorners(
)
.toBe(true);
}

/**
* Waits for the Buzz emoji-picker overrides to be present in the Emoji Mart
* shadow root.
*
* The stylesheet is appended from a `requestAnimationFrame` loop that starts
* only once the `em-emoji-picker` custom element has attached its shadow root,
* so it can land a frame or more after the search input becomes visible. Any
* assertion on an overridden property (control heights, resting background)
* must wait for this, otherwise it can read Emoji Mart's un-overridden defaults.
*/
export async function expectEmojiMartStylesInstalled(picker: Locator) {
await expect
.poll(async () =>
picker.evaluate((element) =>
Boolean(element.shadowRoot?.querySelector("#buzz-emoji-mart-style")),
),
)
.toBe(true);
}