mirror of
https://github.com/block/buzz.git
synced 2026-10-31 22:41:12 +00:00
Add search and skin tones to profile emoji picker
Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
@@ -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]",
|
||||
)}
|
||||
data-testid={`${testIdPrefix}-emoji-picker`}
|
||||
ref={emojiPickerContainerRef}
|
||||
style={emojiMartThemeVars}
|
||||
>
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -38,6 +38,41 @@ 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 expect(page.getByTestId("onboarding-avatar-emoji-picker")).toHaveCSS(
|
||||
"height",
|
||||
"384px",
|
||||
);
|
||||
|
||||
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,
|
||||
}) => {
|
||||
|
||||
@@ -585,6 +585,80 @@ 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 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,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user