From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../addon-component-status/StatusIndicator.mjs | 114 + .../addon-component-status/constants.mjs | 7 + .../.storybook/addon-component-status/index.js | 23 + .../addon-component-status/preset/manager.mjs | 19 + .../addon-component-status/preset/preview.mjs | 12 + .../.storybook/addon-fluent/FluentPanel.mjs | 121 + .../addon-fluent/PseudoLocalizationButton.mjs | 55 + .../.storybook/addon-fluent/constants.mjs | 32 + .../.storybook/addon-fluent/fluent-panel.css | 83 + .../storybook/.storybook/addon-fluent/index.js | 23 + .../.storybook/addon-fluent/preset/manager.mjs | 34 + .../.storybook/addon-fluent/preset/preview.mjs | 27 + .../addon-fluent/withPseudoLocalization.mjs | 89 + .../storybook/.storybook/chrome-styles-loader.js | 145 + .../storybook/.storybook/chrome-uri-utils.js | 30 + .../storybook/.storybook/fluent-utils.mjs | 122 + .../storybook/.storybook/l10n-pseudo.mjs | 110 + browser/components/storybook/.storybook/main.js | 153 + .../storybook/.storybook/markdown-story-loader.js | 149 + .../storybook/.storybook/preview-head.html | 208 + .../components/storybook/.storybook/preview.mjs | 108 + .../storybook/custom-elements-manifest.config.mjs | 46 + .../storybook/docs/README.lit-guide.stories.md | 157 + .../storybook/docs/README.other-widgets.stories.md | 84 + .../docs/README.reusable-widgets.stories.md | 152 + .../storybook/docs/README.storybook.stories.md | 148 + .../storybook/docs/README.typography.stories.md | 389 + .../storybook/docs/README.xul-and-html.stories.md | 67 + browser/components/storybook/mach_commands.py | 108 + browser/components/storybook/moz.build | 8 + browser/components/storybook/package-lock.json | 38503 +++++++++++++++++++ browser/components/storybook/package.json | 28 + .../storybook/stories/button.stories.mjs | 100 + .../stories/fxview-category-navigation.stories.mjs | 113 + .../storybook/stories/fxview-tab-list.stories.md | 103 + .../storybook/stories/fxview-tab-list.stories.mjs | 213 + .../storybook/stories/letter-grade.stories.mjs | 55 + .../stories/login-command-button.stories.mjs | 90 + .../storybook/stories/login-timeline.stories.mjs | 45 + .../storybook/stories/migration-wizard.stories.mjs | 553 + .../storybook/stories/named-deck.stories.mjs | 165 + .../storybook/stories/shopping-card.stories.mjs | 91 + .../stories/shopping-container.stories.mjs | 301 + .../stories/shopping-message-bar.stories.mjs | 54 + 44 files changed, 43237 insertions(+) create mode 100644 browser/components/storybook/.storybook/addon-component-status/StatusIndicator.mjs create mode 100644 browser/components/storybook/.storybook/addon-component-status/constants.mjs create mode 100644 browser/components/storybook/.storybook/addon-component-status/index.js create mode 100644 browser/components/storybook/.storybook/addon-component-status/preset/manager.mjs create mode 100644 browser/components/storybook/.storybook/addon-component-status/preset/preview.mjs create mode 100644 browser/components/storybook/.storybook/addon-fluent/FluentPanel.mjs create mode 100644 browser/components/storybook/.storybook/addon-fluent/PseudoLocalizationButton.mjs create mode 100644 browser/components/storybook/.storybook/addon-fluent/constants.mjs create mode 100644 browser/components/storybook/.storybook/addon-fluent/fluent-panel.css create mode 100644 browser/components/storybook/.storybook/addon-fluent/index.js create mode 100644 browser/components/storybook/.storybook/addon-fluent/preset/manager.mjs create mode 100644 browser/components/storybook/.storybook/addon-fluent/preset/preview.mjs create mode 100644 browser/components/storybook/.storybook/addon-fluent/withPseudoLocalization.mjs create mode 100644 browser/components/storybook/.storybook/chrome-styles-loader.js create mode 100644 browser/components/storybook/.storybook/chrome-uri-utils.js create mode 100644 browser/components/storybook/.storybook/fluent-utils.mjs create mode 100644 browser/components/storybook/.storybook/l10n-pseudo.mjs create mode 100644 browser/components/storybook/.storybook/main.js create mode 100644 browser/components/storybook/.storybook/markdown-story-loader.js create mode 100644 browser/components/storybook/.storybook/preview-head.html create mode 100644 browser/components/storybook/.storybook/preview.mjs create mode 100644 browser/components/storybook/custom-elements-manifest.config.mjs create mode 100644 browser/components/storybook/docs/README.lit-guide.stories.md create mode 100644 browser/components/storybook/docs/README.other-widgets.stories.md create mode 100644 browser/components/storybook/docs/README.reusable-widgets.stories.md create mode 100644 browser/components/storybook/docs/README.storybook.stories.md create mode 100644 browser/components/storybook/docs/README.typography.stories.md create mode 100644 browser/components/storybook/docs/README.xul-and-html.stories.md create mode 100644 browser/components/storybook/mach_commands.py create mode 100644 browser/components/storybook/moz.build create mode 100644 browser/components/storybook/package-lock.json create mode 100644 browser/components/storybook/package.json create mode 100644 browser/components/storybook/stories/button.stories.mjs create mode 100644 browser/components/storybook/stories/fxview-category-navigation.stories.mjs create mode 100644 browser/components/storybook/stories/fxview-tab-list.stories.md create mode 100644 browser/components/storybook/stories/fxview-tab-list.stories.mjs create mode 100644 browser/components/storybook/stories/letter-grade.stories.mjs create mode 100644 browser/components/storybook/stories/login-command-button.stories.mjs create mode 100644 browser/components/storybook/stories/login-timeline.stories.mjs create mode 100644 browser/components/storybook/stories/migration-wizard.stories.mjs create mode 100644 browser/components/storybook/stories/named-deck.stories.mjs create mode 100644 browser/components/storybook/stories/shopping-card.stories.mjs create mode 100644 browser/components/storybook/stories/shopping-container.stories.mjs create mode 100644 browser/components/storybook/stories/shopping-message-bar.stories.mjs (limited to 'browser/components/storybook') diff --git a/browser/components/storybook/.storybook/addon-component-status/StatusIndicator.mjs b/browser/components/storybook/.storybook/addon-component-status/StatusIndicator.mjs new file mode 100644 index 0000000000..05d72146cb --- /dev/null +++ b/browser/components/storybook/.storybook/addon-component-status/StatusIndicator.mjs @@ -0,0 +1,114 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +// eslint-disable-next-line no-unused-vars +import React from "react"; +import { useParameter } from "@storybook/api"; +import { + // eslint-disable-next-line no-unused-vars + Badge, + // eslint-disable-next-line no-unused-vars + WithTooltip, + // eslint-disable-next-line no-unused-vars + TooltipMessage, + // eslint-disable-next-line no-unused-vars + IconButton, +} from "@storybook/components"; +import { TOOL_ID, STATUS_PARAM_KEY } from "./constants.mjs"; + +const VALID_STATUS_MAP = { + stable: { + label: "Stable", + badgeType: "positive", + description: + "This component is widely used in Firefox, in both the chrome and in-content pages.", + }, + "in-development": { + label: "In Development", + badgeType: "warning", + description: + "This component is in active development and starting to be used in Firefox. It may not yet be usable in both the chrome and in-content pages.", + }, + unstable: { + label: "Unstable", + badgeType: "negative", + description: + "This component is still in the early stages of development and may not be ready for general use in Firefox.", + }, +}; + +/** + * Displays a badge with the components status in the Storybook toolbar. + * + * Statuses are set via story parameters. + * We support either passing `status: "statusType"` for using defaults or + * `status: { + type: "stable" | "in-development" | "unstable", + description: "Your description here" + links: [ + { + title: "Link title", + href: "www.example.com", + }, + ], + }` + * when we want to customize the description or add links. + */ +export const StatusIndicator = () => { + let componentStatus = useParameter(STATUS_PARAM_KEY, null); + let statusData = VALID_STATUS_MAP[componentStatus?.type ?? componentStatus]; + + if (!componentStatus || !statusData) { + return ""; + } + + // The tooltip message is added/removed from the DOM when visibility changes. + // We need to update the aira-describedby button relationship accordingly. + let onVisibilityChange = isVisible => { + let button = document.getElementById("statusButton"); + if (isVisible) { + button.setAttribute("aria-describedby", "statusMessage"); + } else { + button.removeAttribute("aria-describedby"); + } + }; + + let description = componentStatus.description || statusData.description; + let links = componentStatus.links || []; + + return ( + ( +
+ +
+ )} + > + + + {statusData.label} + + +
+ ); +}; diff --git a/browser/components/storybook/.storybook/addon-component-status/constants.mjs b/browser/components/storybook/.storybook/addon-component-status/constants.mjs new file mode 100644 index 0000000000..84dc1983ac --- /dev/null +++ b/browser/components/storybook/.storybook/addon-component-status/constants.mjs @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +export const ADDON_ID = "addon-component-status"; +export const TOOL_ID = `${ADDON_ID}/statusIndicator`; +export const STATUS_PARAM_KEY = "status"; diff --git a/browser/components/storybook/.storybook/addon-component-status/index.js b/browser/components/storybook/.storybook/addon-component-status/index.js new file mode 100644 index 0000000000..7f923e2de1 --- /dev/null +++ b/browser/components/storybook/.storybook/addon-component-status/index.js @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ +/* eslint-env node */ + +/** + * This file hooks our addon into Storybook. Having a root-level file like this + * is a Storybook requirement. It handles registering the addon without any + * additional user configuration. + */ + +function config(entry = []) { + return [...entry, require.resolve("./preset/preview.mjs")]; +} + +function managerEntries(entry = []) { + return [...entry, require.resolve("./preset/manager.mjs")]; +} + +module.exports = { + managerEntries, + config, +}; diff --git a/browser/components/storybook/.storybook/addon-component-status/preset/manager.mjs b/browser/components/storybook/.storybook/addon-component-status/preset/manager.mjs new file mode 100644 index 0000000000..4aa611b156 --- /dev/null +++ b/browser/components/storybook/.storybook/addon-component-status/preset/manager.mjs @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +/** This file handles registering the Storybook addon */ + +// eslint-disable-next-line no-unused-vars +import React from "react"; +import { addons, types } from "@storybook/addons"; +import { ADDON_ID, TOOL_ID } from "../constants.mjs"; +import { StatusIndicator } from "../StatusIndicator.mjs"; + +addons.register(ADDON_ID, () => { + addons.add(TOOL_ID, { + type: types.TOOL, + title: "Pseudo Localization", + render: StatusIndicator, + }); +}); diff --git a/browser/components/storybook/.storybook/addon-component-status/preset/preview.mjs b/browser/components/storybook/.storybook/addon-component-status/preset/preview.mjs new file mode 100644 index 0000000000..57f1378af6 --- /dev/null +++ b/browser/components/storybook/.storybook/addon-component-status/preset/preview.mjs @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +export const globalTypes = { + componentStatus: { + name: "Component status", + description: + "Provides a visual indicator of the component's readiness status.", + defaultValue: "default", + }, +}; diff --git a/browser/components/storybook/.storybook/addon-fluent/FluentPanel.mjs b/browser/components/storybook/.storybook/addon-fluent/FluentPanel.mjs new file mode 100644 index 0000000000..692ff73737 --- /dev/null +++ b/browser/components/storybook/.storybook/addon-fluent/FluentPanel.mjs @@ -0,0 +1,121 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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"; +import { addons } from "@storybook/addons"; +// eslint-disable-next-line no-unused-vars +import { AddonPanel } from "@storybook/components"; +import { FLUENT_CHANGED, FLUENT_SET_STRINGS } from "./constants.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "./fluent-panel.css"; + +export class FluentPanel extends React.Component { + constructor(props) { + super(props); + this.channel = addons.getChannel(); + this.state = { + name: null, + strings: [], + }; + } + + componentDidMount() { + const { api } = this.props; + api.on(FLUENT_CHANGED, this.handleFluentChanged); + } + + componentWillUnmount() { + const { api } = this.props; + api.off(FLUENT_CHANGED, this.handleFluentChanged); + } + + handleFluentChanged = strings => { + let storyData = this.props.api.getCurrentStoryData(); + let fileName = `${storyData.component}.ftl`; + this.setState(state => ({ ...state, strings, fileName })); + }; + + onInput = e => { + this.setState(state => { + let strings = []; + for (let [key, value] of state.strings) { + if (key == e.target.name) { + let stringValue = e.target.value; + if (stringValue.startsWith(".")) { + stringValue = "\n" + stringValue; + } + strings.push([key, stringValue]); + } else { + strings.push([key, value]); + } + } + let stringified = strings + .map(([key, value]) => `${key} = ${value}`) + .join("\n"); + this.channel.emit(FLUENT_SET_STRINGS, stringified); + const { fluentStrings } = this.props.api.getGlobals(); + this.props.api.updateGlobals({ + fluentStrings: { ...fluentStrings, [state.fileName]: strings }, + }); + return { ...state, strings }; + }); + }; + + render() { + const { api, active } = this.props; + const { strings } = this.state; + if (strings.length === 0) { + return ( + +
+
+ This story is not configured to use Fluent. +
+
+
+ ); + } + + return ( + +
+ + + + + + + + + {strings.map(([identifier, value]) => ( + + + + + ))} + +
+ Identifier + + String +
+ {identifier} + + +
+
+
+ ); + } +} diff --git a/browser/components/storybook/.storybook/addon-fluent/PseudoLocalizationButton.mjs b/browser/components/storybook/.storybook/addon-fluent/PseudoLocalizationButton.mjs new file mode 100644 index 0000000000..d60112d224 --- /dev/null +++ b/browser/components/storybook/.storybook/addon-fluent/PseudoLocalizationButton.mjs @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +// eslint-disable-next-line no-unused-vars +import React from "react"; +import { useGlobals } from "@storybook/api"; +import { + // eslint-disable-next-line no-unused-vars + Icons, + // eslint-disable-next-line no-unused-vars + IconButton, + // eslint-disable-next-line no-unused-vars + WithTooltip, + // eslint-disable-next-line no-unused-vars + TooltipLinkList, +} from "@storybook/components"; +import { TOOL_ID, STRATEGY_DEFAULT, PSEUDO_STRATEGIES } from "./constants.mjs"; + +// React component for a button + tooltip that gets added to the Storybook toolbar. +export const PseudoLocalizationButton = () => { + const [{ pseudoStrategy = STRATEGY_DEFAULT }, updateGlobals] = useGlobals(); + + const updatePseudoStrategy = strategy => { + updateGlobals({ pseudoStrategy: strategy }); + }; + + const getTooltipLinks = ({ onHide }) => { + return PSEUDO_STRATEGIES.map(strategy => ({ + id: strategy, + title: strategy.charAt(0).toUpperCase() + strategy.slice(1), + onClick: () => { + updatePseudoStrategy(strategy); + onHide(); + }, + active: pseudoStrategy === strategy, + })); + }; + + return ( + } + > + + + + + ); +}; diff --git a/browser/components/storybook/.storybook/addon-fluent/constants.mjs b/browser/components/storybook/.storybook/addon-fluent/constants.mjs new file mode 100644 index 0000000000..3f00b2972a --- /dev/null +++ b/browser/components/storybook/.storybook/addon-fluent/constants.mjs @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +export const ADDON_ID = "addon-fluent"; +export const PANEL_ID = `${ADDON_ID}/fluentPanel`; +export const TOOL_ID = `${ADDON_ID}/toolbarButton`; + +export const STRATEGY_DEFAULT = "default"; +export const STRATEGY_ACCENTED = "accented"; +export const STRATEGY_BIDI = "bidi"; + +export const PSEUDO_STRATEGIES = [ + STRATEGY_DEFAULT, + STRATEGY_ACCENTED, + STRATEGY_BIDI, +]; + +export const DIRECTIONS = { + ltr: "ltr", + rtl: "rtl", +}; + +export const DIRECTION_BY_STRATEGY = { + [STRATEGY_DEFAULT]: DIRECTIONS.ltr, + [STRATEGY_ACCENTED]: DIRECTIONS.ltr, + [STRATEGY_BIDI]: DIRECTIONS.rtl, +}; + +export const UPDATE_STRATEGY_EVENT = "update-strategy"; +export const FLUENT_SET_STRINGS = "fluent-set-strings"; +export const FLUENT_CHANGED = "fluent-changed"; diff --git a/browser/components/storybook/.storybook/addon-fluent/fluent-panel.css b/browser/components/storybook/.storybook/addon-fluent/fluent-panel.css new file mode 100644 index 0000000000..75f4562820 --- /dev/null +++ b/browser/components/storybook/.storybook/addon-fluent/fluent-panel.css @@ -0,0 +1,83 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +.addon-panel-body { + box-sizing: border-box; +} + +.addon-panel-message { + background: #FFF5CF; + color: #333333; + padding: 10px 15px; + line-height: 20px; + box-shadow: rgba(0,0,0,.1) 0 -1px 0 0 inset; + font-size: 13px; +} + +.addon-panel-table { + border-collapse: collapse; + border-spacing: 0; + color: #333333; + font-size: 13px; + line-height: 20px; + text-align: left; + width: 100%; + margin: 0; +} + +.addon-panel-table-head { + color: rgba(51,51,51,0.75); +} + +.addon-panel-table-head th { + padding: 10px 15px; + border: none; + vertical-align: top; +} + +.addon-panel-table-head th:first-of-type, .addon-panel-table-body td:first-of-type { + width: 25%; + padding-left: 20px; +} + +.addon-panel-table-head th:last-of-type, .addon-panel-table-body td:last-of-type { + padding-right: 20px; +} + +.addon-panel-table-body { + border-radius: 4px; +} + +.addon-panel-table-body tr { + overflow: hidden; + border-top: 1px solid #e6e6e6; +} + +.addon-panel-table-body td { + padding: 10px 15px; + font-weight: bold; +} + +.addon-panel-table-body label { + display: flex; +} + +.addon-panel-table-body textarea { + height: fit-content; + appearance: none; + border: none; + box-sizing: inherit; + display: block; + margin: 0; + background-color: rgb(255, 255, 255); + padding: 6px 10px; + color: #333333; + box-shadow: rgba(0,0,0,.1) 0 0 0 1px inset; + border-radius: 4px; + line-height: 20px; + flex: 1; + text-align: left; + overflow: visible; + max-height: 400px; +} diff --git a/browser/components/storybook/.storybook/addon-fluent/index.js b/browser/components/storybook/.storybook/addon-fluent/index.js new file mode 100644 index 0000000000..7f923e2de1 --- /dev/null +++ b/browser/components/storybook/.storybook/addon-fluent/index.js @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ +/* eslint-env node */ + +/** + * This file hooks our addon into Storybook. Having a root-level file like this + * is a Storybook requirement. It handles registering the addon without any + * additional user configuration. + */ + +function config(entry = []) { + return [...entry, require.resolve("./preset/preview.mjs")]; +} + +function managerEntries(entry = []) { + return [...entry, require.resolve("./preset/manager.mjs")]; +} + +module.exports = { + managerEntries, + config, +}; diff --git a/browser/components/storybook/.storybook/addon-fluent/preset/manager.mjs b/browser/components/storybook/.storybook/addon-fluent/preset/manager.mjs new file mode 100644 index 0000000000..0f7ff9299b --- /dev/null +++ b/browser/components/storybook/.storybook/addon-fluent/preset/manager.mjs @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +/** This file handles registering the Storybook addon */ + +// eslint-disable-next-line no-unused-vars +import React from "react"; +import { addons, types } from "@storybook/addons"; +import { ADDON_ID, PANEL_ID, TOOL_ID } from "../constants.mjs"; +import { PseudoLocalizationButton } from "../PseudoLocalizationButton.mjs"; +// eslint-disable-next-line no-unused-vars +import { FluentPanel } from "../FluentPanel.mjs"; + +// Register the addon. +addons.register(ADDON_ID, api => { + // Register the tool. + addons.add(TOOL_ID, { + type: types.TOOL, + title: "Pseudo Localization", + // Toolbar button doesn't show on the "Docs" tab. + match: ({ viewMode }) => !!(viewMode && viewMode.match(/^story$/)), + render: PseudoLocalizationButton, + }); + + addons.add(PANEL_ID, { + title: "Fluent", + //👇 Sets the type of UI element in Storybook + type: types.PANEL, + render: ({ active, key }) => ( + + ), + }); +}); diff --git a/browser/components/storybook/.storybook/addon-fluent/preset/preview.mjs b/browser/components/storybook/.storybook/addon-fluent/preset/preview.mjs new file mode 100644 index 0000000000..cf4f135d40 --- /dev/null +++ b/browser/components/storybook/.storybook/addon-fluent/preset/preview.mjs @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +/** + * This file provides global decorators for the Storybook addon. In theory we + * could combine multiple decorators, but for now we only need one. + */ + +import { + withPseudoLocalization, + withFluentStrings, +} from "../withPseudoLocalization.mjs"; + +export const decorators = [withPseudoLocalization, withFluentStrings]; +export const globalTypes = { + pseudoStrategy: { + name: "Pseudo l10n strategy", + description: "Provides text variants for testing different locales.", + defaultValue: "default", + }, + fluentStrings: { + name: "Fluent string map for components", + description: "Mapping of component to fluent strings.", + defaultValue: {}, + }, +}; diff --git a/browser/components/storybook/.storybook/addon-fluent/withPseudoLocalization.mjs b/browser/components/storybook/.storybook/addon-fluent/withPseudoLocalization.mjs new file mode 100644 index 0000000000..9d6c62af38 --- /dev/null +++ b/browser/components/storybook/.storybook/addon-fluent/withPseudoLocalization.mjs @@ -0,0 +1,89 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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 { useEffect, useGlobals, addons } from "@storybook/addons"; +import { + DIRECTIONS, + DIRECTION_BY_STRATEGY, + UPDATE_STRATEGY_EVENT, + FLUENT_CHANGED, +} from "./constants.mjs"; +import { provideFluent } from "../fluent-utils.mjs"; + +/** + * withPseudoLocalization is a Storybook decorator that handles emitting an + * event to update translations when a new pseudo localization strategy is + * applied. It also handles setting a "dir" attribute on the root element in the + * Storybook iframe. + * + * @param {Function} StoryFn - Provided by Storybook, used to render the story. + * @param {Object} context - Provided by Storybook, data about the story. + * @returns {Function} StoryFn with a modified "dir" attr set. + */ +export const withPseudoLocalization = (StoryFn, context) => { + const [{ pseudoStrategy }] = useGlobals(); + const direction = DIRECTION_BY_STRATEGY[pseudoStrategy] || DIRECTIONS.ltr; + const isInDocs = context.viewMode === "docs"; + const channel = addons.getChannel(); + + useEffect(() => { + if (pseudoStrategy) { + channel.emit(UPDATE_STRATEGY_EVENT, pseudoStrategy); + } + }, [pseudoStrategy]); + + useEffect(() => { + if (isInDocs) { + document.documentElement.setAttribute("dir", DIRECTIONS.ltr); + let storyElements = document.querySelectorAll(".docs-story"); + storyElements.forEach(element => element.setAttribute("dir", direction)); + } else { + document.documentElement.setAttribute("dir", direction); + } + }, [direction, isInDocs]); + + return StoryFn(); +}; + +/** + * withFluentStrings is a Storybook decorator that handles emitting an + * event to update the Fluent strings shown in the Fluent panel. + * + * @param {Function} StoryFn - Provided by Storybook, used to render the story. + * @param {Object} context - Provided by Storybook, data about the story. + * @returns {Function} StoryFn unmodified. + */ +export const withFluentStrings = (StoryFn, context) => { + const [{ fluentStrings }, updateGlobals] = useGlobals(); + const channel = addons.getChannel(); + + const fileName = context.component + ".ftl"; + let strings = []; + + if (context.parameters?.fluent && fileName) { + if (fluentStrings.hasOwnProperty(fileName)) { + strings = fluentStrings[fileName]; + } else { + let resource = provideFluent(context.parameters.fluent, fileName); + for (let message of resource.body) { + strings.push([ + message.id, + [ + message.value, + ...Object.entries(message.attributes).map( + ([key, value]) => ` .${key} = ${value}` + ), + ].join("\n"), + ]); + } + updateGlobals({ + fluentStrings: { ...fluentStrings, [fileName]: strings }, + }); + } + } + + channel.emit(FLUENT_CHANGED, strings); + + return StoryFn(); +}; diff --git a/browser/components/storybook/.storybook/chrome-styles-loader.js b/browser/components/storybook/.storybook/chrome-styles-loader.js new file mode 100644 index 0000000000..4a66128320 --- /dev/null +++ b/browser/components/storybook/.storybook/chrome-styles-loader.js @@ -0,0 +1,145 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ +/* eslint-env node */ + +/** + * This file contains a webpack loader which rewrites JS source files to use CSS + * imports when running in Storybook. This allows JS files loaded in Storybook to use + * chrome:// URIs when loading external stylesheets without having to worry + * about Storybook being able to find and detect changes to the files. + * + * This loader allows Lit-based custom element code like this to work with + * Storybook: + * + * render() { + * return html` + * + * ... + * `; + * } + * + * By rewriting the source to this: + * + * import moztoggleStyles from "toolkit/content/widgets/moz-toggle/moz-toggle.css"; + * ... + * render() { + * return html` + * + * ... + * `; + * } + * + * It works similarly for vanilla JS custom elements that utilize template + * strings. The following code: + * + * static get markup() { + * return` + * + * `; + * } + * + * Gets rewritten to: + * + * import migrationwizardStyles from "browser/themes/shared/migration/migration-wizard.css"; + * ... + * static get markup() { + * return` + * + * `; + * } + */ + +const path = require("path"); +const projectRoot = path.join(process.cwd(), "../../.."); +const rewriteChromeUri = require("./chrome-uri-utils.js"); + +/** + * Return an array of the unique chrome:// CSS URIs referenced in this file. + * + * @param {string} source - The source file to scan. + * @returns {string[]} Unique list of chrome:// CSS URIs + */ +function getReferencedChromeUris(source) { + const chromeRegex = /chrome:\/\/.*?\.css/g; + const matches = new Set(); + for (let match of source.matchAll(chromeRegex)) { + // Add the full URI to the set of matches. + matches.add(match[0]); + } + return [...matches]; +} + +/** + * Replace references to chrome:// URIs with the relative path on disk from the + * project root. + * + * @this {WebpackLoader} https://webpack.js.org/api/loaders/ + * @param {string} source - The source file to update. + * @returns {string} The updated source. + */ +async function rewriteChromeUris(source) { + const chromeUriToLocalPath = new Map(); + // We're going to rewrite the chrome:// URIs, find all referenced URIs. + let chromeDependencies = getReferencedChromeUris(source); + for (let chromeUri of chromeDependencies) { + let localRelativePath = rewriteChromeUri(chromeUri); + if (localRelativePath) { + localRelativePath = localRelativePath.replaceAll("\\", "/"); + // Store the mapping to a local path for this chrome URI. + chromeUriToLocalPath.set(chromeUri, localRelativePath); + // Tell webpack the file being handled depends on the referenced file. + this.addMissingDependency(path.join(projectRoot, localRelativePath)); + } + } + // Rewrite the source file with mapped chrome:// URIs. + let rewrittenSource = source; + for (let [chromeUri, localPath] of chromeUriToLocalPath.entries()) { + // Generate an import friendly variable name for the default export from + // the CSS file e.g. __chrome_styles_loader__moztoggleStyles. + let cssImport = `__chrome_styles_loader__${path + .basename(localPath, ".css") + .replaceAll("-", "")}Styles`; + + // MozTextLabel is a special case for now since we don't use a template. + if ( + this.resourcePath.endsWith("/moz-label.mjs") || + this.resourcePath.endsWith(".js") + ) { + rewrittenSource = rewrittenSource.replaceAll(`"${chromeUri}"`, cssImport); + } else { + rewrittenSource = rewrittenSource.replaceAll( + chromeUri, + `\$\{${cssImport}\}` + ); + } + + // Add a CSS import statement as the first line in the file. + rewrittenSource = + `import ${cssImport} from "${localPath}";\n` + rewrittenSource; + } + return rewrittenSource; +} + +/** + * The WebpackLoader export. Runs async since apparently that's preferred. + * + * @param {string} source - The source to rewrite. + * @param {Map} sourceMap - Source map data, unused. + * @param {Object} meta - Metadata, unused. + */ +module.exports = async function chromeUriLoader(source) { + // Get a callback to tell webpack when we're done. + const callback = this.async(); + // Rewrite the source async since that appears to be preferred (and will be + // necessary once we support rewriting CSS/SVG/etc). + const newSource = await rewriteChromeUris.call(this, source); + // Give webpack the rewritten content. + callback(null, newSource); +}; diff --git a/browser/components/storybook/.storybook/chrome-uri-utils.js b/browser/components/storybook/.storybook/chrome-uri-utils.js new file mode 100644 index 0000000000..514d397961 --- /dev/null +++ b/browser/components/storybook/.storybook/chrome-uri-utils.js @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ +/* eslint-env node */ + +const [prefixMap, aliasMap, sourceMap] = require("./chrome-map.js"); + +function rewriteChromeUri(uri) { + if (uri in aliasMap) { + return rewriteChromeUri(aliasMap[uri]); + } + for (let [prefix, [bundlePath]] of Object.entries(prefixMap)) { + if (uri.startsWith(prefix)) { + if (!bundlePath.endsWith("/")) { + bundlePath += "/"; + } + let relativePath = uri.slice(prefix.length); + let objdirPath = bundlePath + relativePath; + for (let [_objdirPath, [filePath]] of Object.entries(sourceMap)) { + if (_objdirPath == objdirPath) { + // We're just hoping this is the actual path =\ + return filePath; + } + } + } + } + return ""; +} + +module.exports = rewriteChromeUri; diff --git a/browser/components/storybook/.storybook/fluent-utils.mjs b/browser/components/storybook/.storybook/fluent-utils.mjs new file mode 100644 index 0000000000..52a3721820 --- /dev/null +++ b/browser/components/storybook/.storybook/fluent-utils.mjs @@ -0,0 +1,122 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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 { DOMLocalization } from "@fluent/dom"; +import { FluentBundle, FluentResource } from "@fluent/bundle"; +import { addons } from "@storybook/addons"; +import { PSEUDO_STRATEGY_TRANSFORMS } from "./l10n-pseudo.mjs"; +import { + FLUENT_SET_STRINGS, + UPDATE_STRATEGY_EVENT, + STRATEGY_DEFAULT, + PSEUDO_STRATEGIES, +} from "./addon-fluent/constants.mjs"; + +let loadedResources = new Map(); +let currentStrategy; +let storybookBundle = new FluentBundle("en-US", { + transform(str) { + if (currentStrategy in PSEUDO_STRATEGY_TRANSFORMS) { + return PSEUDO_STRATEGY_TRANSFORMS[currentStrategy](str); + } + return str; + }, +}); + +// Listen for update events from addon-fluent. +const channel = addons.getChannel(); +channel.on(UPDATE_STRATEGY_EVENT, updatePseudoStrategy); +channel.on(FLUENT_SET_STRINGS, ftlContents => { + let resource = new FluentResource(ftlContents); + for (let message of resource.body) { + let existingMessage = storybookBundle.getMessage(message.id); + existingMessage.value = message.value; + existingMessage.attributes = message.attributes; + } + document.l10n.translateRoots(); +}); + +/** + * Updates "currentStrategy" when the selected pseudo localization strategy + * changes, which in turn changes the transform used by the Fluent bundle. + * + * @param {string} strategy + * Pseudo localization strategy. Can be "default", "accented", or "bidi". + */ +function updatePseudoStrategy(strategy = STRATEGY_DEFAULT) { + if (strategy !== currentStrategy && PSEUDO_STRATEGIES.includes(strategy)) { + currentStrategy = strategy; + document.l10n.translateRoots(); + } +} + +export function connectFluent() { + document.l10n = new DOMLocalization([], generateBundles); + document.l10n.connectRoot(document.documentElement); + document.l10n.translateRoots(); +} + +function* generateBundles() { + yield* [storybookBundle]; +} + +export async function insertFTLIfNeeded(fileName) { + if (loadedResources.has(fileName)) { + return; + } + + // This should be browser, locales-preview or toolkit. + let [root, ...rest] = fileName.split("/"); + let ftlContents; + + // TODO(mstriemer): These seem like they could be combined but I don't want + // to fight with webpack anymore. + if (root == "toolkit") { + // eslint-disable-next-line no-unsanitized/method + let imported = await import( + /* webpackInclude: /.*[\/\\].*\.ftl$/ */ + `toolkit/locales/en-US/${fileName}` + ); + ftlContents = imported.default; + } else if (root == "browser") { + // eslint-disable-next-line no-unsanitized/method + let imported = await import( + /* webpackInclude: /.*[\/\\].*\.ftl$/ */ + `browser/locales/en-US/${fileName}` + ); + ftlContents = imported.default; + } else if (root == "locales-preview") { + // eslint-disable-next-line no-unsanitized/method + let imported = await import( + /* webpackInclude: /\.ftl$/ */ + `browser/locales-preview/${rest}` + ); + ftlContents = imported.default; + } else if (root == "branding") { + // eslint-disable-next-line no-unsanitized/method + let imported = await import( + /* webpackInclude: /\.ftl$/ */ + `browser/branding/nightly/locales/en-US/${rest}` + ); + ftlContents = imported.default; + } + + if (loadedResources.has(fileName)) { + // Seems possible we've attempted to load this twice before the first call + // resolves, so once the first load is complete we can abandon the others. + return; + } + + provideFluent(ftlContents, fileName); +} + +export function provideFluent(ftlContents, fileName) { + let ftlResource = new FluentResource(ftlContents); + storybookBundle.addResource(ftlResource); + if (fileName) { + loadedResources.set(fileName, ftlResource); + } + document.l10n.translateRoots(); + return ftlResource; +} diff --git a/browser/components/storybook/.storybook/l10n-pseudo.mjs b/browser/components/storybook/.storybook/l10n-pseudo.mjs new file mode 100644 index 0000000000..c92be262e9 --- /dev/null +++ b/browser/components/storybook/.storybook/l10n-pseudo.mjs @@ -0,0 +1,110 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +// Stolen from https://github.com/firefox-devtools/profiler/blob/52a7531662a08d96dc8bd03d25adcdb4c9653b92/src/utils/l10n-pseudo.js +// Which was stolen from https://hg.mozilla.org/mozilla-central/file/a1f74e8c8fb72390d22054d6b00c28b1a32f6c43/intl/l10n/L10nRegistry.jsm#l425 + +/** + * Pseudolocalizations + * + * PSEUDO_STRATEGIES is a dict of strategies to be used to modify a + * context in order to create pseudolocalizations. These can be used by + * developers to test the localizability of their code without having to + * actually speak a foreign language. + * + * Currently, the following pseudolocales are supported: + * + * accented - Ȧȧƈƈḗḗƞŧḗḗḓ Ḗḗƞɠŀīīşħ + * + * In Accented English all Latin letters are replaced by accented + * Unicode counterparts which don't impair the readability of the content. + * This allows developers to quickly test if any given string is being + * correctly displayed in its 'translated' form. Additionally, simple + * heuristics are used to make certain words longer to better simulate the + * experience of international users. + * + * bidi - ɥsıʅƃuƎ ıpıԐ + * + * Bidi English is a fake RTL locale. All words are surrounded by + * Unicode formatting marks forcing the RTL directionality of characters. + * In addition, to make the reversed text easier to read, individual + * letters are flipped. + * + * Note: The name above is hardcoded to be RTL in case code editors have + * trouble with the RLO and PDF Unicode marks. In reality, it should be + * surrounded by those marks as well. + * + * See https://bugzil.la/1450781 for more information. + * + * In this implementation we use code points instead of inline unicode characters + * because the encoding of JSM files mangles them otherwise. + */ + +const ACCENTED_MAP = { + // ȦƁƇḒḖƑƓĦĪĴĶĿḾȠǾƤɊŘŞŦŬṼẆẊẎẐ + // prettier-ignore + "caps": [550, 385, 391, 7698, 7702, 401, 403, 294, 298, 308, 310, 319, 7742, 544, 510, 420, 586, 344, 350, 358, 364, 7804, 7814, 7818, 7822, 7824], + // ȧƀƈḓḗƒɠħīĵķŀḿƞǿƥɋřşŧŭṽẇẋẏẑ + // prettier-ignore + "small": [551, 384, 392, 7699, 7703, 402, 608, 295, 299, 309, 311, 320, 7743, 414, 511, 421, 587, 345, 351, 359, 365, 7805, 7815, 7819, 7823, 7825], +}; + +const FLIPPED_MAP = { + // ∀ԐↃᗡƎℲ⅁HIſӼ⅂WNOԀÒᴚS⊥∩ɅMX⅄Z + // prettier-ignore + "caps": [8704, 1296, 8579, 5601, 398, 8498, 8513, 72, 73, 383, 1276, 8514, 87, 78, 79, 1280, 210, 7450, 83, 8869, 8745, 581, 77, 88, 8516, 90], + // ɐqɔpǝɟƃɥıɾʞʅɯuodbɹsʇnʌʍxʎz + // prettier-ignore + "small": [592, 113, 596, 112, 477, 607, 387, 613, 305, 638, 670, 645, 623, 117, 111, 100, 98, 633, 115, 647, 110, 652, 653, 120, 654, 122], +}; + +function transformString( + map = FLIPPED_MAP, + elongate = false, + prefix = "", + postfix = "", + msg +) { + // Exclude access-keys and other single-char messages + if (msg.length === 1) { + return msg; + } + // XML entities (‪) and XML tags. + const reExcluded = /(&[#\w]+;|<\s*.+?\s*>)/; + + const parts = msg.split(reExcluded); + const modified = parts.map(part => { + if (reExcluded.test(part)) { + return part; + } + return ( + prefix + + part.replace(/[a-z]/gi, ch => { + const cc = ch.charCodeAt(0); + if (cc >= 97 && cc <= 122) { + const newChar = String.fromCodePoint(map.small[cc - 97]); + // duplicate "a", "e", "o" and "u" to emulate ~30% longer text + if ( + elongate && + (cc === 97 || cc === 101 || cc === 111 || cc === 117) + ) { + return newChar + newChar; + } + return newChar; + } + if (cc >= 65 && cc <= 90) { + return String.fromCodePoint(map.caps[cc - 65]); + } + return ch; + }) + + postfix + ); + }); + return modified.join(""); +} + +export const PSEUDO_STRATEGY_TRANSFORMS = { + accented: transformString.bind(null, ACCENTED_MAP, true, "", ""), + bidi: transformString.bind(null, FLIPPED_MAP, false, "\u202e", "\u202c"), +}; diff --git a/browser/components/storybook/.storybook/main.js b/browser/components/storybook/.storybook/main.js new file mode 100644 index 0000000000..3e42f778a4 --- /dev/null +++ b/browser/components/storybook/.storybook/main.js @@ -0,0 +1,153 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ +/* eslint-env node */ + +const path = require("path"); +const webpack = require("webpack"); +const rewriteChromeUri = require("./chrome-uri-utils.js"); + +const projectRoot = path.resolve(__dirname, "../../../../"); + +module.exports = { + // The ordering for this stories array affects the order that they are displayed in Storybook + stories: [ + // Docs section + "../**/README.*.stories.md", + // UI Widgets section + `${projectRoot}/toolkit/content/widgets/**/*.stories.@(js|jsx|mjs|ts|tsx|md)`, + // about:logins components stories + `${projectRoot}/browser/components/aboutlogins/content/components/**/*.stories.mjs`, + // Everything else + "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx|md)", + // Design system files + `${projectRoot}/toolkit/themes/shared/design-system/**/*.stories.@(js|jsx|mjs|ts|tsx|md)`, + ], + addons: [ + "@storybook/addon-links", + { + name: "@storybook/addon-essentials", + options: { + backgrounds: false, + measure: false, + outline: false, + }, + }, + "@storybook/addon-a11y", + path.resolve(__dirname, "addon-fluent"), + path.resolve(__dirname, "addon-component-status"), + ], + framework: "@storybook/web-components", + webpackFinal: async (config, { configType }) => { + // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION' + // You can change the configuration based on that. + // 'PRODUCTION' is used when building the static version of storybook. + + // Make whatever fine-grained changes you need + config.resolve.alias.browser = `${projectRoot}/browser`; + config.resolve.alias.toolkit = `${projectRoot}/toolkit`; + config.resolve.alias[ + "toolkit-widgets" + ] = `${projectRoot}/toolkit/content/widgets/`; + config.resolve.alias[ + "lit.all.mjs" + ] = `${projectRoot}/toolkit/content/widgets/vendor/lit.all.mjs`; + // @mdx-js/react@1.x.x versions don't get hoisted to the root node_modules + // folder due to the versions of React it accepts as a peer dependency. That + // means we have to go one level deeper and look in the node_modules of + // @storybook/addon-docs, which depends on @mdx-js/react. + config.resolve.alias["@storybook/addon-docs"] = + "browser/components/storybook/node_modules/@storybook/addon-docs"; + config.resolve.alias["@mdx-js/react"] = + "@storybook/addon-docs/node_modules/@mdx-js/react"; + + // The @storybook/web-components project uses lit-html. Redirect it to our + // bundled version. + config.resolve.alias["lit-html/directive-helpers.js"] = "lit.all.mjs"; + config.resolve.alias["lit-html"] = "lit.all.mjs"; + + config.plugins.push( + // Rewrite chrome:// URI imports to file system paths. + new webpack.NormalModuleReplacementPlugin(/^chrome:\/\//, resource => { + resource.request = rewriteChromeUri(resource.request); + }) + ); + + config.module.rules.push({ + test: /\.ftl$/, + type: "asset/source", + }); + + config.module.rules.push({ + test: /\.m?js$/, + exclude: /.storybook/, + use: [{ loader: path.resolve(__dirname, "./chrome-styles-loader.js") }], + }); + + // Replace the default CSS rule with a rule to emit a separate CSS file and + // export the URL. This allows us to rewrite the source to use CSS imports + // via the chrome-styles-loader. + let cssFileTest = /\.css$/.toString(); + let cssRuleIndex = config.module.rules.findIndex( + rule => rule.test.toString() === cssFileTest + ); + config.module.rules[cssRuleIndex] = { + test: /\.css$/, + exclude: [/.storybook/, /node_modules/], + type: "asset/resource", + generator: { + filename: "[name].[contenthash].css", + }, + }; + + // We're adding a rule for files matching this pattern in order to support + // writing docs only stories in plain markdown. + const MD_STORY_REGEX = /(stories|story)\.md$/; + + // Find the existing rule for MDX stories. + let mdxStoryTest = /(stories|story)\.mdx$/.toString(); + let mdxRule = config.module.rules.find( + rule => rule.test.toString() === mdxStoryTest + ); + + // Use a custom Webpack loader to transform our markdown stories into MDX, + // then run our new MDX through the same loaders that Storybook usually uses + // for MDX files. This is how we get a docs page from plain markdown. + config.module.rules.push({ + test: MD_STORY_REGEX, + use: [ + ...mdxRule.use, + { loader: path.resolve(__dirname, "./markdown-story-loader.js") }, + ], + }); + + // Find the existing rule for markdown files. + let markdownTest = /\.md$/.toString(); + let markdownRuleIndex = config.module.rules.findIndex( + rule => rule.test.toString() === markdownTest + ); + let markdownRule = config.module.rules[markdownRuleIndex]; + + // Modify the existing markdown rule so it doesn't process .stories.md + // files, but still treats any other markdown files as asset/source. + config.module.rules[markdownRuleIndex] = { + ...markdownRule, + exclude: MD_STORY_REGEX, + }; + + config.optimization = { + splitChunks: false, + runtimeChunk: false, + sideEffects: false, + usedExports: false, + concatenateModules: false, + minimizer: [], + }; + + // Return the altered config + return config; + }, + core: { + builder: "webpack5", + }, +}; diff --git a/browser/components/storybook/.storybook/markdown-story-loader.js b/browser/components/storybook/.storybook/markdown-story-loader.js new file mode 100644 index 0000000000..b11036af74 --- /dev/null +++ b/browser/components/storybook/.storybook/markdown-story-loader.js @@ -0,0 +1,149 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ +/* eslint-env node */ + +/** + * This file contains a Webpack loader that takes markdown as its source and + * outputs a docs only MDX Storybook story. This enables us to write docs only + * pages in plain markdown by specifying a `.stories.md` extension. + * + * For more context on docs only stories, see: + * https://storybook.js.org/docs/web-components/writing-docs/mdx#documentation-only-mdx + * + * The MDX generated by the loader will then get run through the same loaders + * Storybook usually uses to transform MDX files. + */ + +const path = require("path"); +const fs = require("fs"); + +const projectRoot = path.resolve(__dirname, "../../../../"); + +/** + * Takes a file path and returns a string to use as the story title, capitalized + * and split into multiple words. The file name gets transformed into the story + * name, which will be visible in the Storybook sidebar. For example, either: + * + * /stories/hello-world.stories.md or /stories/helloWorld.md + * + * will result in a story named "Hello World". + * + * @param {string} filePath - path of the file being processed. + * @returns {string} The title of the story. + */ +function getStoryTitle(filePath) { + let fileName = path.basename(filePath, ".stories.md"); + if (fileName != "README") { + try { + let relatedFilePath = path.resolve( + "../../../", + filePath.replace(".md", ".mjs") + ); + let relatedFile = fs.readFileSync(relatedFilePath).toString(); + let relatedTitle = relatedFile.match(/title: "(.*)"/)[1]; + if (relatedTitle) { + return relatedTitle + "/README"; + } + } catch {} + } + return separateWords(fileName); +} + +/** + * Splits a string into multiple capitalized words e.g. hello-world, helloWorld, + * and hello.world all become "Hello World." + * @param {string} str - String in any case. + * @returns {string} The string split into multiple words. + */ +function separateWords(str) { + return ( + str + .match(/[A-Z]?[a-z0-9]+/g) + ?.map(text => text[0].toUpperCase() + text.substring(1)) + .join(" ") || str + ); +} + +/** + * Enables rendering code in our markdown docs by parsing the source for + * annotated code blocks and replacing them with Storybook's Canvas component. + * @param {string} source - Stringified markdown source code. + * @returns {string} Source with code blocks replaced by Canvas components. + */ +function parseStoriesFromMarkdown(source) { + let storiesRegex = /```(?:js|html) story\n(?[\s\S]*?)```/g; + // $code comes from the capture group in the regex above. It consists + // of any code in between backticks and gets run when used in a Canvas component. + return source.replace( + storiesRegex, + "\n$" + ); +} + +/** + * The WebpackLoader export. Takes markdown as its source and returns a docs + * only MDX story. Falls back to filing stories under "Docs" for everything + * outside of `toolkit/content/widgets`. + * + * @param {string} source - The markdown source to rewrite to MDX. + */ +module.exports = function markdownStoryLoader(source) { + // Currently we sort docs only stories under "Docs" by default. + let storyPath = "Docs"; + + // `this.resourcePath` is the path of the file being processed. + let relativePath = path + .relative(projectRoot, this.resourcePath) + .replaceAll(path.sep, "/"); + let componentName; + + if (relativePath.includes("toolkit/content/widgets")) { + let storyNameRegex = /(?<=\/widgets\/)(?.*?)(?=\/)/g; + componentName = storyNameRegex.exec(relativePath)?.groups?.name; + if (componentName) { + // Get the common name for a component e.g. Toggle for moz-toggle + storyPath = + "UI Widgets/" + separateWords(componentName).replace(/^Moz/g, ""); + } + } + + let storyTitle = getStoryTitle(relativePath); + let title = storyTitle.includes("/") + ? storyTitle + : `${storyPath}/${storyTitle}`; + + let componentStories; + if (componentName) { + componentStories = this.resourcePath + .replace("README", componentName) + .replace(".md", ".mjs"); + try { + fs.statSync(componentStories); + componentStories = "./" + path.basename(componentStories); + } catch { + componentStories = null; + } + } + + // Unfortunately the indentation/spacing here seems to be important for the + // MDX parser to know what to do in the next step of the Webpack process. + let mdxSource = ` +import { Meta, Description, Canvas, Story } from "@storybook/addon-docs"; +${componentStories ? `import * as Stories from "${componentStories}";` : ""} + + + +${parseStoriesFromMarkdown(source)}`; + + return mdxSource; +}; diff --git a/browser/components/storybook/.storybook/preview-head.html b/browser/components/storybook/.storybook/preview-head.html new file mode 100644 index 0000000000..c2f9e8d1a2 --- /dev/null +++ b/browser/components/storybook/.storybook/preview-head.html @@ -0,0 +1,208 @@ + + + + + + diff --git a/browser/components/storybook/.storybook/preview.mjs b/browser/components/storybook/.storybook/preview.mjs new file mode 100644 index 0000000000..acff43b7c0 --- /dev/null +++ b/browser/components/storybook/.storybook/preview.mjs @@ -0,0 +1,108 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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 { css, html } from "lit.all.mjs"; +import { MozLitElement } from "toolkit/content/widgets/lit-utils.mjs"; +import { setCustomElementsManifest } from "@storybook/web-components"; +import customElementsManifest from "../custom-elements.json"; +import { insertFTLIfNeeded, connectFluent } from "./fluent-utils.mjs"; + +// Base Fluent set up. +connectFluent(); + +// Any fluent imports should go through MozXULElement.insertFTLIfNeeded. +window.MozXULElement = { + insertFTLIfNeeded, +}; + +// Used to set prefs in unprivileged contexts. +window.RPMSetPref = () => { + /* NOOP */ +}; +window.RPMGetFormatURLPref = () => { + /* NOOP */ +}; + +/** + * Wrapper component used to decorate all of our stories by providing access to + * `in-content/common.css` without leaking styles that conflict Storybook's CSS. + * + * More information on decorators can be found at: + * https://storybook.js.org/docs/web-components/writing-stories/decorators + * + * @property {Function} story + * Storybook uses this internally to render stories. We call `story` in our + * render function so that the story contents have the same shadow root as + * `with-common-styles` and styles from `in-content/common` get applied. + * @property {Object} context + * Another Storybook provided property containing additional data stories use + * to render. If we don't make this a reactive property Lit seems to optimize + * away any re-rendering of components inside `with-common-styles`. + */ +class WithCommonStyles extends MozLitElement { + static styles = css` + :host { + display: block; + height: 100%; + padding: 1rem; + box-sizing: border-box; + } + + :host, + :root { + font: message-box; + font-size: var(--font-size-root); + appearance: none; + background-color: var(--color-canvas); + color: var(--text-color); + -moz-box-layout: flex; + } + + :host { + font-size: var(--font-size-root); + } + `; + + static properties = { + story: { type: Function }, + context: { type: Object }, + }; + + connectedCallback() { + super.connectedCallback(); + this.classList.add("anonymous-content-host"); + } + + storyContent() { + if (this.story) { + return this.story(); + } + return html` `; + } + + render() { + return html` + + ${this.storyContent()} + `; + } +} +customElements.define("with-common-styles", WithCommonStyles); + +// Wrap all stories in `with-common-styles`. +export const decorators = [ + (story, context) => + html` + + `, +]; + +// Enable props tables documentation. +setCustomElementsManifest(customElementsManifest); diff --git a/browser/components/storybook/custom-elements-manifest.config.mjs b/browser/components/storybook/custom-elements-manifest.config.mjs new file mode 100644 index 0000000000..d49f646816 --- /dev/null +++ b/browser/components/storybook/custom-elements-manifest.config.mjs @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +/** + * Custom element manifest analyzer plugin to remove static and private + * properties from custom-elements.json that we don't want to document in our + * Storybook props tables. + */ +function removePrivateAndStaticFields() { + return { + packageLinkPhase({ customElementsManifest }) { + customElementsManifest?.modules?.forEach(m => { + m?.declarations?.forEach(declaration => { + if (declaration.members != null) { + declaration.members = declaration.members.filter(member => { + return ( + !member.kind === "field" || + (!member.static && !member.name.startsWith("#")) + ); + }); + } + }); + }); + }, + }; +} + +/** + * Custom element manifest config. Controls how we parse directories for custom + * elements to populate custom-elements.json, which is used by Storybook to + * generate docs. + */ +const config = { + globs: ["../../../toolkit/content/widgets/**/*.mjs"], + exclude: [ + "../../../toolkit/content/widgets/**/*.stories.mjs", + "../../../toolkit/content/widgets/vendor/**", + "../../../toolkit/content/widgets/lit-utils.mjs", + ], + outdir: ".", + litelement: true, + plugins: [removePrivateAndStaticFields()], +}; + +export default config; diff --git a/browser/components/storybook/docs/README.lit-guide.stories.md b/browser/components/storybook/docs/README.lit-guide.stories.md new file mode 100644 index 0000000000..577b17f416 --- /dev/null +++ b/browser/components/storybook/docs/README.lit-guide.stories.md @@ -0,0 +1,157 @@ +# Lit + +## Background + +[Lit](https://lit.dev) is a small library for creating web components that is maintained by Google. It aims to improve the experience of authoring web components by eliminating boilerplate and providing more declarative syntax and re-rendering optimizations, and should feel familiar to developers who have experience working with popular component-based front end frameworks. + +Mozilla developers began experimenting with using Lit to build a handful of new web components in 2021. The developer experience and productivity benefits were noticeable enough that the team tasked with building out a library of new [reusable widgets](./README.reusable-widgets.stories.md) vendored Lit to make it available in `mozilla-central` in late 2022. Lit can now be used for creating new web components anywhere in the codebase. + +## Using Lit + +Lit has comprehensive documentation on their website that should be consulted alongside this document when building new Lit-based custom elements: [https://lit.dev/docs/](https://lit.dev/docs/) + +While Lit was initially introduced to assist with the work of the Reusable Components team it can also be used for creating both reusable and domain-specific UI widgets throughout `mozilla-central`. Some examples of custom elements that have been created using Lit so far include [moz-toggle](https://searchfox.org/mozilla-central/source/toolkit/content/widgets/moz-toggle/moz-toggle.mjs), [moz-button-group](https://searchfox.org/mozilla-central/source/toolkit/content/widgets/moz-button-group/moz-button-group.mjs), and the Credential Management team's [login-timeline](https://searchfox.org/mozilla-central/source/browser/components/aboutlogins/content/components/login-timeline.mjs) component. + +### When to use Lit + +Lit may be a particularly good choice if you're building a highly reactive element that needs to respond efficiently to state changes. Lit's declarative [templates](https://lit.dev/docs/templates/overview/) and [reactive properties](https://lit.dev/docs/components/properties/) can take care of a lot of the work of figuring out which parts of the UI should update in response to specific changes. + +Because Lit components are ultimately just web components, you may also want to use it just because of some of the syntax it provides, like allowing you to write your template code next to your JavaScript code, providing for binding event listeners and properties in your templates, and automatically creating an open `shadowRoot`. + +### When not to use Lit + +Lit cannot be used in cases where you want to [extend a built-in element](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#customized_built-in_elements). Lit can only be used for creating [autonomous custom elements](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#autonomous_custom_elements), i.e. elements that extend `HTMLElement`. + +## Writing components with Lit + +All of the standard features of the Lit library - with the exception of decorators - are available for use in `mozilla-central`, but there are some special considerations and specific files you should be aware of when using Lit for Firefox code. + +### Using external stylesheets + +Using external stylesheets is the preferred way to style your Lit-based components in `mozilla-central`, despite the fact that the the Lit documentation [explicitly recommends against](https://lit.dev/docs/components/styles/#external-stylesheet) this approach. The caveats they list are not particularly relevant to our use cases, and we have implemented platform level workarounds to ensure external styles will not cause a flash-of-unstyled-content. Using external stylesheets makes it so that CSS changes can be detected by our automated linting and review tools, and helps provide greater visibility to Mozilla's `desktop-theme-reviewers` group. + +### The `lit.all.mjs` vendor file + +A somewhat customized, vendored version of Lit is available at [toolkit/content/widgets/vendor/lit.all.mjs](https://searchfox.org/mozilla-central/source/toolkit/content/widgets/vendor/lit.all.mjs). The version of Lit in `mozilla-central` has a number of patches applied to disable minification, source maps, and certain warning messages, as well as patches to replace usage of `innerHTML` with `DOMParser` and to slightly modify the behavior of the `styleMap` directive. More specifics on these patches, as well as information on how to update `lit.all.mjs`, can be found [here](https://searchfox.org/mozilla-central/source/toolkit/content/vendor/lit). + +Because our vendored version of Lit bundles the contents of a few different Lit source files into a single file, imports that would normally come from different files are pulled directly from `lit.all.mjs`. For example, imports that look like this when using the Lit npm package: + +```js +// Standard npm package. +import { LitElement } from "lit"; +import { classMap } from "lit/directives/class-map.js"; +import { ifDefined } from "lit/directives/if-defined.js"; +``` + +Would look like this in `mozilla-central`: + +```js +// All imports come from a single file (relative path also works). +import { LitElement, classMap, ifDefined } from "chrome://global/content/vendor/lit.all.mjs"; +``` + +### `MozLitElement` and `lit-utils.mjs` + +[MozLitElement](https://searchfox.org/mozilla-central/source/toolkit/content/widgets/lit-utils.mjs#84) is an extension of the `LitElement` class that has added functionality to make it more tailored to Mozilla developers' needs. In almost all cases `MozLitElement` should be used as the base class for your new Lit-based custom elements in place of `LitElement`. + +It can be imported from `lit-utils.js` and used as follows: + +```js +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; + +class MyCustomElement extends MozLitElement { + ... +} +``` + +`MozLitElement` differs from `LitElement` in a few important ways: + +#### 1. It provides automatic Fluent support for the shadow DOM + +When working with Fluent in the shadow DOM an element's `shadowRoot` must be connected before Fluent can be used. `MozLitElement` handles this by extending `LitElement`'s `connectedCallback` to [call](https://searchfox.org/mozilla-central/source/toolkit/content/widgets/lit-utils.mjs#84) `document.l10n.connectRoot` if needed. `MozLitElement` also automatically calls `document.l10n.translateFragment` on the renderRoot anytime an element updates. The net result of these modifications is that you can use Fluent in your Lit based components just like you would in any other markup in `mozilla-central`. + +#### 2. It implements support for Lit's `@query` and `@queryAll` decorators + +The Lit library includes `@query` and `@queryAll` [decorators](https://lit.dev/docs/components/shadow-dom/#@query-@queryall-and-@queryasync-decorators) that provide an easy way of finding elements within the internal component DOM. These do not work in `mozilla-central` as we do not have support for JavaScript decorators. Instead, `MozLitElement` provides equivalent [DOM querying functionality](https://searchfox.org/mozilla-central/source/toolkit/content/widgets/lit-utils.mjs#87-99) via defining a static `queries` property on the subclass. For example the following Lit code that queries the component's DOM for certain selectors and assigns the results to different class properties: + +```ts +import { LitElement, html } from "lit"; +import { query } from "lit/decorators/query.js"; + +class MyCustomElement extends LitElement { + @query("#title"); + _title; + + @queryAll("p"); + _paragraphs; + + render() { + return html` +

