Align email buttons consistently across formats and directions
This commit is contained in:
1 parent
f9cf2a648a
commit
7110e1a7f1
3 files changed
+33
-6
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/email-renderer",
|
"name": "@wrnexus/email-renderer",
|
||||||
"version": "0.1.28",
|
"version": "0.1.29",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.",
|
"description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -116,8 +116,13 @@ const buttonVisualCss = (node: BlockNode) => {
|
|||||||
const values = buttonVisuals(node);
|
const values = buttonVisuals(node);
|
||||||
return `color:${values.color};font-size:${values.fontSize};font-weight:${values.fontWeight};border-radius:${values.radius}`;
|
return `color:${values.color};font-size:${values.fontSize};font-weight:${values.fontWeight};border-radius:${values.radius}`;
|
||||||
};
|
};
|
||||||
const emailCell = (body: string, node: BlockNode) =>
|
const buttonAlignment = (node: BlockNode, context: unknown) => {
|
||||||
`<tr><td data-block-id="${html(node.id)}" style="padding:0;${semanticStyle(node)}">${body}</td></tr>`;
|
const value = safeCssValue(styleBase(node, "textAlign"), /^(left|center|right|start|end)$/) || "start";
|
||||||
|
const rtl = (context as { direction?: string } | undefined)?.direction === "rtl";
|
||||||
|
return value === "start" ? (rtl ? "right" : "left") : value === "end" ? (rtl ? "left" : "right") : value;
|
||||||
|
};
|
||||||
|
const emailCell = (body: string, node: BlockNode, align?: string) =>
|
||||||
|
`<tr><td data-block-id="${html(node.id)}"${align ? ` align="${html(align)}"` : ""} style="padding:0;${semanticStyle(node)}">${body}</td></tr>`;
|
||||||
|
|
||||||
export const emailDocumentBlock = defineBlock({
|
export const emailDocumentBlock = defineBlock({
|
||||||
manifest: {
|
manifest: {
|
||||||
@@ -389,16 +394,17 @@ export const buttonBlock = leaf(
|
|||||||
{
|
{
|
||||||
emailHtml: ({ node, context }) =>
|
emailHtml: ({ node, context }) =>
|
||||||
emailCell(
|
emailCell(
|
||||||
`<table role="presentation"><tr><td bgcolor="${html(buttonBackground(node, context))}" style="border-radius:${html(buttonVisuals(node).radius)};"><a href="${html(attrs(node).href)}" style="${html(buttonVisualCss(node))};display:inline-block;font-family:inherit;padding:12px 20px;text-decoration:none;">${html(attrs(node).label)}</a></td></tr></table>`,
|
`<table role="presentation" align="${buttonAlignment(node, context)}" cellpadding="0" cellspacing="0" border="0"><tr><td bgcolor="${html(buttonBackground(node, context))}" style="border-radius:${html(buttonVisuals(node).radius)};"><a href="${html(attrs(node).href)}" style="${html(buttonVisualCss(node))};display:inline-block;font-family:inherit;padding:12px 20px;text-decoration:none;">${html(attrs(node).label)}</a></td></tr></table>`,
|
||||||
node,
|
node,
|
||||||
),
|
),
|
||||||
ampEmail: ({ node }) =>
|
ampEmail: ({ node, context }) =>
|
||||||
emailCell(
|
emailCell(
|
||||||
`<a class="button ${ampLayoutClass(node)}" href="${html(attrs(node).href)}">${html(attrs(node).label)}</a>`,
|
`<a class="button ${ampLayoutClass(node)}" href="${html(attrs(node).href)}">${html(attrs(node).label)}</a>`,
|
||||||
node,
|
node,
|
||||||
|
buttonAlignment(node, context),
|
||||||
),
|
),
|
||||||
mjml: ({ node, context }) =>
|
mjml: ({ node, context }) =>
|
||||||
`<mj-button background-color="${html(buttonBackground(node, context))}" color="${html(buttonVisuals(node).color)}" font-size="${html(buttonVisuals(node).fontSize)}" font-weight="${html(buttonVisuals(node).fontWeight)}" border-radius="${html(buttonVisuals(node).radius)}" href="${html(attrs(node).href)}">${html(attrs(node).label)}</mj-button>`,
|
`<mj-button align="${buttonAlignment(node, context)}" background-color="${html(buttonBackground(node, context))}" color="${html(buttonVisuals(node).color)}" font-size="${html(buttonVisuals(node).fontSize)}" font-weight="${html(buttonVisuals(node).fontWeight)}" border-radius="${html(buttonVisuals(node).radius)}" href="${html(attrs(node).href)}">${html(attrs(node).label)}</mj-button>`,
|
||||||
},
|
},
|
||||||
({ node }) => `${String(attrs(node).label)}: ${String(attrs(node).href)}\n\n`,
|
({ node }) => `${String(attrs(node).label)}: ${String(attrs(node).href)}\n\n`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,27 @@ import { expect, test } from "bun:test";
|
|||||||
import type { BlockDocument } from "@wrnexus/blocks";
|
import type { BlockDocument } from "@wrnexus/blocks";
|
||||||
import { renderEmail, validateMjml } from "../src/index.ts";
|
import { renderEmail, validateMjml } from "../src/index.ts";
|
||||||
|
|
||||||
|
test("button alignment maps physical and logical positions consistently in LTR and RTL", async () => {
|
||||||
|
for (const direction of ["ltr", "rtl"] as const) {
|
||||||
|
for (const alignment of [undefined, "left", "center", "right", "start", "end"]) {
|
||||||
|
const expected = !alignment || alignment === "start" ? (direction === "rtl" ? "right" : "left") : alignment === "end" ? (direction === "rtl" ? "left" : "right") : alignment;
|
||||||
|
const document: BlockDocument = {
|
||||||
|
schemaVersion: 1, kind: "email", meta: { subject: "Alignment", lang: "en" }, theme: { tokens: {} },
|
||||||
|
root: { id: "root", type: "layout.email-document", version: 1, props: { width: "600" }, styles: {}, slots: {
|
||||||
|
content: [{ id: "cta", type: "content.button", version: 1, props: { label: "Act", href: "https://example.com" }, styles: alignment ? { textAlign: { base: alignment } } : {}, slots: {} }],
|
||||||
|
} },
|
||||||
|
};
|
||||||
|
const rendered = await renderEmail(document, { direction, amp: true, mjml: true, ampValidationTimeoutMs: 10000 });
|
||||||
|
expect(rendered.ok).toBe(true);
|
||||||
|
if (!rendered.ok) continue;
|
||||||
|
expect(rendered.value.parts.html.content).toContain(`<table role="presentation" align="${expected}" cellpadding="0"`);
|
||||||
|
expect(rendered.value.parts.amp?.content).toContain(`data-block-id="cta" align="${expected}"`);
|
||||||
|
expect(rendered.value.parts.amp?.validation.valid).toBe(true);
|
||||||
|
expect(rendered.value.exports?.mjml).toContain(`<mj-button align="${expected}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("button typography and corners reach the clickable element in every target", async () => {
|
test("button typography and corners reach the clickable element in every target", async () => {
|
||||||
const document: BlockDocument = {
|
const document: BlockDocument = {
|
||||||
schemaVersion: 1, kind: "email", meta: { subject: "Styled CTA", lang: "en" }, theme: { tokens: {} },
|
schemaVersion: 1, kind: "email", meta: { subject: "Styled CTA", lang: "en" }, theme: { tokens: {} },
|
||||||
|
|||||||
Reference in new issue
Block a user