summaryrefslogtreecommitdiffstats
path: root/browser/components/storybook/.storybook
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/storybook/.storybook')
-rw-r--r--browser/components/storybook/.storybook/addon-component-status/StatusIndicator.jsx (renamed from browser/components/storybook/.storybook/addon-component-status/StatusIndicator.mjs)4
-rw-r--r--browser/components/storybook/.storybook/addon-component-status/preset/manager.mjs6
-rw-r--r--browser/components/storybook/.storybook/addon-fluent/FluentPanel.jsx110
-rw-r--r--browser/components/storybook/.storybook/addon-fluent/FluentPanel.mjs121
-rw-r--r--browser/components/storybook/.storybook/addon-fluent/PseudoLocalizationButton.jsx (renamed from browser/components/storybook/.storybook/addon-fluent/PseudoLocalizationButton.mjs)2
-rw-r--r--browser/components/storybook/.storybook/addon-fluent/fluent-panel.css64
-rw-r--r--browser/components/storybook/.storybook/addon-fluent/preset/manager.mjs13
-rw-r--r--browser/components/storybook/.storybook/addon-fluent/withPseudoLocalization.mjs11
-rw-r--r--browser/components/storybook/.storybook/main.js49
-rw-r--r--browser/components/storybook/.storybook/markdown-story-indexer.js50
-rw-r--r--browser/components/storybook/.storybook/markdown-story-loader.js123
-rw-r--r--browser/components/storybook/.storybook/markdown-story-utils.js197
-rw-r--r--browser/components/storybook/.storybook/preview-head.html9
-rw-r--r--browser/components/storybook/.storybook/preview.mjs30
14 files changed, 455 insertions, 334 deletions
diff --git a/browser/components/storybook/.storybook/addon-component-status/StatusIndicator.mjs b/browser/components/storybook/.storybook/addon-component-status/StatusIndicator.jsx
index 05d72146cb..581d63a6fa 100644
--- a/browser/components/storybook/.storybook/addon-component-status/StatusIndicator.mjs
+++ b/browser/components/storybook/.storybook/addon-component-status/StatusIndicator.jsx
@@ -4,7 +4,7 @@
// eslint-disable-next-line no-unused-vars
import React from "react";
-import { useParameter } from "@storybook/api";
+import { useParameter } from "@storybook/manager-api";
import {
// eslint-disable-next-line no-unused-vars
Badge,
@@ -85,7 +85,7 @@ export const StatusIndicator = () => {
style={{
display: "flex",
}}
- onVisibilityChange={onVisibilityChange}
+ onVisibleChange={onVisibilityChange}
tooltip={() => (
<div id="statusMessage">
<TooltipMessage
diff --git a/browser/components/storybook/.storybook/addon-component-status/preset/manager.mjs b/browser/components/storybook/.storybook/addon-component-status/preset/manager.mjs
index 4aa611b156..3fe5eb3f1b 100644
--- a/browser/components/storybook/.storybook/addon-component-status/preset/manager.mjs
+++ b/browser/components/storybook/.storybook/addon-component-status/preset/manager.mjs
@@ -4,11 +4,9 @@
/** 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 { addons, types } from "@storybook/manager-api";
import { ADDON_ID, TOOL_ID } from "../constants.mjs";
-import { StatusIndicator } from "../StatusIndicator.mjs";
+import { StatusIndicator } from "../StatusIndicator.jsx";
addons.register(ADDON_ID, () => {
addons.add(TOOL_ID, {
diff --git a/browser/components/storybook/.storybook/addon-fluent/FluentPanel.jsx b/browser/components/storybook/.storybook/addon-fluent/FluentPanel.jsx
new file mode 100644
index 0000000000..71714d8d25
--- /dev/null
+++ b/browser/components/storybook/.storybook/addon-fluent/FluentPanel.jsx
@@ -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/. */
+
+// eslint-disable-next-line no-unused-vars
+import React, { useEffect, useState } from "react";
+import { addons, useGlobals, useStorybookApi } from "@storybook/manager-api";
+// eslint-disable-next-line no-unused-vars
+import { AddonPanel, Table, Form } 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 const FluentPanel = ({ active }) => {
+ const [fileName, setFileName] = useState(null);
+ const [strings, setStrings] = useState([]);
+ const [{ fluentStrings }, updateGlobals] = useGlobals();
+ const channel = addons.getChannel();
+ const api = useStorybookApi();
+
+ useEffect(() => {
+ channel.on(FLUENT_CHANGED, handleFluentChanged);
+ return () => {
+ channel.off(FLUENT_CHANGED, handleFluentChanged);
+ };
+ }, [channel]);
+
+ const handleFluentChanged = (nextStrings, fluentFile) => {
+ setFileName(fluentFile);
+ setStrings(nextStrings);
+ };
+
+ const onInput = e => {
+ let nextStrings = [];
+ for (let [key, value] of strings) {
+ if (key == e.target.name) {
+ let stringValue = e.target.value;
+ if (stringValue.startsWith(".")) {
+ stringValue = "\n" + stringValue;
+ }
+ nextStrings.push([key, stringValue]);
+ } else {
+ nextStrings.push([key, value]);
+ }
+ }
+ let stringified = nextStrings
+ .map(([key, value]) => `${key} = ${value}`)
+ .join("\n");
+ channel.emit(FLUENT_SET_STRINGS, stringified);
+ updateGlobals({
+ fluentStrings: { ...fluentStrings, [fileName]: nextStrings },
+ });
+ return { fileName, strings };
+ };
+
+ const addonTemplate = () => {
+ if (strings.length === 0) {
+ return (
+ <AddonPanel active={!!active} api={api}>
+ <div className="addon-panel-body">
+ <div className="addon-panel-message">
+ This story is not configured to use Fluent.
+ </div>
+ </div>
+ </AddonPanel>
+ );
+ }
+
+ return (
+ <AddonPanel active={!!active} api={api}>
+ <div className="addon-panel-body">
+ <Table aria-hidden="false" className="addon-panel-table">
+ <thead className="addon-panel-table-head">
+ <tr>
+ <th>
+ <span>Identifier</span>
+ </th>
+ <th>
+ <span>String</span>
+ </th>
+ </tr>
+ </thead>
+ <tbody className="addon-panel-table-body">
+ {strings.map(([identifier, value]) => (
+ <tr key={identifier}>
+ <td>
+ <span>{identifier}</span>
+ </td>
+ <td>
+ <Form.Textarea
+ name={identifier}
+ onInput={onInput}
+ defaultValue={value
+ .trim()
+ .split("\n")
+ .map(s => s.trim())
+ .join("\n")}
+ ></Form.Textarea>
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </Table>
+ </div>
+ </AddonPanel>
+ );
+ };
+
+ return addonTemplate();
+};
diff --git a/browser/components/storybook/.storybook/addon-fluent/FluentPanel.mjs b/browser/components/storybook/.storybook/addon-fluent/FluentPanel.mjs
deleted file mode 100644
index 692ff73737..0000000000
--- a/browser/components/storybook/.storybook/addon-fluent/FluentPanel.mjs
+++ /dev/null
@@ -1,121 +0,0 @@
-/* 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 (
- <AddonPanel active={!!active} api={api}>
- <div className="addon-panel-body">
- <div className="addon-panel-message">
- This story is not configured to use Fluent.
- </div>
- </div>
- </AddonPanel>
- );
- }
-
- return (
- <AddonPanel active={!!active} api={api}>
- <div className="addon-panel-body">
- <table aria-hidden="false" className="addon-panel-table">
- <thead className="addon-panel-table-head">
- <tr>
- <th>
- <span>Identifier</span>
- </th>
- <th>
- <span>String</span>
- </th>
- </tr>
- </thead>
- <tbody className="addon-panel-table-body">
- {strings.map(([identifier, value]) => (
- <tr key={identifier}>
- <td>
- <span>{identifier}</span>
- </td>
- <td>
- <label>
- <textarea
- name={identifier}
- onInput={this.onInput}
- defaultValue={value
- .trim()
- .split("\n")
- .map(s => s.trim())
- .join("\n")}
- ></textarea>
- </label>
- </td>
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- </AddonPanel>
- );
- }
-}
diff --git a/browser/components/storybook/.storybook/addon-fluent/PseudoLocalizationButton.mjs b/browser/components/storybook/.storybook/addon-fluent/PseudoLocalizationButton.jsx
index d60112d224..9af7e5ad2b 100644
--- a/browser/components/storybook/.storybook/addon-fluent/PseudoLocalizationButton.mjs
+++ b/browser/components/storybook/.storybook/addon-fluent/PseudoLocalizationButton.jsx
@@ -4,7 +4,7 @@
// eslint-disable-next-line no-unused-vars
import React from "react";
-import { useGlobals } from "@storybook/api";
+import { useGlobals } from "@storybook/manager-api";
import {
// eslint-disable-next-line no-unused-vars
Icons,
diff --git a/browser/components/storybook/.storybook/addon-fluent/fluent-panel.css b/browser/components/storybook/.storybook/addon-fluent/fluent-panel.css
index 75f4562820..cb6dd9b6f2 100644
--- a/browser/components/storybook/.storybook/addon-fluent/fluent-panel.css
+++ b/browser/components/storybook/.storybook/addon-fluent/fluent-panel.css
@@ -15,69 +15,59 @@
font-size: 13px;
}
-.addon-panel-table {
- border-collapse: collapse;
+table.addon-panel-table {
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);
+table.addon-panel-table thead.addon-panel-table-head th {
+ border: none;
+ color: rgba(46, 52, 56, 0.75);
}
-.addon-panel-table-head th {
- padding: 10px 15px;
- border: none;
- vertical-align: top;
+@media (prefers-color-scheme: dark) {
+ table.addon-panel-table thead.addon-panel-table-head th {
+ color: rgba(201, 205, 207, 0.55)
+ }
+}
+
+table.addon-panel-table thead.addon-panel-table-head tr {
+ border-top: none;
}
-.addon-panel-table-head th:first-of-type, .addon-panel-table-body td:first-of-type {
+.addon-panel-table-head th:first-of-type,
+.addon-panel-table-body td:first-of-type {
width: 25%;
padding-left: 20px;
+ border-inline: none;
}
-.addon-panel-table-head th:last-of-type, .addon-panel-table-body td:last-of-type {
+.addon-panel-table-head th:last-of-type,
+.addon-panel-table-body td:last-of-type {
padding-right: 20px;
+ border-inline-start: none;
}
-.addon-panel-table-body {
- border-radius: 4px;
-}
-
-.addon-panel-table-body tr {
+.addon-panel-body {
overflow: hidden;
- border-top: 1px solid #e6e6e6;
}
.addon-panel-table-body td {
- padding: 10px 15px;
font-weight: bold;
+ vertical-align: top;
}
-.addon-panel-table-body label {
- display: flex;
+table.addon-panel-table .addon-panel-table-body tr:nth-of-type(2n) {
+ background-color: unset;
+}
+
+.addon-panel-table-body tr:last-of-type td {
+ border-bottom: none;
}
.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;
+ width: 100%;
}
diff --git a/browser/components/storybook/.storybook/addon-fluent/preset/manager.mjs b/browser/components/storybook/.storybook/addon-fluent/preset/manager.mjs
index 0f7ff9299b..29b57812bd 100644
--- a/browser/components/storybook/.storybook/addon-fluent/preset/manager.mjs
+++ b/browser/components/storybook/.storybook/addon-fluent/preset/manager.mjs
@@ -4,13 +4,10 @@
/** 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 { addons, types } from "@storybook/manager-api";
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";
+import { PseudoLocalizationButton } from "../PseudoLocalizationButton.jsx";
+import { FluentPanel } from "../FluentPanel.jsx";
// Register the addon.
addons.register(ADDON_ID, api => {
@@ -27,8 +24,6 @@ addons.register(ADDON_ID, api => {
title: "Fluent",
//👇 Sets the type of UI element in Storybook
type: types.PANEL,
- render: ({ active, key }) => (
- <FluentPanel active={active} api={api} key={key}></FluentPanel>
- ),
+ render: ({ active }) => FluentPanel({ active }),
});
});
diff --git a/browser/components/storybook/.storybook/addon-fluent/withPseudoLocalization.mjs b/browser/components/storybook/.storybook/addon-fluent/withPseudoLocalization.mjs
index 9d6c62af38..74fd67a6cd 100644
--- a/browser/components/storybook/.storybook/addon-fluent/withPseudoLocalization.mjs
+++ b/browser/components/storybook/.storybook/addon-fluent/withPseudoLocalization.mjs
@@ -2,7 +2,7 @@
* 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 { useEffect, useGlobals, useChannel } from "@storybook/preview-api";
import {
DIRECTIONS,
DIRECTION_BY_STRATEGY,
@@ -25,11 +25,11 @@ 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();
+ const emit = useChannel({});
useEffect(() => {
if (pseudoStrategy) {
- channel.emit(UPDATE_STRATEGY_EVENT, pseudoStrategy);
+ emit(UPDATE_STRATEGY_EVENT, pseudoStrategy);
}
}, [pseudoStrategy]);
@@ -56,8 +56,7 @@ export const withPseudoLocalization = (StoryFn, context) => {
*/
export const withFluentStrings = (StoryFn, context) => {
const [{ fluentStrings }, updateGlobals] = useGlobals();
- const channel = addons.getChannel();
-
+ const emit = useChannel({});
const fileName = context.component + ".ftl";
let strings = [];
@@ -83,7 +82,7 @@ export const withFluentStrings = (StoryFn, context) => {
}
}
- channel.emit(FLUENT_CHANGED, strings);
+ emit(FLUENT_CHANGED, strings, fileName);
return StoryFn();
};
diff --git a/browser/components/storybook/.storybook/main.js b/browser/components/storybook/.storybook/main.js
index 3e42f778a4..5791d1e492 100644
--- a/browser/components/storybook/.storybook/main.js
+++ b/browser/components/storybook/.storybook/main.js
@@ -6,6 +6,7 @@
const path = require("path");
const webpack = require("webpack");
const rewriteChromeUri = require("./chrome-uri-utils.js");
+const mdIndexer = require("./markdown-story-indexer.js");
const projectRoot = path.resolve(__dirname, "../../../../");
@@ -37,34 +38,35 @@ module.exports = {
path.resolve(__dirname, "addon-fluent"),
path.resolve(__dirname, "addon-component-status"),
],
- framework: "@storybook/web-components",
+ framework: {
+ name: "@storybook/web-components-webpack5",
+ options: {},
+ },
+
+ experimental_indexers: async existingIndexers => {
+ const customIndexer = {
+ test: /(stories|story)\.md$/,
+ createIndex: mdIndexer,
+ };
+ return [...existingIndexers, customIndexer];
+ },
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.resolve.alias = {
+ browser: `${projectRoot}/browser`,
+ toolkit: `${projectRoot}/toolkit`,
+ "toolkit-widgets": `${projectRoot}/toolkit/content/widgets/`,
+ "lit.all.mjs": `${projectRoot}/toolkit/content/widgets/vendor/lit.all.mjs`,
+ react: "browser/components/storybook/node_modules/react",
+ "react/jsx-runtime":
+ "browser/components/storybook/node_modules/react/jsx-runtime",
+ "@storybook/addon-docs":
+ "browser/components/storybook/node_modules/@storybook/addon-docs",
+ };
config.plugins.push(
// Rewrite chrome:// URI imports to file system paths.
@@ -147,7 +149,4 @@ module.exports = {
// Return the altered config
return config;
},
- core: {
- builder: "webpack5",
- },
};
diff --git a/browser/components/storybook/.storybook/markdown-story-indexer.js b/browser/components/storybook/.storybook/markdown-story-indexer.js
new file mode 100644
index 0000000000..7a03f94844
--- /dev/null
+++ b/browser/components/storybook/.storybook/markdown-story-indexer.js
@@ -0,0 +1,50 @@
+/* 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 { loadCsf } = require("@storybook/csf-tools");
+const { compile } = require("@storybook/mdx2-csf");
+const { getStoryTitle, getMDXSource } = require("./markdown-story-utils.js");
+const fs = require("fs");
+
+/**
+ * Function that tells Storybook how to index markdown based stories. This is
+ * responsible for Storybook knowing how to populate the sidebar in the
+ * Storybook UI, then retrieve the relevant file when a story is selected. In
+ * order to get the data Storybook needs, we have to convert the markdown to
+ * MDX, the convert that to CSF.
+ * More info on indexers can be found here: storybook.js.org/docs/api/main-config-indexers
+ * @param {string} fileName - Path to the file being processed.
+ * @param {Object} opts - Options to configure the indexer.
+ * @returns Array of IndexInput objects.
+ */
+module.exports = async (fileName, opts) => {
+ // eslint-disable-next-line no-unsanitized/method
+ const content = fs.readFileSync(fileName, "utf8");
+ const title = getStoryTitle(fileName);
+ const code = getMDXSource(content, title);
+
+ // Compile MDX into CSF
+ const csfCode = await compile(code, opts);
+
+ // Parse CSF component
+ let csf = loadCsf(csfCode, { fileName, makeTitle: () => title }).parse();
+
+ // Return an array of story indexes.
+ // Cribbed from https://github.com/storybookjs/storybook/blob/4169cd5b4ec9111de69f64a5e06edab9a6d2b0b8/code/addons/docs/src/preset.ts#L189
+ const { indexInputs, stories } = csf;
+ return indexInputs.map((input, index) => {
+ const docsOnly = stories[index].parameters?.docsOnly;
+ const tags = input.tags ? input.tags : [];
+ if (docsOnly) {
+ tags.push("stories-mdx-docsOnly");
+ }
+ // the mdx-csf compiler automatically adds the 'stories-mdx' tag to meta,
+ // here' we're just making sure it is always there
+ if (!tags.includes("stories-mdx")) {
+ tags.push("stories-mdx");
+ }
+ return { ...input, tags };
+ });
+};
diff --git a/browser/components/storybook/.storybook/markdown-story-loader.js b/browser/components/storybook/.storybook/markdown-story-loader.js
index b11036af74..f1848d0e82 100644
--- a/browser/components/storybook/.storybook/markdown-story-loader.js
+++ b/browser/components/storybook/.storybook/markdown-story-loader.js
@@ -15,71 +15,7 @@
* 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(?<code>[\s\S]*?)```/g;
- // $code comes from the <code> 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,
- "<Canvas withSource='none'><with-common-styles>\n$<code></with-common-styles></Canvas>"
- );
-}
+const { getStoryTitle, getMDXSource } = require("./markdown-story-utils.js");
/**
* The WebpackLoader export. Takes markdown as its source and returns a docs
@@ -89,61 +25,8 @@ function parseStoriesFromMarkdown(source) {
* @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\/)(?<name>.*?)(?=\/)/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}";` : ""}
-
-<Meta
- title="${title}"
- ${componentStories ? `of={Stories}` : ""}
- parameters={{
- previewTabs: {
- canvas: { hidden: true },
- },
- viewMode: "docs",
- }}
-/>
-
-${parseStoriesFromMarkdown(source)}`;
-
+ let title = getStoryTitle(this.resourcePath);
+ let mdxSource = getMDXSource(source, title, this.resourcePath);
return mdxSource;
};
diff --git a/browser/components/storybook/.storybook/markdown-story-utils.js b/browser/components/storybook/.storybook/markdown-story-utils.js
new file mode 100644
index 0000000000..1cc78164ad
--- /dev/null
+++ b/browser/components/storybook/.storybook/markdown-story-utils.js
@@ -0,0 +1,197 @@
+/* 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 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 getTitleFromPath(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(?<code>[\s\S]*?)```/g;
+ // $code comes from the <code> 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,
+ "<Canvas withSource='none'><with-common-styles>\n$<code></with-common-styles></Canvas>"
+ );
+}
+
+/**
+ * Finds the name of the component for files in toolkit widgets.
+ * @param {string} resourcePath - Path to the file being processed.
+ * @returns The component name e.g. "moz-toggle"
+ */
+function getComponentName(resourcePath) {
+ let componentName = "";
+ if (resourcePath.includes("toolkit/content/widgets")) {
+ let storyNameRegex = /(?<=\/widgets\/)(?<name>.*?)(?=\/)/g;
+ componentName = storyNameRegex.exec(resourcePath)?.groups?.name;
+ }
+ return componentName;
+}
+
+/**
+ * Figures out where a markdown based story should live in Storybook i.e.
+ * whether it belongs under "Docs" or "UI Widgets" as well as what name to
+ * display in the sidebar.
+ * @param {string} resourcePath - Path to the file being processed.
+ * @returns {string} The title of the story.
+ */
+function getStoryTitle(resourcePath) {
+ // Currently we sort docs only stories under "Docs" by default.
+ let storyPath = "Docs";
+
+ let relativePath = path
+ .relative(projectRoot, resourcePath)
+ .replaceAll(path.sep, "/");
+
+ let componentName = getComponentName(relativePath);
+ 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 = getTitleFromPath(relativePath);
+ let title = storyTitle.includes("/")
+ ? storyTitle
+ : `${storyPath}/${storyTitle}`;
+ return title;
+}
+
+/**
+ * Figures out the path to import a component for cases where we have
+ * interactive examples in the docs that require the component to have been
+ * loaded. This wasn't necessary prior to Storybook V7 since everything was
+ * loaded up front; now stories are loaded on demand.
+ * @param {string} resourcePath - Path to the file being processed.
+ * @returns Path used to import a component into a story.
+ */
+function getImportPath(resourcePath) {
+ // Limiting this to toolkit widgets for now since we don't have any
+ // interactive examples in other docs stories.
+ if (!resourcePath.includes("toolkit/content/widgets")) {
+ return "";
+ }
+ let componentName = getComponentName(resourcePath);
+ let fileExtension = "";
+ if (componentName) {
+ let mjsPath = resourcePath.replace(
+ "README.stories.md",
+ `${componentName}.mjs`
+ );
+ let jsPath = resourcePath.replace(
+ "README.stories.md",
+ `${componentName}.js`
+ );
+
+ if (fs.existsSync(mjsPath)) {
+ fileExtension = "mjs";
+ } else if (fs.existsSync(jsPath)) {
+ fileExtension = "js";
+ } else {
+ return "";
+ }
+ }
+ return `"toolkit-widgets/${componentName}/${componentName}.${fileExtension}"`;
+}
+
+/**
+ * Takes markdown and re-writes it to MDX. Conditionally includes a table of
+ * arguments when we're documenting a component.
+ * @param {string} source - The markdown source to rewrite to MDX.
+ * @param {string} title - The title of the story.
+ * @param {string} resourcePath - Path to the file being processed.
+ * @returns The markdown source converted to MDX.
+ */
+function getMDXSource(source, title, resourcePath = "") {
+ let importPath = getImportPath(resourcePath);
+ let componentName = getComponentName(resourcePath);
+
+ // 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, Canvas, ArgTypes } from "@storybook/addon-docs";
+${importPath ? `import ${importPath};` : ""}
+
+<Meta
+ title="${title}"
+ parameters={{
+ previewTabs: {
+ canvas: { hidden: true },
+ },
+ viewMode: "docs",
+ }}
+/>
+
+${parseStoriesFromMarkdown(source)}
+
+${
+ importPath &&
+ `
+## Args Table
+
+<ArgTypes of={"${componentName}"} />
+`
+}`;
+
+ return mdxSource;
+}
+
+module.exports = {
+ getMDXSource,
+ getStoryTitle,
+};
diff --git a/browser/components/storybook/.storybook/preview-head.html b/browser/components/storybook/.storybook/preview-head.html
index c2f9e8d1a2..206972e714 100644
--- a/browser/components/storybook/.storybook/preview-head.html
+++ b/browser/components/storybook/.storybook/preview-head.html
@@ -21,6 +21,15 @@
text-decoration: underline !important;
}
+ div:has(p, .toc-wrapper) {
+ width: unset !important;
+ min-width: 10rem !important;
+ }
+
+ .toc-wrapper {
+ margin-inline-end: 8px;
+ }
+
/* Override the default Storybook padding in favour of styles
provided by our WithCommonStyles wrapper */
.sb-show-main.sb-main-padded {
diff --git a/browser/components/storybook/.storybook/preview.mjs b/browser/components/storybook/.storybook/preview.mjs
index acff43b7c0..4e0f3f407d 100644
--- a/browser/components/storybook/.storybook/preview.mjs
+++ b/browser/components/storybook/.storybook/preview.mjs
@@ -94,15 +94,27 @@ class WithCommonStyles extends MozLitElement {
customElements.define("with-common-styles", WithCommonStyles);
// Wrap all stories in `with-common-styles`.
-export const decorators = [
- (story, context) =>
- html`
- <with-common-styles
- .story=${story}
- .context=${context}
- ></with-common-styles>
- `,
-];
+export default {
+ decorators: [
+ (story, context) =>
+ html`
+ <with-common-styles
+ .story=${story}
+ .context=${context}
+ ></with-common-styles>
+ `,
+ ],
+ parameters: {
+ docs: {
+ toc: {
+ disable: false,
+ headingSelector: "h2, h3",
+ ignoreSelector: "h2.text-truncated-ellipsis, .toc-ignore",
+ title: "On this page",
+ },
+ },
+ },
+};
// Enable props tables documentation.
setCustomElementsManifest(customElementsManifest);