The title

+

Some other paragraph.

+ `; + } +} +``` + +Is equivalent to this in `mozilla-central`: + +```js +import { html } from "chrome://global/content/vendor/lit.all.mjs"; +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; + +class MyCustomElement extends MozLitElement { + static queries = { + _title: "#title", // equivalent to @query + _paragraphs: { all: "p" }, // equivalent to @queryAll + }; + + render() { + return html` +

The title

+

Some other paragraph.

+ `; + } +} +``` + +#### 3. It adds a `dispatchOnUpdateComplete` method + +The `dispatchOnUpdateComplete` method provides an easy way to communicate to test code or other element consumers that a reactive property change has taken effect. It leverages Lit's [updateComplete](https://lit.dev/docs/components/lifecycle/#updatecomplete) promise to emit an event after all updates have been applied and the component's DOM is ready to be queried. It has the potential to be particularly useful when you need to query the DOM in test code, for example: + +```js +// my-custom-element.mjs +class MyCustomElement extends MozLitElement { + static properties = { + clicked: { type: Boolean }, + }; + + async handleClick() { + if (!this.clicked) { + this.clicked = true; + } + this.dispatchOnUpdateComplete(new CustomEvent("button-clicked")); + } + + render() { + return html` +

The button was ${this.clicked ? "clicked" : "not clicked"}

