Bug 1762488 - Remove text configuration nesting with raw unlocalized text r=emcminn

Also allow configuring styles like zap and color. Clean up some unnecessary conditional rendering as Localized already handles that.

Differential Revision: https://phabricator.services.mozilla.com/D142673
This commit is contained in:
Ed Lee 2022-04-01 16:20:45 +00:00
Родитель 8b26a85901
Коммит 352b921b1e
6 изменённых файлов: 126 добавлений и 72 удалений

Просмотреть файл

@ -355,10 +355,12 @@ __webpack_require__.r(__webpack_exports__);
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
const MS_STRING_PROP = "string_id";
const CONFIGURABLE_STYLES = ["color", "fontSize"];
const ZAP_SIZE_THRESHOLD = 160;
/**
* Based on the .text prop, localizes an inner element if a string_id
* is provided, OR renders plain text, OR hides it if nothing is provided.
* Allows configuring of some styles including zap underline and color.
*
* Examples:
*
@ -373,6 +375,7 @@ const MS_STRING_PROP = "string_id";
* Unlocalized text
* jsx:
* <Localized text="Welcome"><h1 /></Localized>
* <Localized text={{raw: "Welcome"}}><h1 /></Localized>
* output:
* <h1>Welcome</h1>
*/
@ -381,29 +384,55 @@ const Localized = ({
text,
children
}) => {
// Dynamically determine the size of the zap style.
const zapRef = /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createRef();
(0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
const {
current
} = zapRef;
if (current) requestAnimationFrame(() => current === null || current === void 0 ? void 0 : current.classList.replace("short", current.getBoundingClientRect().width > ZAP_SIZE_THRESHOLD ? "long" : "short"));
}); // Skip rendering of children with no text.
if (!text) {
return null;
}
} // Allow augmenting existing child container properties.
let props = children ? children.props : {};
let textNode;
if (typeof text === "object" && text[MS_STRING_PROP]) {
props = { ...props
};
props["data-l10n-id"] = text[MS_STRING_PROP];
const props = {
children: [],
className: "",
style: {},
...(children === null || children === void 0 ? void 0 : children.props)
}; // Support nested Localized by starting with their children.
const textNodes = props.children; // Pick desired fluent or raw/plain text to render.
if (text.string_id) {
props["data-l10n-id"] = text.string_id;
if (text.args) props["data-l10n-args"] = JSON.stringify(text.args);
} else if (text.raw) {
textNodes.push(text.raw);
} else if (typeof text === "string") {
textNode = text;
}
textNodes.push(text);
} // Add zap style and content in a way that allows fluent to insert too.
if (!children) {
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", props, textNode);
} else if (textNode) {
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().cloneElement(children, props, textNode);
}
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().cloneElement(children, props);
if (text.zap) {
props.className += " welcomeZap";
textNodes.push( /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {
className: "short zap",
"data-l10n-name": "zap",
ref: zapRef
}, text.zap));
} // Apply certain configurable styles.
CONFIGURABLE_STYLES.forEach(style => {
if (text[style]) props.style[style] = text[style];
});
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().cloneElement( // Provide a default container for the text if necessary.
children ?? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null), props, // Conditionally pass in as void elements can't accept empty array.
textNodes.length ? textNodes : null);
};
/***/ }),
@ -679,7 +708,7 @@ class ProtonScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
}
render() {
var _this$props$appAndSys, _content$primary_butt;
var _this$props$appAndSys, _content$primary_butt, _content$primary_butt2;
const {
autoAdvance,
@ -716,11 +745,11 @@ class ProtonScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
text: content.hero_text
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("h1", null)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "spacer-bottom"
})), content.help_text && content.help_text.text ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
text: content.help_text.text
})), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
text: content.help_text
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {
className: "attrib-text"
})) : null) : null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
}))) : null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "section-main"
}, content.secondary_button_top ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MultiStageAboutWelcome__WEBPACK_IMPORTED_MODULE_5__.SecondaryCTA, {
content: content,
@ -749,19 +778,19 @@ class ProtonScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
text: content.title
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("h1", {
id: "mainContentHeader"
})), content.subtitle ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
})), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
text: content.subtitle
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("h2", {
"data-l10n-args": JSON.stringify({
"addon-name": this.props.addonName,
...((_this$props$appAndSys = this.props.appAndSystemLocaleInfo) === null || _this$props$appAndSys === void 0 ? void 0 : _this$props$appAndSys.displayNames)
})
})) : null), this.renderContentTiles(), this.renderLanguageSwitcher(), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
text: content.primary_button ? content.primary_button.label : null
}))), this.renderContentTiles(), this.renderLanguageSwitcher(), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
text: (_content$primary_butt = content.primary_button) === null || _content$primary_butt === void 0 ? void 0 : _content$primary_butt.label
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", {
className: "primary",
value: "primary_button",
disabled: ((_content$primary_butt = content.primary_button) === null || _content$primary_butt === void 0 ? void 0 : _content$primary_butt.disabled) === true,
disabled: ((_content$primary_butt2 = content.primary_button) === null || _content$primary_butt2 === void 0 ? void 0 : _content$primary_butt2.disabled) === true,
onClick: this.props.handleAction
})), content.secondary_button ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MultiStageAboutWelcome__WEBPACK_IMPORTED_MODULE_5__.SecondaryCTA, {
content: content,
@ -1077,7 +1106,7 @@ const Themes = props => {
onClick: props.handleAction
})), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: `icon ${theme === props.activeTheme ? " selected" : ""} ${theme}`
}), label && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
}), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
text: label
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "text"

