From 248f0822ad27290cf7fcf1ada61a11ed45ed3366 Mon Sep 17 00:00:00 2001 From: Brian Stephan Date: Thu, 17 Sep 2026 14:06:21 -0400 Subject: [PATCH 1/9] feat: expose text color fields --- .../locales/platform/cs/visual-editor.json | 1 + .../locales/platform/da/visual-editor.json | 1 + .../locales/platform/de/visual-editor.json | 1 + .../locales/platform/en-GB/visual-editor.json | 1 + .../locales/platform/en/visual-editor.json | 1 + .../locales/platform/es/visual-editor.json | 1 + .../locales/platform/et/visual-editor.json | 1 + .../locales/platform/fi/visual-editor.json | 1 + .../locales/platform/fr/visual-editor.json | 1 + .../locales/platform/hr/visual-editor.json | 1 + .../locales/platform/hu/visual-editor.json | 1 + .../locales/platform/it/visual-editor.json | 1 + .../locales/platform/ja/visual-editor.json | 1 + .../locales/platform/lt/visual-editor.json | 1 + .../locales/platform/lv/visual-editor.json | 1 + .../locales/platform/nb/visual-editor.json | 1 + .../locales/platform/nl/visual-editor.json | 1 + .../locales/platform/pl/visual-editor.json | 1 + .../locales/platform/pt/visual-editor.json | 1 + .../locales/platform/ro/visual-editor.json | 1 + .../locales/platform/sk/visual-editor.json | 1 + .../locales/platform/sv/visual-editor.json | 1 + .../locales/platform/tr/visual-editor.json | 1 + .../locales/platform/zh-TW/visual-editor.json | 1 + .../locales/platform/zh/visual-editor.json | 1 + .../src/components/atoms/cta.test.ts | 123 ++++++++++++++ .../src/components/atoms/cta.tsx | 8 +- .../src/components/atoms/hoursStatus.tsx | 7 +- .../src/components/atoms/phone.test.ts | 125 +++++++++++++++ .../src/components/atoms/phone.tsx | 22 ++- .../src/components/atoms/timestamp.tsx | 10 +- .../src/components/contentBlocks/Address.tsx | 28 +++- .../src/components/contentBlocks/CTAGroup.tsx | 8 + .../components/contentBlocks/CtaWrapper.tsx | 13 ++ .../components/contentBlocks/Emails.test.ts | 104 ++++++++++++ .../src/components/contentBlocks/Emails.tsx | 13 +- .../contentBlocks/GetDirections.tsx | 15 +- .../components/contentBlocks/HoursStatus.tsx | 12 +- .../components/contentBlocks/HoursTable.tsx | 14 +- .../src/components/contentBlocks/Phone.tsx | 16 ++ .../components/contentBlocks/PhoneList.tsx | 2 + .../src/components/contentBlocks/TextList.tsx | 13 +- .../components/contentBlocks/Timestamp.tsx | 9 ++ .../footer/CopyrightMessageSlot.tsx | 10 ++ .../footer/FooterExpandedLinksWrapper.test.ts | 92 +++++++++++ .../src/components/index.test.ts | 151 ++++++++++++++++++ .../components/locator/LocatorResultCard.tsx | 31 +++- .../src/components/locator/Results.test.ts | 83 ++++++++++ .../src/components/locator/Results.tsx | 10 ++ .../NearbyLocations/NearbyLocationCard.tsx | 4 +- .../NearbyLocationsCardsWrapper.tsx | 7 + .../ReviewsSection/ReviewsSection.tsx | 17 +- packages/visual-editor/src/docs/components.md | 64 ++++---- 53 files changed, 986 insertions(+), 50 deletions(-) create mode 100644 packages/visual-editor/src/components/atoms/cta.test.ts create mode 100644 packages/visual-editor/src/components/atoms/phone.test.ts create mode 100644 packages/visual-editor/src/components/contentBlocks/Emails.test.ts create mode 100644 packages/visual-editor/src/components/footer/FooterExpandedLinksWrapper.test.ts create mode 100644 packages/visual-editor/src/components/index.test.ts create mode 100644 packages/visual-editor/src/components/locator/Results.test.ts diff --git a/packages/visual-editor/locales/platform/cs/visual-editor.json b/packages/visual-editor/locales/platform/cs/visual-editor.json index 478378219b..a780488b92 100644 --- a/packages/visual-editor/locales/platform/cs/visual-editor.json +++ b/packages/visual-editor/locales/platform/cs/visual-editor.json @@ -275,6 +275,7 @@ "key": "Klíč", "keywordsDisplayName": "Zobrazovaný název klíčových slov", "label": "Označení", + "labelColor": "Barva označení", "latitude": "Zeměpisná šířka", "limit": "Omezit", "link": "Odkaz", diff --git a/packages/visual-editor/locales/platform/da/visual-editor.json b/packages/visual-editor/locales/platform/da/visual-editor.json index 730e6d100a..5a83e239b3 100644 --- a/packages/visual-editor/locales/platform/da/visual-editor.json +++ b/packages/visual-editor/locales/platform/da/visual-editor.json @@ -275,6 +275,7 @@ "key": "Nøgle", "keywordsDisplayName": "Søgeord Vist navn", "label": "Mærke", + "labelColor": "Mærkefarve", "latitude": "Breddegrad", "limit": "Begrænse", "link": "Forbindelse", diff --git a/packages/visual-editor/locales/platform/de/visual-editor.json b/packages/visual-editor/locales/platform/de/visual-editor.json index c37425bef4..ab91a051e0 100644 --- a/packages/visual-editor/locales/platform/de/visual-editor.json +++ b/packages/visual-editor/locales/platform/de/visual-editor.json @@ -275,6 +275,7 @@ "key": "Schlüssel", "keywordsDisplayName": "Anzeigename der Schlüsselwörter", "label": "Bezeichnung", + "labelColor": "Bezeichnungsfarbe", "latitude": "Breitengrad", "limit": "Limit", "link": "Link", diff --git a/packages/visual-editor/locales/platform/en-GB/visual-editor.json b/packages/visual-editor/locales/platform/en-GB/visual-editor.json index b0cc9408b9..4d5f942e48 100644 --- a/packages/visual-editor/locales/platform/en-GB/visual-editor.json +++ b/packages/visual-editor/locales/platform/en-GB/visual-editor.json @@ -275,6 +275,7 @@ "key": "Key", "keywordsDisplayName": "Keywords Display Name", "label": "Label", + "labelColor": "Label Color", "latitude": "Latitude", "limit": "Limit", "link": "Link", diff --git a/packages/visual-editor/locales/platform/en/visual-editor.json b/packages/visual-editor/locales/platform/en/visual-editor.json index 9cc235ee4f..4c3cc83297 100644 --- a/packages/visual-editor/locales/platform/en/visual-editor.json +++ b/packages/visual-editor/locales/platform/en/visual-editor.json @@ -275,6 +275,7 @@ "key": "Key", "keywordsDisplayName": "Keywords Display Name", "label": "Label", + "labelColor": "Label Color", "latitude": "Latitude", "limit": "Limit", "link": "Link", diff --git a/packages/visual-editor/locales/platform/es/visual-editor.json b/packages/visual-editor/locales/platform/es/visual-editor.json index 100285408a..af1e17df23 100644 --- a/packages/visual-editor/locales/platform/es/visual-editor.json +++ b/packages/visual-editor/locales/platform/es/visual-editor.json @@ -275,6 +275,7 @@ "key": "Clave", "keywordsDisplayName": "Palabras clave Nombre para mostrar", "label": "Etiqueta", + "labelColor": "Color de la etiqueta", "latitude": "Latitud", "limit": "Límite", "link": "Enlace", diff --git a/packages/visual-editor/locales/platform/et/visual-editor.json b/packages/visual-editor/locales/platform/et/visual-editor.json index 2ffc05e39e..3045e230bd 100644 --- a/packages/visual-editor/locales/platform/et/visual-editor.json +++ b/packages/visual-editor/locales/platform/et/visual-editor.json @@ -275,6 +275,7 @@ "key": "Võti", "keywordsDisplayName": "Märksõnad Kuvatav nimi", "label": "Silt", + "labelColor": "Sildi värv", "latitude": "Laius", "limit": "Piiranguid", "link": "Link", diff --git a/packages/visual-editor/locales/platform/fi/visual-editor.json b/packages/visual-editor/locales/platform/fi/visual-editor.json index f46f8bb818..eddcb12251 100644 --- a/packages/visual-editor/locales/platform/fi/visual-editor.json +++ b/packages/visual-editor/locales/platform/fi/visual-editor.json @@ -275,6 +275,7 @@ "key": "Avain", "keywordsDisplayName": "Avainsanat Näyttönimi", "label": "Merkitä", + "labelColor": "Merkinnän väri", "latitude": "Leveysaste", "limit": "Rajoittaa", "link": "Linkki", diff --git a/packages/visual-editor/locales/platform/fr/visual-editor.json b/packages/visual-editor/locales/platform/fr/visual-editor.json index 7995f345dc..01c4f09e51 100644 --- a/packages/visual-editor/locales/platform/fr/visual-editor.json +++ b/packages/visual-editor/locales/platform/fr/visual-editor.json @@ -275,6 +275,7 @@ "key": "Clé", "keywordsDisplayName": "Nom d'affichage des mots clés", "label": "Étiquette", + "labelColor": "Couleur de l'étiquette", "latitude": "Latitude", "limit": "Limite", "link": "Lien", diff --git a/packages/visual-editor/locales/platform/hr/visual-editor.json b/packages/visual-editor/locales/platform/hr/visual-editor.json index af68b3416b..1a4e6c8e59 100644 --- a/packages/visual-editor/locales/platform/hr/visual-editor.json +++ b/packages/visual-editor/locales/platform/hr/visual-editor.json @@ -275,6 +275,7 @@ "key": "Ključ", "keywordsDisplayName": "Naziv za prikaz ključnih riječi", "label": "Označiti", + "labelColor": "Boja oznake", "latitude": "Širina", "limit": "Ograničiti", "link": "Link", diff --git a/packages/visual-editor/locales/platform/hu/visual-editor.json b/packages/visual-editor/locales/platform/hu/visual-editor.json index 930883970d..8afc28ba7e 100644 --- a/packages/visual-editor/locales/platform/hu/visual-editor.json +++ b/packages/visual-editor/locales/platform/hu/visual-editor.json @@ -275,6 +275,7 @@ "key": "Kulcsfontosságú", "keywordsDisplayName": "Kulcsszavak Megjelenített név", "label": "Címke", + "labelColor": "Címke színe", "latitude": "Szélesség", "limit": "Határ", "link": "Link", diff --git a/packages/visual-editor/locales/platform/it/visual-editor.json b/packages/visual-editor/locales/platform/it/visual-editor.json index 4f315b4034..5a9198a0f5 100644 --- a/packages/visual-editor/locales/platform/it/visual-editor.json +++ b/packages/visual-editor/locales/platform/it/visual-editor.json @@ -275,6 +275,7 @@ "key": "Chiave", "keywordsDisplayName": "Nome visualizzato delle parole chiave", "label": "Etichetta", + "labelColor": "Colore dell'etichetta", "latitude": "Latitudine", "limit": "Limite", "link": "Collegamento", diff --git a/packages/visual-editor/locales/platform/ja/visual-editor.json b/packages/visual-editor/locales/platform/ja/visual-editor.json index 8c2f7fb44f..b59265212e 100644 --- a/packages/visual-editor/locales/platform/ja/visual-editor.json +++ b/packages/visual-editor/locales/platform/ja/visual-editor.json @@ -275,6 +275,7 @@ "key": "キー", "keywordsDisplayName": "キーワードの表示名", "label": "ラベル", + "labelColor": "ラベルの色", "latitude": "緯度", "limit": "制限", "link": "リンク", diff --git a/packages/visual-editor/locales/platform/lt/visual-editor.json b/packages/visual-editor/locales/platform/lt/visual-editor.json index ebb5157dce..4d78e14351 100644 --- a/packages/visual-editor/locales/platform/lt/visual-editor.json +++ b/packages/visual-editor/locales/platform/lt/visual-editor.json @@ -275,6 +275,7 @@ "key": "Raktas", "keywordsDisplayName": "Raktiniai žodžiai Rodomas pavadinimas", "label": "Etiketė", + "labelColor": "Etiketės spalva", "latitude": "Platuma", "limit": "Limit", "link": "Nuoroda", diff --git a/packages/visual-editor/locales/platform/lv/visual-editor.json b/packages/visual-editor/locales/platform/lv/visual-editor.json index 4c63efe5ca..eaf93d4eeb 100644 --- a/packages/visual-editor/locales/platform/lv/visual-editor.json +++ b/packages/visual-editor/locales/platform/lv/visual-editor.json @@ -275,6 +275,7 @@ "key": "Atslēga", "keywordsDisplayName": "Atslēgvārdi Parādāmais nosaukums", "label": "Etiķete", + "labelColor": "Etiķetes krāsa", "latitude": "Platums", "limit": "Ierobežot", "link": "Saite", diff --git a/packages/visual-editor/locales/platform/nb/visual-editor.json b/packages/visual-editor/locales/platform/nb/visual-editor.json index 0328f3cd5b..68fc76da15 100644 --- a/packages/visual-editor/locales/platform/nb/visual-editor.json +++ b/packages/visual-editor/locales/platform/nb/visual-editor.json @@ -275,6 +275,7 @@ "key": "Nøkkel", "keywordsDisplayName": "Visningsnavn for søkeord", "label": "Merkelapp", + "labelColor": "Merkelappfarge", "latitude": "Breddegrad", "limit": "Begrense", "link": "Lenke", diff --git a/packages/visual-editor/locales/platform/nl/visual-editor.json b/packages/visual-editor/locales/platform/nl/visual-editor.json index 695957fddc..492fc997ec 100644 --- a/packages/visual-editor/locales/platform/nl/visual-editor.json +++ b/packages/visual-editor/locales/platform/nl/visual-editor.json @@ -275,6 +275,7 @@ "key": "Sleutel", "keywordsDisplayName": "Trefwoorden Weergavenaam", "label": "Label", + "labelColor": "Labelkleur", "latitude": "Breedte", "limit": "Beperken", "link": "Link", diff --git a/packages/visual-editor/locales/platform/pl/visual-editor.json b/packages/visual-editor/locales/platform/pl/visual-editor.json index 74553c7a00..c4e3f5cf85 100644 --- a/packages/visual-editor/locales/platform/pl/visual-editor.json +++ b/packages/visual-editor/locales/platform/pl/visual-editor.json @@ -275,6 +275,7 @@ "key": "Klucz", "keywordsDisplayName": "Nazwa wyświetlana słów kluczowych", "label": "Etykieta", + "labelColor": "Kolor etykiety", "latitude": "Szerokość", "limit": "Limit", "link": "Link", diff --git a/packages/visual-editor/locales/platform/pt/visual-editor.json b/packages/visual-editor/locales/platform/pt/visual-editor.json index a8b3d3f89d..b68b7e4b08 100644 --- a/packages/visual-editor/locales/platform/pt/visual-editor.json +++ b/packages/visual-editor/locales/platform/pt/visual-editor.json @@ -275,6 +275,7 @@ "key": "Chave", "keywordsDisplayName": "Nome de exibição de palavras-chave", "label": "Rótulo", + "labelColor": "Cor do rótulo", "latitude": "Latitude", "limit": "Limite", "link": "Link", diff --git a/packages/visual-editor/locales/platform/ro/visual-editor.json b/packages/visual-editor/locales/platform/ro/visual-editor.json index 4fb9aa99c3..df70115059 100644 --- a/packages/visual-editor/locales/platform/ro/visual-editor.json +++ b/packages/visual-editor/locales/platform/ro/visual-editor.json @@ -275,6 +275,7 @@ "key": "Cheie", "keywordsDisplayName": "Nume afișat pentru cuvinte cheie", "label": "Eticheta", + "labelColor": "Culoarea etichetei", "latitude": "Latitudine", "limit": "Limită", "link": "Legătură", diff --git a/packages/visual-editor/locales/platform/sk/visual-editor.json b/packages/visual-editor/locales/platform/sk/visual-editor.json index 2c072f56fb..0f821b11b4 100644 --- a/packages/visual-editor/locales/platform/sk/visual-editor.json +++ b/packages/visual-editor/locales/platform/sk/visual-editor.json @@ -275,6 +275,7 @@ "key": "kľúč", "keywordsDisplayName": "Zobrazovaný názov kľúčových slov", "label": "Štítok", + "labelColor": "Farba štítka", "latitude": "Šírka", "limit": "Obmedzenie", "link": "Prepojiť", diff --git a/packages/visual-editor/locales/platform/sv/visual-editor.json b/packages/visual-editor/locales/platform/sv/visual-editor.json index fc1df9bfff..158226d3f0 100644 --- a/packages/visual-editor/locales/platform/sv/visual-editor.json +++ b/packages/visual-editor/locales/platform/sv/visual-editor.json @@ -275,6 +275,7 @@ "key": "Nyckel", "keywordsDisplayName": "Sökord Visningsnamn", "label": "Märka", + "labelColor": "Etikettfärg", "latitude": "Latitud", "limit": "Begränsa", "link": "Länk", diff --git a/packages/visual-editor/locales/platform/tr/visual-editor.json b/packages/visual-editor/locales/platform/tr/visual-editor.json index 5f2e090a0a..cc36b003cb 100644 --- a/packages/visual-editor/locales/platform/tr/visual-editor.json +++ b/packages/visual-editor/locales/platform/tr/visual-editor.json @@ -275,6 +275,7 @@ "key": "Anahtar", "keywordsDisplayName": "Anahtar Kelimeler Görünen Ad", "label": "Etiket", + "labelColor": "Etiket Rengi", "latitude": "Enlem", "limit": "Sınırlamak", "link": "Bağlantı", diff --git a/packages/visual-editor/locales/platform/zh-TW/visual-editor.json b/packages/visual-editor/locales/platform/zh-TW/visual-editor.json index 3094bce092..b59ec2ba24 100644 --- a/packages/visual-editor/locales/platform/zh-TW/visual-editor.json +++ b/packages/visual-editor/locales/platform/zh-TW/visual-editor.json @@ -275,6 +275,7 @@ "key": "鍵", "keywordsDisplayName": "關鍵字顯示名稱", "label": "標籤", + "labelColor": "標籤顏色", "latitude": "緯度", "limit": "限制", "link": "鏈接", diff --git a/packages/visual-editor/locales/platform/zh/visual-editor.json b/packages/visual-editor/locales/platform/zh/visual-editor.json index 36a0351747..389e5bd2f3 100644 --- a/packages/visual-editor/locales/platform/zh/visual-editor.json +++ b/packages/visual-editor/locales/platform/zh/visual-editor.json @@ -275,6 +275,7 @@ "key": "钥匙", "keywordsDisplayName": "关键词显示名称", "label": "标签", + "labelColor": "标签颜色", "latitude": "纬度", "limit": "限制", "link": "关联", diff --git a/packages/visual-editor/src/components/atoms/cta.test.ts b/packages/visual-editor/src/components/atoms/cta.test.ts new file mode 100644 index 0000000000..d151042cd8 --- /dev/null +++ b/packages/visual-editor/src/components/atoms/cta.test.ts @@ -0,0 +1,123 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { CTA, CTAProps } from "./cta.tsx"; + +vi.mock("../../hooks/useDocument.tsx", () => ({ + useDocument: () => ({}), +})); + +vi.mock("../../hooks/useBackground.tsx", () => ({ + useBackground: () => ({ + selectedColor: "white", + contrastingColor: "black", + isDarkColor: false, + }), +})); + +vi.mock("@yext/pages-components", () => ({ + getDirections: () => "#", + Link: ({ + children, + cta: _cta, + eventName: _eventName, + ...props + }: { + children: React.ReactNode; + cta: unknown; + eventName?: string; + } & React.AnchorHTMLAttributes) => + React.createElement("a", { href: "#", ...props }, children), +})); + +const renderCta = (props: Partial = {}): void => { + render( + React.createElement(CTA, { + label: "Call to action", + link: "#", + normalizeLink: false, + variant: "primary", + ...props, + }) + ); +}; + +describe("CTA text color", () => { + it("uses a palette color for primary link text and icons", () => { + renderCta({ + textColor: { + selectedColor: "palette-secondary", + contrastingColor: "palette-secondary-contrast", + }, + }); + + const link = screen.getByRole("link"); + expect(link.style.color).toBe("var(--colors-palette-secondary)"); + expect(link.querySelector("svg")?.style.color).toBe(""); + }); + + it("uses a custom color for a primary button", () => { + renderCta({ + actionType: "button", + textColor: { selectedColor: "[#00E5FF]", contrastingColor: "black" }, + }); + + expect(screen.getByRole("button").style.color).toBe("rgb(0, 229, 255)"); + }); + + it("uses the fill contrast color when text color is Default", () => { + renderCta({ + color: { + selectedColor: "palette-primary", + contrastingColor: "palette-primary-contrast", + }, + }); + + expect(screen.getByRole("link").style.color).toBe( + "var(--colors-palette-primary-contrast)" + ); + }); + + it("uses the selected text color for a disabled primary CTA", () => { + renderCta({ + disabled: true, + textColor: { selectedColor: "black", contrastingColor: "white" }, + }); + + expect(screen.getByRole("button").style.color).toBe("black"); + }); + + it("ignores text color for non-primary CTAs", () => { + renderCta({ + variant: "secondary", + color: { + selectedColor: "palette-primary", + contrastingColor: "palette-primary-contrast", + }, + textColor: { + selectedColor: "palette-secondary", + contrastingColor: "palette-secondary-contrast", + }, + }); + + expect(screen.getByRole("link").style.color).toBe( + "var(--colors-palette-primary)" + ); + }); + + it("lets an external-link icon inherit the primary text color", () => { + renderCta({ + openInNewTab: true, + textColor: { selectedColor: "white", contrastingColor: "black" }, + }); + + const link = screen.getByRole("link"); + expect(link.style.color).toBe("white"); + expect(link.querySelectorAll("svg")).toHaveLength(2); + expect( + Array.from(link.querySelectorAll("svg")).every( + (icon) => icon.style.color === "" + ) + ).toBe(true); + }); +}); diff --git a/packages/visual-editor/src/components/atoms/cta.tsx b/packages/visual-editor/src/components/atoms/cta.tsx index 2b26d30263..81a9be0cae 100644 --- a/packages/visual-editor/src/components/atoms/cta.tsx +++ b/packages/visual-editor/src/components/atoms/cta.tsx @@ -52,6 +52,8 @@ export type CTAProps = { ) => void; disabled?: boolean; color?: ThemeColor; + /** Overrides the text and icon color for primary CTAs. */ + textColor?: ThemeColor; openInNewTab?: boolean; /** * When true and variant is "link", applies vertical padding (py-3) to the CTA. @@ -241,6 +243,7 @@ export const CTA = (props: CTAProps) => { onClick, disabled = false, color, + textColor, openInNewTab = false, setPadding = false, actionType = "link", @@ -296,13 +299,14 @@ export const CTA = (props: CTAProps) => { ))); const dynamicStyle: React.CSSProperties = (() => { const bg = getThemeColorCssValue(color?.selectedColor); - const textColor = getThemeColorCssValue(color?.contrastingColor); const border = bg; if (variant === "primary") { return { backgroundColor: bg, - color: textColor, + color: getThemeColorCssValue( + textColor?.selectedColor ?? color?.contrastingColor + ), borderColor: border, }; } diff --git a/packages/visual-editor/src/components/atoms/hoursStatus.tsx b/packages/visual-editor/src/components/atoms/hoursStatus.tsx index fce7ab5713..486a8a86ea 100644 --- a/packages/visual-editor/src/components/atoms/hoursStatus.tsx +++ b/packages/visual-editor/src/components/atoms/hoursStatus.tsx @@ -19,6 +19,7 @@ export interface HoursStatusAtomProps { timezone?: string; boldCurrentStatus?: boolean; bodyVariant?: "lg" | "base" | "sm"; + style?: React.CSSProperties; } export const HoursStatusAtom = React.memo( @@ -33,6 +34,7 @@ export const HoursStatusAtom = React.memo( timezone, boldCurrentStatus = true, bodyVariant = "lg", + style, }: HoursStatusAtomProps): any => { const { t, i18n } = useTranslation(); @@ -115,7 +117,10 @@ export const HoursStatusAtom = React.memo( } return ( -
+
{(showCurrentStatus || isComingSoon) && hoursCurrentTemplateOverride(params, t, boldCurrentStatus)} {!isComingSoon && diff --git a/packages/visual-editor/src/components/atoms/phone.test.ts b/packages/visual-editor/src/components/atoms/phone.test.ts new file mode 100644 index 0000000000..3818d5af60 --- /dev/null +++ b/packages/visual-editor/src/components/atoms/phone.test.ts @@ -0,0 +1,125 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { PhoneAtom } from "./phone.tsx"; + +vi.mock("./cta.tsx", () => ({ + CTA: ({ label, color }: { label: React.ReactNode; color?: unknown }) => + React.createElement("a", { + href: "#", + "data-testid": "phone-link", + "data-color": JSON.stringify(color), + children: label, + }), +})); + +describe("PhoneAtom colors", () => { + it("applies independent palette colors to the label, number, and icon", () => { + const { container } = render( + React.createElement(PhoneAtom, { + phoneNumber: "+12025550123", + label: "Phone", + format: "domestic", + includeHyperlink: false, + includeIcon: true, + labelColor: { + selectedColor: "palette-primary", + contrastingColor: "palette-primary-contrast", + }, + linkColor: { + selectedColor: "palette-secondary", + contrastingColor: "palette-secondary-contrast", + }, + iconColor: { + selectedColor: "palette-tertiary", + contrastingColor: "palette-tertiary-contrast", + }, + }) + ); + + expect( + screen.getByText("Phone").classList.contains("text-palette-primary") + ).toBe(true); + expect( + screen + .getByText("(202) 555-0123") + .classList.contains("text-palette-secondary") + ).toBe(true); + expect( + container + .querySelector("svg") + ?.classList.contains("text-palette-tertiary") + ).toBe(true); + }); + + it("applies independent custom colors to the label, number, and icon", () => { + const { container } = render( + React.createElement(PhoneAtom, { + phoneNumber: "+12025550123", + label: "Phone", + format: "domestic", + includeHyperlink: false, + includeIcon: true, + labelColor: { selectedColor: "[#112233]", contrastingColor: "white" }, + linkColor: { selectedColor: "[#445566]", contrastingColor: "white" }, + iconColor: { selectedColor: "[#778899]", contrastingColor: "black" }, + }) + ); + + expect(screen.getByText("Phone").style.color).toBe("rgb(17, 34, 51)"); + expect(screen.getByText("(202) 555-0123").style.color).toBe( + "rgb(68, 85, 102)" + ); + expect(container.querySelector("svg")?.style.color).toBe( + "rgb(119, 136, 153)" + ); + }); + + it("keeps the phone link color separate from label and icon colors", () => { + const { container } = render( + React.createElement(PhoneAtom, { + phoneNumber: "+12025550123", + label: "Phone", + format: "domestic", + includeHyperlink: true, + includeIcon: true, + backgroundColor: { + selectedColor: "palette-quaternary", + contrastingColor: "palette-quaternary-contrast", + }, + labelColor: { + selectedColor: "palette-primary", + contrastingColor: "palette-primary-contrast", + }, + linkColor: { + selectedColor: "palette-secondary", + contrastingColor: "palette-secondary-contrast", + }, + iconColor: { + selectedColor: "palette-tertiary", + contrastingColor: "palette-tertiary-contrast", + }, + }) + ); + + expect( + screen.getByText("Phone").classList.contains("text-palette-primary") + ).toBe(true); + expect(screen.getByTestId("phone-link").getAttribute("data-color")).toBe( + JSON.stringify({ + selectedColor: "palette-secondary", + contrastingColor: "palette-secondary-contrast", + }) + ); + expect( + container + .querySelector("svg") + ?.classList.contains("text-palette-tertiary") + ).toBe(true); + expect( + container + .querySelector("svg") + ?.parentElement?.classList.contains("bg-palette-quaternary") + ).toBe(true); + }); +}); diff --git a/packages/visual-editor/src/components/atoms/phone.tsx b/packages/visual-editor/src/components/atoms/phone.tsx index fbd877ce96..c386c21419 100644 --- a/packages/visual-editor/src/components/atoms/phone.tsx +++ b/packages/visual-editor/src/components/atoms/phone.tsx @@ -6,6 +6,8 @@ import { ThemeColor } from "../../utils/themeConfigOptions.ts"; import { getBackgroundColorClasses, getBackgroundColorStyle, + getTextColorClass, + getTextColorStyle, } from "../../utils/colors.ts"; export type PhoneAtomProps = { @@ -17,6 +19,8 @@ export type PhoneAtomProps = { includeHyperlink: boolean; includeIcon: boolean; linkColor?: ThemeColor; + labelColor?: ThemeColor; + iconColor?: ThemeColor; onClick?: () => void; }; @@ -43,12 +47,22 @@ export const PhoneAtom = (props: PhoneAtomProps) => { )}`} style={getBackgroundColorStyle(props.backgroundColor)} > - +
) : ( - + ))} - {props.label && {props.label}} + {props.label && ( + + {props.label} + + )} {props.includeHyperlink ? ( { alwaysHideCaret={true} /> ) : ( - {formattedPhoneNumber} + {formattedPhoneNumber} )}
); diff --git a/packages/visual-editor/src/components/atoms/timestamp.tsx b/packages/visual-editor/src/components/atoms/timestamp.tsx index 0ee3acd54c..8427d12287 100644 --- a/packages/visual-editor/src/components/atoms/timestamp.tsx +++ b/packages/visual-editor/src/components/atoms/timestamp.tsx @@ -1,3 +1,6 @@ +import { ThemeColor } from "../../utils/themeConfigOptions.ts"; +import { getTextColorClass, getTextColorStyle } from "../../utils/colors.ts"; + const format1: Intl.DateTimeFormatOptions = { month: "short", day: "numeric", @@ -30,6 +33,7 @@ export type TimestampAtomProps = { locale?: string; dateFormatOverride?: Omit; dateTimeFormatOverride?: Omit; + textColor?: ThemeColor; }; type TimestampFormatterPropsType = { @@ -113,6 +117,7 @@ export const TimestampAtom = ({ locale, dateFormatOverride, dateTimeFormatOverride, + textColor, }: TimestampAtomProps): JSX.Element => { let timestamp; try { @@ -144,7 +149,10 @@ export const TimestampAtom = ({ const formattedTimestamp = timestamp.replaceAll("\u202F", " "); return ( -
+
{formattedTimestamp}
); diff --git a/packages/visual-editor/src/components/contentBlocks/Address.tsx b/packages/visual-editor/src/components/contentBlocks/Address.tsx index b34f52292f..a0bff02879 100644 --- a/packages/visual-editor/src/components/contentBlocks/Address.tsx +++ b/packages/visual-editor/src/components/contentBlocks/Address.tsx @@ -23,6 +23,7 @@ import { } from "../../utils/themeConfigOptions.ts"; import { resolveDataFromParent } from "../../editor/ParentData.tsx"; import { YextComponentConfig, YextFields } from "../../fields/fields.ts"; +import { getTextColorClass, getTextColorStyle } from "../../utils/colors.ts"; /** Props for the Address component */ export interface AddressProps { @@ -51,6 +52,12 @@ export interface AddressProps { ctaVariant: CTAVariant; color?: ThemeColor; + + /** The color of the address text. */ + textColor?: ThemeColor; + + /** The text and icon color for a primary get directions CTA. */ + ctaTextColor?: ThemeColor; }; /** @internal */ @@ -105,6 +112,16 @@ export const AddressStyleFields: YextFields = { label: msg("fields.linkColor", "Link Color"), options: "SITE_COLOR", }, + textColor: { + type: "basicSelector", + label: msg("fields.textColor", "Text Color"), + options: "SITE_COLOR", + }, + ctaTextColor: { + type: "basicSelector", + label: msg("fields.textColor", "Text Color"), + options: "SITE_COLOR", + }, }; export const addressFields: YextFields = { @@ -163,7 +180,10 @@ const AddressComponent: PuckComponent = (props) => { ); return showAddress ? ( -
+
= (props) => { target="_blank" variant={styles.ctaVariant} color={resolvedColor} + textColor={styles.ctaTextColor} /> )}
@@ -218,6 +239,11 @@ export const resolveAddressFields = ( "styles.objectFields.color.visible", showGetDirectionsLink && showColor ); + updatedFields = setDeep( + updatedFields, + "styles.objectFields.ctaTextColor.visible", + showGetDirectionsLink && ctaVariant === "primary" + ); return updatedFields; }; diff --git a/packages/visual-editor/src/components/contentBlocks/CTAGroup.tsx b/packages/visual-editor/src/components/contentBlocks/CTAGroup.tsx index 4f7a7f17dd..307a5133c3 100644 --- a/packages/visual-editor/src/components/contentBlocks/CTAGroup.tsx +++ b/packages/visual-editor/src/components/contentBlocks/CTAGroup.tsx @@ -28,6 +28,8 @@ type BasicCTAProps = { /** The image to use if the CTA is set to preset image */ presetImage?: PresetImageType; color?: ThemeColor; + /** The text and icon color for the primary variant. */ + textColor?: ThemeColor; }; const defaultButton: BasicCTAProps = { @@ -83,6 +85,11 @@ const ctaGroupFields: YextFields = { label: msg("fields.color", "Color"), options: "SITE_COLOR", }, + textColor: { + type: "basicSelector", + label: msg("fields.textColor", "Text Color"), + options: "SITE_COLOR", + }, }, getItemSummary: (_: BasicCTAProps, i?: number) => pt("cta", "CTA") + " " + ((i ?? 0) + 1), @@ -144,6 +151,7 @@ const CTAGroupComponent: PuckComponent = ({ buttons }) => { presetImageType={button.presetImage} className="truncate w-full" color={button.color} + textColor={button.textColor} />
) diff --git a/packages/visual-editor/src/components/contentBlocks/CtaWrapper.tsx b/packages/visual-editor/src/components/contentBlocks/CtaWrapper.tsx index e1a7d215e3..7d0bfd3b9e 100644 --- a/packages/visual-editor/src/components/contentBlocks/CtaWrapper.tsx +++ b/packages/visual-editor/src/components/contentBlocks/CtaWrapper.tsx @@ -46,6 +46,8 @@ export interface CTAWrapperProps { /** The image to use if the CTA is set to preset image */ presetImage?: PresetImageType; color?: ThemeColor; + /** The text and icon color for the primary variant. */ + textColor?: ThemeColor; }; /** Additional CSS classes to apply to the CTA. */ @@ -165,6 +167,11 @@ const ctaWrapperFields: YextFields = { label: msg("fields.color", "Color"), options: "SITE_COLOR", }, + textColor: { + type: "basicSelector", + label: msg("fields.textColor", "Text Color"), + options: "SITE_COLOR", + }, }, }, }; @@ -264,6 +271,7 @@ const CTAWrapperComponent: PuckComponent = (props) => { } eventName={eventName} color={styles.color} + textColor={styles.textColor} id={data.customId} ariaLabel={actionType === "button" ? resolvedAriaLabel : undefined} dataAttributes={dataAttributeProps} @@ -350,6 +358,11 @@ export const CTAWrapper: YextComponentConfig = { const showColor = isCtaVariantWithColor(ctaVariant) && effectiveCtaType !== "presetImage"; setDeep(updatedFields, "styles.objectFields.color.visible", showColor); + setDeep( + updatedFields, + "styles.objectFields.textColor.visible", + ctaVariant === "primary" && effectiveCtaType !== "presetImage" + ); if (data.props.parentData) { return updatedFields; diff --git a/packages/visual-editor/src/components/contentBlocks/Emails.test.ts b/packages/visual-editor/src/components/contentBlocks/Emails.test.ts new file mode 100644 index 0000000000..3e5493c222 --- /dev/null +++ b/packages/visual-editor/src/components/contentBlocks/Emails.test.ts @@ -0,0 +1,104 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { Emails, EmailsProps } from "./Emails.tsx"; + +vi.mock("../../hooks/useDocument.tsx", () => ({ + useDocument: () => ({}), +})); + +vi.mock("../../editor/EntityField.tsx", () => ({ + EntityField: ({ children }: { children: React.ReactNode }) => children, +})); + +vi.mock("../atoms/background.tsx", () => ({ + Background: ({ + children, + background, + className, + }: { + children: React.ReactNode; + background?: unknown; + className?: string; + }) => + React.createElement("div", { + className, + "data-testid": "icon-background", + "data-color": JSON.stringify(background), + children, + }), +})); + +vi.mock("../atoms/cta.tsx", () => ({ + CTA: ({ label, color }: { label: React.ReactNode; color?: unknown }) => + React.createElement("a", { + href: "#", + "data-testid": "email-link", + "data-color": JSON.stringify(color), + children: label, + }), +})); + +const renderEmails = (styles: EmailsProps["styles"]): HTMLElement => { + return render( + React.createElement(Emails.render as React.ComponentType, { + data: { + list: { field: "emails", constantValue: [] }, + }, + styles, + parentData: { field: "emails", list: ["test@example.com"] }, + puck: { isEditing: false }, + }) + ).container; +}; + +describe("Emails colors", () => { + it("applies independent palette colors to the link and icon", () => { + const container = renderEmails({ + showIcon: true, + color: { + selectedColor: "palette-primary", + contrastingColor: "palette-primary-contrast", + }, + iconColor: { + selectedColor: "palette-secondary", + contrastingColor: "palette-secondary-contrast", + }, + }); + + expect(screen.getByTestId("email-link").getAttribute("data-color")).toBe( + JSON.stringify({ + selectedColor: "palette-primary", + contrastingColor: "palette-primary-contrast", + }) + ); + expect( + screen.getByTestId("icon-background").getAttribute("data-color") + ).toBe( + JSON.stringify({ + selectedColor: "palette-primary", + contrastingColor: "palette-primary-contrast", + }) + ); + expect( + container + .querySelector("svg") + ?.classList.contains("text-palette-secondary") + ).toBe(true); + }); + + it("applies a custom icon color", () => { + const container = renderEmails({ + showIcon: true, + color: { + selectedColor: "palette-primary", + contrastingColor: "palette-primary-contrast", + }, + iconColor: { selectedColor: "[#00E5FF]", contrastingColor: "black" }, + }); + + expect(container.querySelector("svg")?.style.color).toBe( + "rgb(0, 229, 255)" + ); + }); +}); diff --git a/packages/visual-editor/src/components/contentBlocks/Emails.tsx b/packages/visual-editor/src/components/contentBlocks/Emails.tsx index 53dc015d69..c7b88b8451 100644 --- a/packages/visual-editor/src/components/contentBlocks/Emails.tsx +++ b/packages/visual-editor/src/components/contentBlocks/Emails.tsx @@ -20,6 +20,7 @@ import { YextComponentConfig, YextFields, } from "../../fields/fields.ts"; +import { getTextColorClass, getTextColorStyle } from "../../utils/colors.ts"; export interface EmailsProps { data: { @@ -31,6 +32,8 @@ export interface EmailsProps { showIcon?: boolean; /** The color applied to both the email icon background and the email link. */ color?: ThemeColor; + /** The color of the email icon. */ + iconColor?: ThemeColor; }; /** @internal Event name to be used for click analytics */ @@ -82,6 +85,11 @@ export const EmailsFields: YextFields = { label: msg("fields.color", "Color"), options: "SITE_COLOR", }, + iconColor: { + type: "basicSelector", + label: msg("fields.iconColor", "Icon Color"), + options: "SITE_COLOR", + }, }, }, }; @@ -126,7 +134,10 @@ const EmailsComponent: PuckComponent = (props) => { background={styles?.color ?? backgroundColors.background2.value} className="h-10 w-10 shrink-0 flex justify-center rounded-full items-center" > - + )} = { @@ -23,9 +25,18 @@ const getDirectionsFields: YextFields = { label: msg("fields.color", "Color"), options: "SITE_COLOR", }, + textColor: { + type: "basicSelector", + label: msg("fields.textColor", "Text Color"), + options: "SITE_COLOR", + }, }; -const GetDirectionsComponent = ({ variant, color }: GetDirectionsProps) => { +const GetDirectionsComponent = ({ + variant, + color, + textColor, +}: GetDirectionsProps) => { const { t } = useTranslation(); return ( @@ -39,6 +50,7 @@ const GetDirectionsComponent = ({ variant, color }: GetDirectionsProps) => { target="_blank" variant={variant} color={color} + textColor={textColor} /> ); }; @@ -54,6 +66,7 @@ export const GetDirections: YextComponentConfig = { const ctaVariant = data.props.variant; const showColor = isCtaVariantWithColor(ctaVariant); setDeep(updatedFields, "color.visible", showColor); + setDeep(updatedFields, "textColor.visible", ctaVariant === "primary"); return updatedFields; }, render: (props) => , diff --git a/packages/visual-editor/src/components/contentBlocks/HoursStatus.tsx b/packages/visual-editor/src/components/contentBlocks/HoursStatus.tsx index 811e9c3479..fe4b750136 100644 --- a/packages/visual-editor/src/components/contentBlocks/HoursStatus.tsx +++ b/packages/visual-editor/src/components/contentBlocks/HoursStatus.tsx @@ -9,6 +9,8 @@ import { msg, pt } from "../../utils/i18n/platform.ts"; import { HoursStatusAtom } from "../atoms/hoursStatus.tsx"; import { resolveDataFromParent } from "../../editor/ParentData.tsx"; import { YextComponentConfig, YextFields } from "../../fields/fields.ts"; +import { ThemeColor } from "../../utils/themeConfigOptions.ts"; +import { getTextColorClass, getTextColorStyle } from "../../utils/colors.ts"; export interface HoursStatusProps { data: { @@ -29,6 +31,8 @@ export interface HoursStatusProps { className?: string; /** The body size variant */ bodyVariant?: "lg" | "base" | "sm"; + /** The color of the hours status text. */ + textColor?: ThemeColor; }; /** @internal */ @@ -90,6 +94,11 @@ export const hoursStatusWrapperFields: YextFields = { { label: msg("fields.options.long", "Long"), value: "long" }, ], }, + textColor: { + type: "basicSelector", + label: msg("fields.textColor", "Text Color"), + options: "SITE_COLOR", + }, }, }, }; @@ -118,12 +127,13 @@ const HoursStatusWrapper: PuckComponent = ({ hours={hours ?? {}} comingSoon={comingSoon} timezone={timezone} - className={styles.className} + className={`${styles.className ?? ""} ${getTextColorClass(styles.textColor) ?? ""}`} showCurrentStatus={styles.showCurrentStatus} showDayNames={styles.showDayNames} timeFormat={styles.timeFormat} dayOfWeekFormat={styles.dayOfWeekFormat} bodyVariant={styles.bodyVariant} + style={getTextColorStyle(styles.textColor)} /> ) : puck.isEditing ? ( diff --git a/packages/visual-editor/src/components/contentBlocks/HoursTable.tsx b/packages/visual-editor/src/components/contentBlocks/HoursTable.tsx index 17326a3482..d41ef79335 100644 --- a/packages/visual-editor/src/components/contentBlocks/HoursTable.tsx +++ b/packages/visual-editor/src/components/contentBlocks/HoursTable.tsx @@ -10,6 +10,8 @@ import { YextEntityField } from "../../editor/YextEntityFieldSelector.tsx"; import { msg, pt } from "../../utils/i18n/platform.ts"; import { Body } from "../atoms/body.tsx"; import { YextComponentConfig, YextFields } from "../../fields/fields.ts"; +import { ThemeColor } from "../../utils/themeConfigOptions.ts"; +import { getTextColorClass, getTextColorStyle } from "../../utils/colors.ts"; /** Props for the HoursTable component. */ export interface HoursTableProps { @@ -29,6 +31,8 @@ export interface HoursTableProps { showAdditionalHoursText: boolean; /** Alignment of the text in the hours table */ alignment: "items-start" | "items-center"; + /** The color of the hours text. */ + textColor?: ThemeColor; }; } @@ -96,6 +100,11 @@ export const hoursTableFields: YextFields = { }, ], }, + textColor: { + type: "basicSelector", + label: msg("fields.textColor", "Text Color"), + options: "SITE_COLOR", + }, }, }, }; @@ -112,7 +121,10 @@ const VisualEditorHoursTable: PuckComponent = (props) => { }; return hours || comingSoon ? ( -
+
= { label: msg("fields.color", "Color"), options: "SITE_COLOR", }, + labelColor: { + type: "basicSelector", + label: msg("fields.labelColor", "Label Color"), + options: "SITE_COLOR", + }, + iconColor: { + type: "basicSelector", + label: msg("fields.iconColor", "Icon Color"), + options: "SITE_COLOR", + }, }; export const defaultPhoneDataProps: PhoneProps["data"] = { @@ -134,6 +148,8 @@ const PhoneComponent = ({ data, styles, parentData }: PhoneProps) => { includeHyperlink={styles.includePhoneHyperlink} includeIcon={styles.includeIcon ?? true} linkColor={styles.color} + labelColor={styles.labelColor} + iconColor={styles.iconColor} /> ); diff --git a/packages/visual-editor/src/components/contentBlocks/PhoneList.tsx b/packages/visual-editor/src/components/contentBlocks/PhoneList.tsx index 0b39854e30..df6e231843 100644 --- a/packages/visual-editor/src/components/contentBlocks/PhoneList.tsx +++ b/packages/visual-editor/src/components/contentBlocks/PhoneList.tsx @@ -137,6 +137,8 @@ export const PhoneListComponent: PuckComponent = (props) => { includeHyperlink={styles.includePhoneHyperlink} includeIcon={styles.includeIcon ?? true} linkColor={styles.color} + labelColor={styles.labelColor} + iconColor={styles.iconColor} />
diff --git a/packages/visual-editor/src/components/contentBlocks/TextList.tsx b/packages/visual-editor/src/components/contentBlocks/TextList.tsx index e4767ab00e..751d44ebb0 100644 --- a/packages/visual-editor/src/components/contentBlocks/TextList.tsx +++ b/packages/visual-editor/src/components/contentBlocks/TextList.tsx @@ -7,10 +7,14 @@ import { TranslatableString } from "../../types/types.ts"; import { resolveComponentData } from "../../utils/resolveComponentData.tsx"; import { msg, pt } from "../../utils/i18n/platform.ts"; import { YextComponentConfig, YextFields } from "../../fields/fields.ts"; +import { ThemeColor } from "../../utils/themeConfigOptions.ts"; +import { getTextColorClass, getTextColorStyle } from "../../utils/colors.ts"; export interface TextListProps { list: YextEntityField; commaSeparated: boolean; + /** The color of the list text. */ + textColor?: ThemeColor; } export const textListFields: YextFields = { @@ -30,11 +34,17 @@ export const textListFields: YextFields = { { label: msg("fields.options.no", "No"), value: false }, ], }, + textColor: { + type: "basicSelector", + label: msg("fields.textColor", "Text Color"), + options: "SITE_COLOR", + }, }; const TextListComponent: PuckComponent = ({ list: textListField, commaSeparated, + textColor, puck, }) => { const { i18n } = useTranslation(); @@ -58,11 +68,12 @@ const TextListComponent: PuckComponent = ({ > {resolvedTextList && resolvedTextList.length > 0 ? (