+ + `; + } +} +``` + +```js +// test_my_custom_element.mjs +add_task(async function testButtonClicked() { + let { button, message } = this.convenientHelperToGetElements(); + is(message.textContent.trim(), "The button was not clicked"); + + let clicked = BrowserTestUtils.waitForEvent(button, "button-clicked"); + synthesizeMouseAtCenter(button, {}); + await clicked; + + is(message.textContent.trim(), "The button was clicked"); +}); +``` diff --git a/browser/components/storybook/docs/README.other-widgets.stories.md b/browser/components/storybook/docs/README.other-widgets.stories.md new file mode 100644 index 0000000000..b2614d88b6 --- /dev/null +++ b/browser/components/storybook/docs/README.other-widgets.stories.md @@ -0,0 +1,84 @@ +# Other types of UI Widgets + +In addition to the [new reusable UI widgets](https://firefox-source-docs.mozilla.org/browser/components/storybook/docs/README.reusable-widgets.stories.html) there are existing elements that serve a similar role. +These older elements are broken down into two groups: Mozilla Custom Elements and User Agent (UA) Widgets. +Additionally, we also have domain-specific widgets that are similar to the reusable widgets but are created to serve a specific need and may or may not adhere to the Design Systems specifications. + +## Older custom elements in `toolkit/widgets` + +There are existing UI widgets in `toolkit/content/widgets/` that belong to one of two groups: Mozilla Custom Elements or User Agent (UA) Widgets. +These [existing custom elements](https://searchfox.org/mozilla-central/rev/cde3d4a8d228491e8b7f1bd94c63bbe039850696/toolkit/content/customElements.js#792-809,847-866) are loaded into all privileged main process documents automatically. +You can determine if a custom element belongs to the existing UI widgets category by either [viewing the array](https://searchfox.org/mozilla-central/rev/cde3d4a8d228491e8b7f1bd94c63bbe039850696/toolkit/content/customElements.js#792-809,847-866) or by viewing the [files in toolkit/content/widgets](https://searchfox.org/mozilla-central/source/toolkit/content/widgets). +Additionally, these older custom elements are a mix of XUL and HTML elements. + + +### Mozilla Custom Elements + +Unlike newer reusable UI widgets, the older Mozilla Custom Elements do not have a dedicated directory. +For example `arrowscrollbox.js` is an older single file custom element versus `moz-button-group/moz-button-group.mjs` which exemplifies the structure followed by newer custom elements. + +### User Agent (UA) widgets + +User agent (UA) widgets are like custom elements but run in per-origin UA widget scope instead of the chrome or content scope. +There are a much smaller number of these widgets compared to the Mozilla Custom Elements: +- [datetimebox.js](https://searchfox.org/mozilla-central/source/toolkit/content/widgets/datetimebox.js) +- [marquee.js](https://searchfox.org/mozilla-central/source/toolkit/content/widgets/marquee.js) +- [textrecognition.js](https://searchfox.org/mozilla-central/source/toolkit/content/widgets/textrecognition.js) +- [videocontrols.js](https://searchfox.org/mozilla-central/source/toolkit/content/widgets/videocontrols.js) + +Please refer to the existing [UA widgets documentation](https://firefox-source-docs.mozilla.org/toolkit/content/toolkit_widgets/ua_widget.html) for more details. + +### How to use existing Mozilla Custom Elements + +The existing Mozilla Custom Elements are automatically imported into all chrome privileged documents. +These existing elements do not need to be imported individually via ` +``` + +Or use `window.ensureCustomElements("")` as previously stated in [the using new design system components section.](#using-new-design-system-components) diff --git a/browser/components/storybook/docs/README.reusable-widgets.stories.md b/browser/components/storybook/docs/README.reusable-widgets.stories.md new file mode 100644 index 0000000000..f26c18a2b0 --- /dev/null +++ b/browser/components/storybook/docs/README.reusable-widgets.stories.md @@ -0,0 +1,152 @@ +# Reusable UI widgets + +## Background + +Different Firefox surfaces make use of similar UI elements such as cards, menus, +toggles, and message bars. A group of designers and developers have started +working together to create standardized versions of these elements in the form +of new web components. The intention is for these components to encapsulate our +design system, ensure accessibility and usability across the application, and +reduce the maintenance burden associated with supporting multiple different +implementations of the same UI patterns. + +Many of these components are being built using the [Lit +library](https://lit.dev/) to take advantage of its templating syntax and +re-rendering logic. All new components are being documented in Storybook in an +effort to create a catalog that engineers and designers can use to see which +components can be easily lifted off the shelf for use throughout Firefox. + +## Designing new reusable widgets + +Widgets that live at the global level, "UI Widgets", should be created in collaboration with the Design System team. +This ensures consistency with the rest of the elements in the Design System and the existing UI elements. +Otherwise, you should consult with your team and the appropriate designer to create domain-specific UI widgets. +Ideally, these domain widgets should be consistent with the rest of the UI patterns established in Firefox. + +### Does an existing widget cover the use case you need? + +Before creating a new reusable widget, make sure there isn't a widget you could use already. +When designing a new reusable widget, ensure it is designed for all users. +Here are some questions you can use to help include all users: how will people perceive, operate, and understand this widget? Will the widget use standards proven technology. +[Please refer to the "General Considerations" section of the Mozilla Accessibility Release Guidelines document](https://wiki.mozilla.org/Accessibility/Guidelines#General_Considerations) for more details to ensure your widget adheres to accessibility standards. + +### Supporting widget use in different processes + +A newly designed widget may need to work in the parent process, the content process, or both depending on your use case. +[See the Process Model document for more information about these different processes](https://firefox-source-docs.mozilla.org/dom/ipc/process_model.html). +You will likely be using your widget in a privileged process (such as the parent or privileged content) with access to `Services`, `XPCOMUtils`, and other globals. +Storybook and other web content do not have access to these privileged globals, so you will need to write workarounds for `Services`, `XPCOMUtils`, chrome URIs for CSS files and assets, etc. +[Check out moz-support-link.mjs and moz-support-link.stories.mjs for an example of a widget being used in the parent/chrome and needing to handle `XPCOMUtils` in Storybook](https://searchfox.org/mozilla-central/search?q=moz-support-link&path=&case=false®exp=false). +[See moz-toggle.mjs for handling chrome URIs for CSS in Storybook](https://searchfox.org/mozilla-central/source/toolkit/content/widgets/moz-toggle/moz-toggle.mjs). +[See moz-label.mjs for an example of handling `Services` in Storybook](https://searchfox.org/mozilla-central/source/toolkit/content/widgets/moz-label/moz-label.mjs). + +### Autonomous or Customized built-in Custom Elements + +There are two types of custom elements, autonomous elements that extend `HTMLElement` and customized built-in elements that extend basic HTML elements. +If you use autonomous elements, you can use Shadow DOM and/or the Lit library. +[Lit does not support customized built-in custom elements](https://github.com/lit/lit-element/issues/879). + +In some cases, you may want to provide some functionality on top of a built-in HTML element, [like how `moz-support-link` prepares the `href` value for anchor elements](https://searchfox.org/mozilla-central/rev/3563da061ca2b32f7f77f5f68088dbf9b5332a9f/toolkit/content/widgets/moz-support-link/moz-support-link.mjs#83-89). +In other cases, you may want to focus on creating markup and reacting to changes on the element. +This is where Lit can be useful for declaritively defining the markup and reacting to changes when attributes are updated. + +### How will developers use your widget? + +What does the interface to your widget look like? +Do you expect developers to use reactive attributes or [slots](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots#adding_flexibility_with_slots)? +If there are many ways to accomplish the same end result, this could result in future confusion and increase the maintainance cost. + +You should write stories for your widget to demonstrate how it can be used. +These stories can be used as guides for new use cases that may appear in the future. +This can also help draw the line for the responsibilities of your widget. + +## Adding new design system components + +We have a `./mach addwidget` scaffold command to make it easier to create new +reusable components and hook them up to Storybook. Currently this command can +only be used to add a new Lit based web component to `toolkit/content/widgets`. +In the future we may expand it to support options for creating components +without using Lit and for adding components to different directories. +See [Bug 1803677](https://bugzilla.mozilla.org/show_bug.cgi?id=1803677) for more details on these future use cases. + +To create a new component, you run: + +```sh +# Component names should be in kebab-case and contain at least 1 -. +./mach addwidget component-name +``` + +The scaffold command will generate the following files: + +```sh +└── toolkit + └── content + ├── tests + │ └── widgets + │ └── test_component_name.html # chrome test + └── widgets + └── component-name # new folder for component code + ├── component-name.css # component specific CSS + ├── component-name.mjs # Lit based component + └── component-name.stories.mjs # component stories +``` + +It will also make modifications to `toolkit/content/jar.mn` to add `chrome://` +URLs for the new files, and to `toolkit/content/tests/widgets/chrome.ini` to +enable running the newly added test. + +After running the scaffold command you can start Storybook and you will see +placeholder content that has been generated for your component. You can then +start altering the generated files and see your changes reflected in Storybook. + +### Known `browser_all_files_referenced.js` issue + +Unfortunately for now [the +browser_all_files_referenced.js test](https://searchfox.org/mozilla-central/source/browser/base/content/test/static/browser_all_files_referenced.js) +will fail unless your new component is immediately used somewhere outside +of Storybook. We have plans to fix this issue, [see Bug 1806002 for more details](https://bugzilla.mozilla.org/show_bug.cgi?id=1806002), but for now you can get around it +by updating [this array](https://searchfox.org/mozilla-central/rev/5c922d8b93b43c18bf65539bfc72a30f84989003/browser/base/content/test/static/browser_all_files_referenced.js#113) to include your new chrome filepath. + +### Using new design system components + +Once you've added a new component to `toolkit/content/widgets` and created +`chrome://` URLs via `toolkit/content/jar.mn` you should be able to start using it +throughout Firefox. You can import the component into `html`/`xhtml` files via a +`script` tag with `type="module"`: + +```html + +``` + +If you are unable to import the new component in html, you can use [`ensureCustomElements()` in customElements.js](https://searchfox.org/mozilla-central/rev/31f5847a4494b3646edabbdd7ea39cb88509afe2/toolkit/content/customElements.js#865) in the relevant JS file. +For example, [we use `window.ensureCustomElements("moz-button-group")` in `browser-siteProtections.js`](https://searchfox.org/mozilla-central/rev/31f5847a4494b3646edabbdd7ea39cb88509afe2/browser/base/content/browser-siteProtections.js#1749). +**Note** you will need to add your new widget to [the switch in importCustomElementFromESModule](https://searchfox.org/mozilla-central/rev/85b4f7363292b272eb9b606e00de2c37a6be73f0/toolkit/content/customElements.js#845-859) for `ensureCustomElements()` to work as expected. +Once [Bug 1803810](https://bugzilla.mozilla.org/show_bug.cgi?id=1803810) lands, this process will be simplified: you won't need to use `ensureCustomElements()` and you will [add your widget to the appropriate array in customElements.js instead of the switch statement](https://searchfox.org/mozilla-central/rev/85b4f7363292b272eb9b606e00de2c37a6be73f0/toolkit/content/customElements.js#818-841). + +## Common pitfalls + +If you're trying to use a reusable widget but nothing is appearing on the +page it may be due to one of the following issues: + +- Omitting the `type="module"` in your `script` tag. +- Wrong file path for the `src` of your imported module. +- Widget is not declared or incorrectly declared in the correct `jar.mn` file. +- Not specifying the `html:` namespace when using a custom HTML element in an + `xhtml` file. For example the tag should look something like this: + + ```html + + ``` +- Adding a `script` tag to an `inc.xhtml` file. For example when using a new + component in the privacy section of `about:preferences` the `script` tag needs + to be added to `preferences.xhtml` rather than to `privacy.inc.xhtml`. +- Trying to extend a built-in HTML element in Lit. [Because Webkit never + implemented support for customized built-ins, Lit doesn't support it either.](https://github.com/lit/lit-element/issues/879#issuecomment-1061892879) + That means if you want to do something like: + + ```js + customElements.define("cool-button", CoolButton, { extends: "button" }); + ``` + + you will need to make a vanilla custom element, you cannot use Lit. + [For an example of extending an HTML element, see `moz-support-link`](https://searchfox.org/mozilla-central/source/toolkit/content/widgets/moz-support-link/moz-support-link.mjs). diff --git a/browser/components/storybook/docs/README.storybook.stories.md b/browser/components/storybook/docs/README.storybook.stories.md new file mode 100644 index 0000000000..bb0fcdd1a2 --- /dev/null +++ b/browser/components/storybook/docs/README.storybook.stories.md @@ -0,0 +1,148 @@ +# Storybook for Firefox + +[Storybook](https://storybook.js.org/) is an interactive tool that creates a +playground for UI components. We use Storybook to document our design system, +reusable components, and any specific components you might want to test with +dummy data. [Take a look at our Storybook +instance!](https://firefoxux.github.io/firefox-desktop-components/?path=/story/docs-reusable-widgets--page) + +## Background + +Storybook lists components that can be reused, and helps document +what common elements we have. It can also list implementation specific +components, but they should be added to the "Domain-Specific UI Widgets" section. + +Changes to files directly referenced from Storybook (so basically non-chrome:// +paths) should automatically reflect changes in the opened browser. If you make a +change to a `chrome://` referenced file then you'll need to do a hard refresh +(Cmd+Shift+R/Ctrl+Shift+R) to notice the changes. If you're on Windows you may +need to `./mach build faster` to have the `chrome://` URL show the latest version. + +## Running Storybook + +Installing the npm dependencies and running the `storybook` npm script should be +enough to get Storybook running. This can be done via `./mach storybook` +commands, or with your personal npm/node that happens to be compatible. + +### Running with mach commands + +This is the recommended approach for installing dependencies and running +Storybook locally. + +To install dependencies, start the Storybook server, and launch the Storybook +site in a local build of Firefox, just run: + +```sh +# This uses npm ci under the hood to install the package-lock.json exactly. +./mach storybook +``` + +This single command will first install any missing dependencies then start the +local Storybook server. It will also start your local browser and point it to +`http://localhost:5703` while enabling certain preferences to ensure components +display as expected (specifically `svg.context-properties.content.enabled` and +`layout.css.light-dark.enabled`). + +It's necessary to use your local build to test in Storybook since `chrome://` +URLs are currently being pulled from the running browser, so any changes to +common-shared.css for example will come from your build. + +The Storybook server will continue running and will watch for component file +changes. + +#### Alternative mach commands + +Although running `./mach storybook` is the most convenient way to interact with +Storybook locally it is also possible to run separate commands to start the +Storybook server and run your local build with the necessary prefs. + +If you only want to start the Storybook server - for example in cases where you +already have a local build running - you can pass a `--no-open` flag to `./mach +storybook`: + +```sh +# Start the storybook server without launching a local Firefox build. +./mach storybook --no-open +``` + +If you just want to spin up a local build of Firefox with the required prefs +enabled you can use the `launch` subcommand: + +```sh +# In another terminal: +./mach storybook launch +``` + +This will run your local browser and point it at `http://localhost:5703`. + +Alternatively, you can simply navigate to `http://localhost:5703/` or run: + +```sh +# In another terminal: +./mach run http://localhost:5703/ +``` + +although with this option certain prefs won't be enabled, so what's displayed in +Storybook may not exactly reflect how components will look when used in Firefox. + +### Personal npm + +You can use your own `npm` to install and run Storybook. Compatibility is up +to you to sort out. + +```sh +cd browser/components/storybook +npm ci # Install the package-lock.json exactly so lockfileVersion won't change. +npm run storybook +``` + +## Updating Storybook dependencies + +On occasion you may need to update or add a npm dependency for Storybook. +This can be done using the version of `npm` packaged with `mach`: + +```sh +# Install a dev dependency from within the storybook directory. +cd browser/components/storybook && ../../../mach npm i -D your-package +``` + +## Adding new stories + +Storybook is currently configured to search for story files (any file with a +`.stories.(js|mjs|md)` extension) in `toolkit/content/widgets` and +`browser/components/storybook/stories`. + +Stories in `toolkit/content/widgets` are used to document design system +components, also known as UI widgets. +As long as you used `./mach addwidget` correctly, there is no additional setup needed to view your newly created story in Storybook. + +Stories in `browser/components/storybook/stories` are used for non-design system components, also called domain-specific UI widgets. +The easiest way to use Storybook for non-design system element is to use `./mach addstory new-component "Your Project"`. +You can also use `./mach addstory new-component "Your Project" --path browser/components/new-component.mjs` where `--path` is the path to your new components' source. +[See the Credential Management/Timeline widget for an example.](https://searchfox.org/mozilla-central/rev/2c11f18f89056a806c299a9d06bfa808718c2e84/browser/components/storybook/stories/login-timeline.stories.mjs#11) + +If you want to colocate your story with the code it is documenting you will need +to add to the `stories` array in the `.storybook/main.js` [configuration +file](https://searchfox.org/mozilla-central/source/browser/components/storybook/.storybook/main.js) +so that Storybook knows where to look for your files. + +The Storybook docs site has a [good +overview](https://storybook.js.org/docs/web-components/get-started/whats-a-story) +of what's involved in writing a new story. For convenience you can use the [Lit +library](https://lit.dev/) to define the template code for your story, but this +is not a requirement. + +### UI Widgets versus Domain-Specific UI Widgets + +Widgets that are part of [our design system](https://acorn.firefox.com/latest/acorn.html) and intended to be used across the Mozilla suite of products live under the "UI Widgets" category in Storybook and under `toolkit/content/widgets/` in Firefox. +These global widgets are denoted in code by the `moz-` prefix in their name. +For example, the name `moz-support-link` informs us that this widget is design system compliant and can be used anywhere in Firefox. + +Storybook can also be used to help document and prototype widgets that are specific to a part of the codebase and not intended for more global use. +Stories for these types of widgets live under the "Domain-Specific UI Widgets" category, while the code can live in any appropriate folder in `mozilla-central`. +[See the Credential Management folder as an example of a domain specific folder](https://firefoxux.github.io/firefox-desktop-components/?path=/docs/domain-specific-ui-widgets-credential-management-timeline--empty-timeline) and [see the login-timeline.stories.mjs for how to make a domain specific folder in Storybook](https://searchfox.org/mozilla-central/source/browser/components/storybook/stories/login-timeline.stories.mjs). +[To add a non-team specific widget to the "Domain-specific UI Widgets" section, see the migration-wizard.stories.mjs file](https://searchfox.org/mozilla-central/source/browser/components/storybook/stories/migration-wizard.stories.mjs). + +Creating and documenting domain specific UI widgets allows other teams to be aware of and take inspiration from existing UI patterns. +With these widgets, **there is no guarantee that the element will work for your domain.** +If you need to use a domain-specific widget outside of its intended domain, it may be worth discussing how to convert this domain specific widget into a global UI widget. diff --git a/browser/components/storybook/docs/README.typography.stories.md b/browser/components/storybook/docs/README.typography.stories.md new file mode 100644 index 0000000000..4b85d59ef8 --- /dev/null +++ b/browser/components/storybook/docs/README.typography.stories.md @@ -0,0 +1,389 @@ +# Typography +## Scale +[In-content pages and the browser chrome](https://acorn.firefox.com/latest/resources/browser-anatomy/desktop-ZaxCgqkt) follow different type scales due to the chrome relying on operating systems' font sizing, while in-content pages follow the type scale set by the design system. + +We set `font: message-box` at the root of `common-shared.css` and `global.css` stylesheets so that both in-content and the chrome can have access to operating system font families. + +We also don't specify line height units and rely on the default. + +### In-content + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameHTML class/tag or CSS tokenPreviewFont sizeFont weight
Heading XLargeh1,
.heading-xlarge
+ ```html story +

The quick brown fox jumps over the lazy dog

+ ``` +
+ 1.6rem (24px) + + 600 +
Heading Largeh2,
.heading-large
+ ```html story +

The quick brown fox jumps over the lazy dog

+ ``` +
+ 1.467rem (22px) + + 600 +
Heading Mediumh3,
.heading-medium
+ ```html story +

The quick brown fox jumps over the lazy dog

+ ``` +
+ 1.133rem (17px) + + 600 +
Root (body)--font-size-root set at the :root of common-shared.css + ```html story +

The quick brown fox jumps over the lazy dog

+ ``` +
+ 15px (1rem) + + normal +
Body Small--font-size-small + ```html story +

The quick brown fox jumps over the lazy dog

+ ``` +
+ 0.867rem (13px) + + normal +
+ +### Chrome + +The chrome solely relies on `font` declarations (it also relies on `font: menu` for panels) so that it can inherit the operating system font family **and** sizing in order for it to feel like it is part of the user's operating system. Keep in mind that font sizes and families vary between macOS, Windows, and Linux. Moreover, you will only see a difference between `font: message-box` and `font: menu` font sizes on macOS. + +Note that there currently isn't a hierarchy of multiple headings on the chrome since every panel and modal that opens from it relies only on an `h1` for its title; so today, we just bold the existing fonts in order to create headings. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameClassPreviewFont keywordFont weight
Menu Headingh1 + ```html story +

The quick brown fox jumps over the lazy dog

+ ``` +
+ menu + + 600 +
MenuApplied directly to panel classes in panel.css and panelUI-shared.css + ```html story +

The quick brown fox jumps over the lazy dog

+ ``` +
+ menu + + normal +
Headingh1 + ```html story +

The quick brown fox jumps over the lazy dog

+ ``` +
+ message-box + + 600 +
Root (body)message-box set at the :root of global.css + ```html story +

The quick brown fox jumps over the lazy dog

+ ``` +
+ message-box + + normal +
+ +## Design tokens +Type setting relies on design tokens for font size and font weight. + +#### Font size + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Base tokenIn-content valueChrome value
+ --font-size-xxlarge + + 1.6rem + + unset +
+ --font-size-xlarge + + 1.467rem + + unset +
+ --font-size-large + + 1.133rem + + unset +
+ --font-size-root + + 15px + + unset +
+ --font-size-small + + 0.867rem + + unset +
+ + +#### Font weight + + + + + + + + + + + + + + + + + + + + +
Base tokenIn-content valueChrome value
+ --font-weight-default + + normal + + normal +
+ --font-weight-bold + + 600 + + 600 +
+ +## Helpers +### text-and-typography.css + +The text and typography stylesheet found in `toolkit/themes/shared/design-system/text-and-typography.css` contains type setting declarations, and text and typography helper classes: + +- It applies the design system's type scale by default, therefore it styles the `root` and headings automatically. +- It comes with helper classes for contexts where designers may visually prefer an `h1` to start at the "medium" heading size instead of "large" (e.g. Shopping sidebar). It also contains text related helpers for truncating and deemphasizing text. + +You should rely on typography helper classes and the defaults set by the design system. + +This file is imported into `common-shared.css` and `global-shared.css` so that both in-content pages and the chrome receive their respective typography scale treatments, and have access to helper classes. + +#### Heading +##### XLarge (h1) +###### In-content +```html story +