Просмотреть файл

@ -38,9 +38,7 @@ const DEFAULT_WELCOME_CONTENT = {
string_id: "mr1-welcome-screen-hero-text",
},
help_text: {
text: {
string_id: "mr1-onboarding-welcome-image-caption",
},
string_id: "mr1-onboarding-welcome-image-caption",
},
has_noodles: true,
primary_button: {
@ -443,7 +441,7 @@ async function prepareContentForReact(content) {
if (Services.locale.appLocaleAsBCP47.split("-")[0] !== "en") {
delete content.screens?.find(
screen => screen.content?.help_text?.deleteIfNotEn
)?.content.help_text.text;
)?.content.help_text;
}
let shouldRemoveLanguageMismatchScreen = true;

Просмотреть файл

@ -2,12 +2,14 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
import React from "react";
const MS_STRING_PROP = "string_id";
import React, { useEffect } from "react";
const CONFIGURABLE_STYLES = ["color", "fontSize"];
const ZAP_SIZE_THRESHOLD = 160;
/**
* Based on the .text prop, localizes an inner element if a string_id
* is provided, OR renders plain text, OR hides it if nothing is provided.
* Allows configuring of some styles including zap underline and color.
*
* Examples:
*
@ -22,30 +24,67 @@ const MS_STRING_PROP = "string_id";
* Unlocalized text
* jsx:
* <Localized text="Welcome"><h1 /></Localized>
* <Localized text={{raw: "Welcome"}}><h1 /></Localized>
* output:
* <h1>Welcome</h1>
*/
export const Localized = ({ text, children }) => {
// Dynamically determine the size of the zap style.
const zapRef = React.createRef();
useEffect(() => {
const { current } = zapRef;
if (current)
requestAnimationFrame(() =>
current?.classList.replace(
"short",
current.getBoundingClientRect().width > ZAP_SIZE_THRESHOLD
? "long"
: "short"
)
);
});
// Skip rendering of children with no text.
if (!text) {
return null;
}
let props = children ? children.props : {};
let textNode;
// Allow augmenting existing child container properties.
const props = { children: [], className: "", style: {}, ...children?.props };
// Support nested Localized by starting with their children.
const textNodes = props.children;
if (typeof text === "object" && text[MS_STRING_PROP]) {
props = { ...props };
props["data-l10n-id"] = text[MS_STRING_PROP];
// Pick desired fluent or raw/plain text to render.
if (text.string_id) {
props["data-l10n-id"] = text.string_id;
if (text.args) props["data-l10n-args"] = JSON.stringify(text.args);
} else if (text.raw) {
textNodes.push(text.raw);
} else if (typeof text === "string") {
textNode = text;
textNodes.push(text);
}
if (!children) {
return React.createElement("span", props, textNode);
} else if (textNode) {
return React.cloneElement(children, props, textNode);
// Add zap style and content in a way that allows fluent to insert too.
if (text.zap) {
props.className += " welcomeZap";
textNodes.push(
<span className="short zap" data-l10n-name="zap" ref={zapRef}>
{text.zap}
</span>
);
}
return React.cloneElement(children, props);
// Apply certain configurable styles.
CONFIGURABLE_STYLES.forEach(style => {
if (text[style]) props.style[style] = text[style];
});
return React.cloneElement(
// Provide a default container for the text if necessary.
children ?? <span />,
props,
// Conditionally pass in as void elements can't accept empty array.
textNodes.length ? textNodes : null
);
};

Просмотреть файл

@ -197,11 +197,9 @@ export class ProtonScreen extends React.PureComponent {
</Localized>
<div className="spacer-bottom" />
</div>
{content.help_text && content.help_text.text ? (
<Localized text={content.help_text.text}>
<span className="attrib-text" />
</Localized>
) : null}
<Localized text={content.help_text}>
<span className="attrib-text" />
</Localized>
</div>
) : null}
<div className="section-main">
@ -237,25 +235,19 @@ export class ProtonScreen extends React.PureComponent {
<Localized text={content.title}>
<h1 id="mainContentHeader" />
</Localized>
{content.subtitle ? (
<Localized text={content.subtitle}>
<h2
data-l10n-args={JSON.stringify({
"addon-name": this.props.addonName,
...this.props.appAndSystemLocaleInfo?.displayNames,
})}
/>
</Localized>
) : null}
<Localized text={content.subtitle}>
<h2
data-l10n-args={JSON.stringify({
"addon-name": this.props.addonName,
...this.props.appAndSystemLocaleInfo?.displayNames,
})}
/>
</Localized>
</div>
{this.renderContentTiles()}
{this.renderLanguageSwitcher()}
<div>
<Localized
text={
content.primary_button ? content.primary_button.label : null
}
>
<Localized text={content.primary_button?.label}>
<button
className="primary"
value="primary_button"

Просмотреть файл

@ -37,11 +37,9 @@ export const Themes = props => {
theme === props.activeTheme ? " selected" : ""
} ${theme}`}
/>
{label && (
<Localized text={label}>
<div className="text" />
</Localized>
)}
<Localized text={label}>
<div className="text" />
</Localized>
</label>
</Localized>
)

Просмотреть файл

@ -114,7 +114,7 @@ describe("MultiStageAboutWelcomeProton module", () => {
it("should have an image caption", async () => {
const data = await prepConfig();
assert.property(data.screens[0].content.help_text, "text");
assert.property(data.screens[0].content, "help_text");
});
it("should remove the caption if deleteIfNotEn is true", async () => {
sandbox.stub(global.Services.locale, "appLocaleAsBCP47").value("de");
@ -132,16 +132,14 @@ describe("MultiStageAboutWelcomeProton module", () => {
position: "corner",
help_text: {
deleteIfNotEn: true,
text: {
string_id: "mr1-onboarding-welcome-image-caption",
},
string_id: "mr1-onboarding-welcome-image-caption",
},
},
},
],
});
assert.notProperty(data.screens[0].content.help_text, "text");
assert.notProperty(data.screens[0].content, "help_text");
});
});
describe("AboutWelcomeDefaults prepareContentForReact", () => {