Firefox View

+``` + +###### Chrome +```html story +

Close window and quit Firefox?

+``` + +###### Chrome menus +```html story +

Edit bookmark

+``` + +```css story +h1, +.heading-xlarge { + font-weight: var(--font-weight-bold); + font-size: var(--font-size-xxlarge); +} +``` + +*Reminder: There's no hierarchy of headings on the chrome. So here's just in-content's preview:* + +##### Large (h2) +```html story +

Recent browsing

+``` + +```css story +h2, +.heading-large { + font-weight: var(--font-weight-bold); + font-size: var(--font-size-xlarge); +} +``` + +##### Medium (h3) +```html story +

Tabs from other devices

+``` + +```css story +h3, +.heading-medium { + font-weight: var(--font-weight-bold); + font-size: var(--font-size-large); +} +``` + +#### Text +##### De-emphasized + +```html story + Get your passwords on your other devices. +``` + +```css story +.text-deemphasized { + font-size: var(--font-size-small); + color: var(--text-color-deemphasized); +} +``` + +##### Truncated ellipsis + +```html story +
A really long piece of text a really long piece of text a really long piece of text a really long piece of text a really long piece of text a really long piece of text.
+``` + +```css story +.text-truncated-ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +``` + +`.text-truncated-ellipsis` can be applied to `display: block` or `display: inline-block` elements. + +For `display: flex` or `display: grid` elements, you'll need to wrap its contents with an element with the `.text-truncated-ellipsis` class instead. + +Example: + +```html +
+ A really long string of text that needs truncation. +
+``` diff --git a/browser/components/storybook/docs/README.xul-and-html.stories.md b/browser/components/storybook/docs/README.xul-and-html.stories.md new file mode 100644 index 0000000000..8d2d4cb7bf --- /dev/null +++ b/browser/components/storybook/docs/README.xul-and-html.stories.md @@ -0,0 +1,67 @@ +# XUL and HTML + +This document gives a quick overview of XUL and HTML, especially as it pertains to desktop front-end developers. +As we migrate away from XUL elements to HTML elements where possible, it is important to note the differences between these two technologies. +Additionally it is helpful to know how to use both where needed, as some elements will still need to use XUL. + +## What is XUL + +XUL is an XML-based language for building cross-platform user interfaces and applications, so all the features of XML are available in XUL as well. +This is in contrast to HTML which is intended for developing web pages. +Because of this XUL is oriented towards application artifacts such as windows, scrollbars, and menus instead of pages, headings, links, etc. +These XUL elements are provided to an HTML page without the page having any control over them. + +XUL was and is used to create various UI elements, for example: +- Input controls such as textboxes and checkboxes +- Toolbars with buttons or other content +- Menus on a menu bar or pop up menus +- Tabbed dialogs +- Trees for hierarchical or tabular information + +XUL is a Mozilla-specific technology with many similarities but also many differences to HTML. +These include a different box model (although it is now synthesized in the HTML box model) and the ability to be backed by C++ code. + +## What requires XUL + +While many of the existing XUL elements that make up the browser can be migrated to HTML, there are some elements that require XUL. +These elements tend to be fundamental to the browser such as windows, popups, panels, etc. +Elements that need to emulate OS-specific styling also tend to be XUL elements. +While there are parts of these elements that must be XUL, that does not mean that the component must be entirely implmented in XUL. +For example, we require that a `panel` can be drawn outside of a window's bounds, but then we can have HTML inside of that `panel` element. + +The following is not an exhaustive list of elements that require XUL: +- Browser Window + - https://searchfox.org/mozilla-central/source/xpfe/appshell/nsIXULBrowserWindow.idl +- Popups + - https://searchfox.org/mozilla-central/source/dom/webidl/XULPopupElement.webidl + - https://searchfox.org/mozilla-central/source/layout/xul/nsMenuPopupFrame.cpp + - https://searchfox.org/mozilla-central/source/toolkit/content/widgets/autocomplete-popup.js + - https://searchfox.org/mozilla-central/source/toolkit/content/widgets/menupopup.js +- Panels + - https://searchfox.org/mozilla-central/source/toolkit/content/widgets/panel.js + +## When to use HTML or XUL + +Now that HTML is powerful enough for us to create almost an entire application with it, the need for the features of XUL has diminished. +We now prefer to write HTML components over XUL components since that model is better understood by the web and front-end community. +This also allows us to gain new features of the web in the UI that we write without backporting them to XUL. + +There are some cases where XUL may still be required for non-standard functionality. +Some XUL elements have more functions over similar HTML elements, such as the XUL `` element compared to the HTML `