storybook-addon-pseudo-states 1.15.4 → 1.15.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.babelrc.js CHANGED
@@ -1,4 +1,14 @@
1
+ const modules = process.env.BABEL_ESM === 'true' ? false : 'auto';
2
+
1
3
  module.exports = {
2
- targets: "defaults",
3
- presets: ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"],
4
+ presets: [
5
+ [
6
+ "@babel/preset-env",
7
+ {
8
+ targets: 'defaults',
9
+ modules
10
+ }
11
+ ],
12
+ "@babel/preset-react"
13
+ ],
4
14
  }
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.PseudoStateTool = void 0;
7
+ var _react = _interopRequireWildcard(require("react"));
8
+ var _api = require("@storybook/api");
9
+ var _components = require("@storybook/components");
10
+ var _theming = require("@storybook/theming");
11
+ var _constants = require("./constants");
12
+ function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
13
+ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
14
+ const LinkTitle = _theming.styled.span(_ref => {
15
+ let {
16
+ active
17
+ } = _ref;
18
+ return {
19
+ color: active ? _theming.color.secondary : "inherit"
20
+ };
21
+ });
22
+ const LinkIcon = (0, _theming.styled)(_components.Icons)(_ref2 => {
23
+ let {
24
+ active
25
+ } = _ref2;
26
+ return {
27
+ opacity: active ? 1 : 0,
28
+ path: {
29
+ fill: active ? _theming.color.secondary : "inherit"
30
+ }
31
+ };
32
+ });
33
+ const options = Object.keys(_constants.PSEUDO_STATES).sort();
34
+ const PseudoStateTool = () => {
35
+ const [{
36
+ pseudo
37
+ }, updateGlobals] = (0, _api.useGlobals)();
38
+ const isActive = (0, _react.useCallback)(option => {
39
+ if (!pseudo) return false;
40
+ return pseudo[option] === true;
41
+ }, [pseudo]);
42
+ const toggleOption = (0, _react.useCallback)(option => () => updateGlobals({
43
+ pseudo: {
44
+ ...pseudo,
45
+ [option]: !isActive(option)
46
+ }
47
+ }), [pseudo]);
48
+ return /*#__PURE__*/_react.default.createElement(_components.WithTooltip, {
49
+ placement: "top",
50
+ trigger: "click",
51
+ tooltip: () => /*#__PURE__*/_react.default.createElement(_components.TooltipLinkList, {
52
+ links: options.map(option => ({
53
+ id: option,
54
+ title: /*#__PURE__*/_react.default.createElement(LinkTitle, {
55
+ active: isActive(option)
56
+ }, ":", _constants.PSEUDO_STATES[option]),
57
+ right: /*#__PURE__*/_react.default.createElement(LinkIcon, {
58
+ icon: "check",
59
+ width: 12,
60
+ height: 12,
61
+ active: isActive(option)
62
+ }),
63
+ onClick: toggleOption(option),
64
+ active: isActive(option)
65
+ }))
66
+ })
67
+ }, /*#__PURE__*/_react.default.createElement(_components.IconButton, {
68
+ key: "pseudo-state",
69
+ title: "Select CSS pseudo states",
70
+ active: options.some(isActive)
71
+ }, /*#__PURE__*/_react.default.createElement(_components.Icons, {
72
+ icon: "button"
73
+ })));
74
+ };
75
+ exports.PseudoStateTool = PseudoStateTool;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.TOOL_ID = exports.PSEUDO_STATES = exports.EXCLUDED_PSEUDO_ELEMENTS = exports.ADDON_ID = void 0;
7
+ const ADDON_ID = "storybook/pseudo-states";
8
+ exports.ADDON_ID = ADDON_ID;
9
+ const TOOL_ID = `${ADDON_ID}/tool`;
10
+
11
+ // Pseudo-elements which are not allowed to have classes applied on them
12
+ // E.g. ::-webkit-scrollbar-thumb.pseudo-hover is not a valid selector
13
+ exports.TOOL_ID = TOOL_ID;
14
+ const EXCLUDED_PSEUDO_ELEMENTS = ["::-webkit-scrollbar-thumb"];
15
+
16
+ // Dynamic pseudo-classes
17
+ // @see https://www.w3.org/TR/2018/REC-selectors-3-20181106/#dynamic-pseudos
18
+ exports.EXCLUDED_PSEUDO_ELEMENTS = EXCLUDED_PSEUDO_ELEMENTS;
19
+ const PSEUDO_STATES = {
20
+ hover: "hover",
21
+ active: "active",
22
+ focusVisible: "focus-visible",
23
+ focusWithin: "focus-within",
24
+ focus: "focus",
25
+ // must come after its alternatives
26
+ visited: "visited",
27
+ link: "link",
28
+ target: "target"
29
+ };
30
+ exports.PSEUDO_STATES = PSEUDO_STATES;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+
3
+ var _addons = require("@storybook/addons");
4
+ var _constants = require("../constants");
5
+ var _PseudoStateTool = require("../PseudoStateTool");
6
+ _addons.addons.register(_constants.ADDON_ID, () => {
7
+ _addons.addons.add(_constants.TOOL_ID, {
8
+ type: _addons.types.TOOL,
9
+ title: "CSS pseudo states",
10
+ match: _ref => {
11
+ let {
12
+ viewMode
13
+ } = _ref;
14
+ return viewMode === "story";
15
+ },
16
+ render: _PseudoStateTool.PseudoStateTool
17
+ });
18
+ });
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.decorators = void 0;
7
+ var _withPseudoState = require("../withPseudoState");
8
+ const decorators = [_withPseudoState.withPseudoState];
9
+ exports.decorators = decorators;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.rewriteStyleSheet = void 0;
7
+ var _constants = require("./constants");
8
+ var _splitSelectors = require("./splitSelectors");
9
+ const pseudoStates = Object.values(_constants.PSEUDO_STATES);
10
+ const matchOne = new RegExp(`:(${pseudoStates.join("|")})`);
11
+ const matchAll = new RegExp(`:(${pseudoStates.join("|")})`, "g");
12
+ const warnings = new Set();
13
+ const warnOnce = message => {
14
+ if (warnings.has(message)) return;
15
+ // eslint-disable-next-line no-console
16
+ console.warn(message);
17
+ warnings.add(message);
18
+ };
19
+ const isExcludedPseudoElement = (selector, pseudoState) => _constants.EXCLUDED_PSEUDO_ELEMENTS.some(element => selector.endsWith(`${element}:${pseudoState}`));
20
+ const rewriteRule = (cssText, selectorText, shadowRoot) => {
21
+ return cssText.replace(selectorText, (0, _splitSelectors.splitSelectors)(selectorText).flatMap(selector => {
22
+ if (selector.includes(".pseudo-")) {
23
+ return [];
24
+ }
25
+ if (!matchOne.test(selector)) {
26
+ return [selector];
27
+ }
28
+ const states = [];
29
+ const plainSelector = selector.replace(matchAll, (_, state) => {
30
+ states.push(state);
31
+ return "";
32
+ });
33
+ const classSelector = states.reduce((acc, state) => !isExcludedPseudoElement(selector, state) && acc.replace(new RegExp(`(?<!Y):${state}`, "g"), `.pseudo-${state}`), selector);
34
+ if (selector.startsWith(":host(") || selector.startsWith("::slotted(")) {
35
+ return [selector, classSelector].filter(Boolean);
36
+ }
37
+ const ancestorSelector = shadowRoot ? `:host(${states.map(s => `.pseudo-${s}`).join("")}) ${plainSelector}` : `${states.map(s => `.pseudo-${s}`).join("")} ${plainSelector}`;
38
+ return [selector, classSelector, ancestorSelector].filter(selector => selector && !selector.includes(":not()"));
39
+ }).join(", "));
40
+ };
41
+
42
+ // Rewrites the style sheet to add alternative selectors for any rule that targets a pseudo state.
43
+ // A sheet can only be rewritten once, and may carry over between stories.
44
+ const rewriteStyleSheet = (sheet, shadowRoot, shadowHosts) => {
45
+ if (sheet.__pseudoStatesRewritten) return;
46
+ sheet.__pseudoStatesRewritten = true;
47
+ try {
48
+ let index = 0;
49
+ for (const {
50
+ cssText,
51
+ selectorText
52
+ } of sheet.cssRules) {
53
+ if (matchOne.test(selectorText)) {
54
+ const newRule = rewriteRule(cssText, selectorText, shadowRoot);
55
+ sheet.deleteRule(index);
56
+ sheet.insertRule(newRule, index);
57
+ if (shadowRoot) shadowHosts.add(shadowRoot.host);
58
+ }
59
+ index++;
60
+ if (index > 1000) {
61
+ warnOnce("Reached maximum of 1000 pseudo selectors per sheet, skipping the rest.");
62
+ break;
63
+ }
64
+ }
65
+ } catch (e) {
66
+ if (e.toString().includes("cssRules")) {
67
+ warnOnce(`Can't access cssRules, likely due to CORS restrictions: ${sheet.href}`);
68
+ } else {
69
+ // eslint-disable-next-line no-console
70
+ console.error(e, sheet.href);
71
+ }
72
+ }
73
+ };
74
+ exports.rewriteStyleSheet = rewriteStyleSheet;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+
3
+ var _rewriteStyleSheet = require("./rewriteStyleSheet");
4
+ class Sheet {
5
+ constructor() {
6
+ this.__pseudoStatesRewritten = false;
7
+ for (var _len = arguments.length, rules = new Array(_len), _key = 0; _key < _len; _key++) {
8
+ rules[_key] = arguments[_key];
9
+ }
10
+ this.cssRules = rules.map(cssText => ({
11
+ cssText,
12
+ selectorText: cssText.slice(0, cssText.indexOf(" {"))
13
+ }));
14
+ }
15
+ deleteRule(index) {
16
+ this.cssRules.splice(index, 1);
17
+ }
18
+ insertRule(cssText, index) {
19
+ this.cssRules.splice(index, 0, cssText);
20
+ }
21
+ }
22
+ describe("rewriteStyleSheet", () => {
23
+ it("adds alternative selector targeting the element directly", () => {
24
+ const sheet = new Sheet("a:hover { color: red }");
25
+ (0, _rewriteStyleSheet.rewriteStyleSheet)(sheet);
26
+ expect(sheet.cssRules[0]).toContain("a.pseudo-hover");
27
+ });
28
+ it("adds alternative selector targeting an ancestor", () => {
29
+ const sheet = new Sheet("a:hover { color: red }");
30
+ (0, _rewriteStyleSheet.rewriteStyleSheet)(sheet);
31
+ expect(sheet.cssRules[0]).toContain(".pseudo-hover a");
32
+ });
33
+ it("does not add .pseudo-<class> to pseudo-class, which does not support classes", () => {
34
+ const sheet = new Sheet("::-webkit-scrollbar-thumb:hover { border-color: transparent; }");
35
+ (0, _rewriteStyleSheet.rewriteStyleSheet)(sheet);
36
+ console.log(sheet.cssRules[0]);
37
+ expect(sheet.cssRules[0]).not.toContain("::-webkit-scrollbar-thumb.pseudo-hover");
38
+ });
39
+ it("adds alternative selector for each pseudo selector", () => {
40
+ const sheet = new Sheet("a:hover, a:focus { color: red }");
41
+ (0, _rewriteStyleSheet.rewriteStyleSheet)(sheet);
42
+ expect(sheet.cssRules[0]).toContain("a.pseudo-hover");
43
+ expect(sheet.cssRules[0]).toContain("a.pseudo-focus");
44
+ expect(sheet.cssRules[0]).toContain(".pseudo-hover a");
45
+ expect(sheet.cssRules[0]).toContain(".pseudo-focus a");
46
+ });
47
+ it("keeps non-pseudo selectors as-is", () => {
48
+ const sheet = new Sheet("a.class, a:hover, a:focus, a#id { color: red }");
49
+ (0, _rewriteStyleSheet.rewriteStyleSheet)(sheet);
50
+ expect(sheet.cssRules[0]).toContain("a.class");
51
+ expect(sheet.cssRules[0]).toContain("a#id");
52
+ });
53
+ it("supports combined pseudo selectors", () => {
54
+ const sheet = new Sheet("a:hover:focus { color: red }");
55
+ (0, _rewriteStyleSheet.rewriteStyleSheet)(sheet);
56
+ expect(sheet.cssRules[0]).toContain("a.pseudo-hover.pseudo-focus");
57
+ expect(sheet.cssRules[0]).toContain(".pseudo-hover.pseudo-focus a");
58
+ });
59
+ it('supports ":host"', () => {
60
+ const sheet = new Sheet(":host(:hover) { color: red }");
61
+ (0, _rewriteStyleSheet.rewriteStyleSheet)(sheet);
62
+ expect(sheet.cssRules[0]).toEqual(":host(:hover), :host(.pseudo-hover) { color: red }");
63
+ });
64
+ it('supports ":not"', () => {
65
+ const sheet = new Sheet(":not(:hover) { color: red }");
66
+ (0, _rewriteStyleSheet.rewriteStyleSheet)(sheet);
67
+ expect(sheet.cssRules[0]).toEqual(":not(:hover), :not(.pseudo-hover) { color: red }");
68
+ });
69
+ });
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.splitSelectors = void 0;
7
+ const isAtRule = selector => selector.indexOf("@") === 0;
8
+ const splitSelectors = selectors => {
9
+ if (isAtRule(selectors)) return [selectors];
10
+ let result = [];
11
+ let parentheses = 0;
12
+ let brackets = 0;
13
+ let selector = "";
14
+ for (let i = 0, len = selectors.length; i < len; i++) {
15
+ const char = selectors[i];
16
+ if (char === "(") {
17
+ parentheses += 1;
18
+ } else if (char === ")") {
19
+ parentheses -= 1;
20
+ } else if (char === "[") {
21
+ brackets += 1;
22
+ } else if (char === "]") {
23
+ brackets -= 1;
24
+ } else if (char === ",") {
25
+ if (!parentheses && !brackets) {
26
+ result.push(selector.trim());
27
+ selector = "";
28
+ continue;
29
+ }
30
+ }
31
+ selector += char;
32
+ }
33
+ result.push(selector.trim());
34
+ return result;
35
+ };
36
+ exports.splitSelectors = splitSelectors;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+
3
+ var _splitSelectors = require("./splitSelectors");
4
+ describe("splitSelectors", () => {
5
+ test("handles basic selectors", () => {
6
+ expect((0, _splitSelectors.splitSelectors)(".a")).toEqual([".a"]);
7
+ expect((0, _splitSelectors.splitSelectors)(".a, .b")).toEqual([".a", ".b"]);
8
+ });
9
+ test("supports ::slotted and :is", () => {
10
+ expect((0, _splitSelectors.splitSelectors)("::slotted(:is(button, a):active)")).toEqual(["::slotted(:is(button, a):active)"]);
11
+ expect((0, _splitSelectors.splitSelectors)("::slotted(:is(button, a):active), ::slotted(:is(button, a):hover)")).toEqual(["::slotted(:is(button, a):active)", "::slotted(:is(button, a):hover)"]);
12
+ });
13
+ test("supports :host", () => {
14
+ expect((0, _splitSelectors.splitSelectors)(":host([type='secondary']) ::slotted(:is(button, a)), :host([type='primary']) ::slotted(:is(button, a):active)")).toEqual([":host([type='secondary']) ::slotted(:is(button, a))", ":host([type='primary']) ::slotted(:is(button, a):active)"]);
15
+ expect((0, _splitSelectors.splitSelectors)(":host([outline]) ::slotted(:is(button, a):focus-within:focus-visible:not(:active))")).toEqual([":host([outline]) ::slotted(:is(button, a):focus-within:focus-visible:not(:active))"]);
16
+ });
17
+ });
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.withPseudoState = void 0;
7
+ var _addons = require("@storybook/addons");
8
+ var _coreEvents = require("@storybook/core-events");
9
+ var _constants = require("./constants");
10
+ var _rewriteStyleSheet = require("./rewriteStyleSheet");
11
+ /* eslint-env browser */
12
+
13
+ const channel = _addons.addons.getChannel();
14
+ const shadowHosts = new Set();
15
+
16
+ // Drops any existing pseudo state classnames that carried over from a previously viewed story
17
+ // before adding the new classnames. We do this the old-fashioned way, for IE compatibility.
18
+ const applyClasses = (element, classnames) => {
19
+ element.className = element.className.split(" ").filter(classname => classname && classname.indexOf("pseudo-") !== 0).concat(...classnames).join(" ");
20
+ };
21
+ const applyParameter = (rootElement, parameter) => {
22
+ const map = new Map([[rootElement, new Set()]]);
23
+ const add = (target, state) => map.set(target, new Set([...(map.get(target) || []), state]));
24
+ Object.entries(parameter || {}).forEach(_ref => {
25
+ let [state, value] = _ref;
26
+ if (typeof value === "boolean") {
27
+ // default API - applying pseudo class to root element.
28
+ add(rootElement, value && state);
29
+ } else if (typeof value === "string") {
30
+ // explicit selectors API - applying pseudo class to a specific element
31
+ rootElement.querySelectorAll(value).forEach(el => add(el, state));
32
+ } else if (Array.isArray(value)) {
33
+ // explicit selectors API - we have an array (of strings) recursively handle each one
34
+ value.forEach(sel => rootElement.querySelectorAll(sel).forEach(el => add(el, state)));
35
+ }
36
+ });
37
+ map.forEach((states, target) => {
38
+ const classnames = [];
39
+ states.forEach(key => _constants.PSEUDO_STATES[key] && classnames.push(`pseudo-${_constants.PSEUDO_STATES[key]}`));
40
+ applyClasses(target, classnames);
41
+ });
42
+ };
43
+
44
+ // Traverses ancestry to collect relevant pseudo classnames, and applies them to the shadow host.
45
+ // Shadow DOM can only access classes on its host. Traversing is needed to mimic the CSS cascade.
46
+ const updateShadowHost = shadowHost => {
47
+ const classnames = new Set();
48
+ for (let element = shadowHost.parentElement; element; element = element.parentElement) {
49
+ if (!element.className) continue;
50
+ element.className.split(" ").filter(classname => classname.indexOf("pseudo-") === 0).forEach(classname => classnames.add(classname));
51
+ }
52
+ applyClasses(shadowHost, classnames);
53
+ };
54
+
55
+ // Global decorator that rewrites stylesheets and applies classnames to render pseudo styles
56
+ const withPseudoState = (StoryFn, _ref2) => {
57
+ let {
58
+ viewMode,
59
+ parameters,
60
+ id,
61
+ globals: globalsArgs
62
+ } = _ref2;
63
+ const {
64
+ pseudo: parameter
65
+ } = parameters;
66
+ const {
67
+ pseudo: globals
68
+ } = globalsArgs;
69
+
70
+ // Sync parameter to globals, used by the toolbar (only in canvas as this
71
+ // doesn't make sense for docs because many stories are displayed at once)
72
+ (0, _addons.useEffect)(() => {
73
+ if (parameter !== globals && viewMode === "story") {
74
+ channel.emit(_coreEvents.UPDATE_GLOBALS, {
75
+ globals: {
76
+ pseudo: parameter
77
+ }
78
+ });
79
+ }
80
+ }, [parameter, viewMode]);
81
+
82
+ // Convert selected states to classnames and apply them to the story root element.
83
+ // Then update each shadow host to redetermine its own pseudo classnames.
84
+ (0, _addons.useEffect)(() => {
85
+ const timeout = setTimeout(() => {
86
+ const element = document.getElementById(viewMode === "docs" ? `story--${id}` : `root`);
87
+ applyParameter(element, globals || parameter);
88
+ shadowHosts.forEach(updateShadowHost);
89
+ }, 0);
90
+ return () => clearTimeout(timeout);
91
+ }, [globals, parameter, viewMode]);
92
+ return StoryFn();
93
+ };
94
+
95
+ // Rewrite CSS rules for pseudo-states on all stylesheets to add an alternative selector
96
+ exports.withPseudoState = withPseudoState;
97
+ const rewriteStyleSheets = shadowRoot => {
98
+ let styleSheets = shadowRoot ? shadowRoot.styleSheets : document.styleSheets;
99
+ if (shadowRoot && shadowRoot.adoptedStyleSheets ? shadowRoot.adoptedStyleSheets.length : undefined) styleSheets = shadowRoot.adoptedStyleSheets;
100
+ Array.from(styleSheets).forEach(sheet => (0, _rewriteStyleSheet.rewriteStyleSheet)(sheet, shadowRoot, shadowHosts));
101
+ };
102
+
103
+ // Only track shadow hosts for the current story
104
+ channel.on(_coreEvents.STORY_CHANGED, () => shadowHosts.clear());
105
+
106
+ // Reinitialize CSS enhancements every time the story changes
107
+ channel.on(_coreEvents.STORY_RENDERED, () => rewriteStyleSheets());
108
+
109
+ // Reinitialize CSS enhancements every time a docs page is rendered
110
+ channel.on(_coreEvents.DOCS_RENDERED, () => rewriteStyleSheets());
111
+
112
+ // IE doesn't support shadow DOM
113
+ if (Element.prototype.attachShadow) {
114
+ // Monkeypatch the attachShadow method so we can handle pseudo styles inside shadow DOM
115
+ Element.prototype._attachShadow = Element.prototype.attachShadow;
116
+ Element.prototype.attachShadow = function attachShadow(init) {
117
+ // Force "open" mode, so we can access the shadowRoot
118
+ const shadowRoot = this._attachShadow({
119
+ ...init,
120
+ mode: "open"
121
+ });
122
+ // Wait for it to render and apply its styles before rewriting them
123
+ requestAnimationFrame(() => {
124
+ rewriteStyleSheets(shadowRoot);
125
+ updateShadowHost(shadowRoot.host);
126
+ });
127
+ return shadowRoot;
128
+ };
129
+ }
@@ -0,0 +1,66 @@
1
+ import React, { useCallback, useMemo } from "react";
2
+ import { useGlobals } from "@storybook/api";
3
+ import { Icons, IconButton, WithTooltip, TooltipLinkList } from "@storybook/components";
4
+ import { styled, color } from "@storybook/theming";
5
+ import { PSEUDO_STATES } from "./constants";
6
+ const LinkTitle = styled.span(_ref => {
7
+ let {
8
+ active
9
+ } = _ref;
10
+ return {
11
+ color: active ? color.secondary : "inherit"
12
+ };
13
+ });
14
+ const LinkIcon = styled(Icons)(_ref2 => {
15
+ let {
16
+ active
17
+ } = _ref2;
18
+ return {
19
+ opacity: active ? 1 : 0,
20
+ path: {
21
+ fill: active ? color.secondary : "inherit"
22
+ }
23
+ };
24
+ });
25
+ const options = Object.keys(PSEUDO_STATES).sort();
26
+ export const PseudoStateTool = () => {
27
+ const [{
28
+ pseudo
29
+ }, updateGlobals] = useGlobals();
30
+ const isActive = useCallback(option => {
31
+ if (!pseudo) return false;
32
+ return pseudo[option] === true;
33
+ }, [pseudo]);
34
+ const toggleOption = useCallback(option => () => updateGlobals({
35
+ pseudo: {
36
+ ...pseudo,
37
+ [option]: !isActive(option)
38
+ }
39
+ }), [pseudo]);
40
+ return /*#__PURE__*/React.createElement(WithTooltip, {
41
+ placement: "top",
42
+ trigger: "click",
43
+ tooltip: () => /*#__PURE__*/React.createElement(TooltipLinkList, {
44
+ links: options.map(option => ({
45
+ id: option,
46
+ title: /*#__PURE__*/React.createElement(LinkTitle, {
47
+ active: isActive(option)
48
+ }, ":", PSEUDO_STATES[option]),
49
+ right: /*#__PURE__*/React.createElement(LinkIcon, {
50
+ icon: "check",
51
+ width: 12,
52
+ height: 12,
53
+ active: isActive(option)
54
+ }),
55
+ onClick: toggleOption(option),
56
+ active: isActive(option)
57
+ }))
58
+ })
59
+ }, /*#__PURE__*/React.createElement(IconButton, {
60
+ key: "pseudo-state",
61
+ title: "Select CSS pseudo states",
62
+ active: options.some(isActive)
63
+ }, /*#__PURE__*/React.createElement(Icons, {
64
+ icon: "button"
65
+ })));
66
+ };
@@ -0,0 +1,20 @@
1
+ export const ADDON_ID = "storybook/pseudo-states";
2
+ export const TOOL_ID = `${ADDON_ID}/tool`;
3
+
4
+ // Pseudo-elements which are not allowed to have classes applied on them
5
+ // E.g. ::-webkit-scrollbar-thumb.pseudo-hover is not a valid selector
6
+ export const EXCLUDED_PSEUDO_ELEMENTS = ["::-webkit-scrollbar-thumb"];
7
+
8
+ // Dynamic pseudo-classes
9
+ // @see https://www.w3.org/TR/2018/REC-selectors-3-20181106/#dynamic-pseudos
10
+ export const PSEUDO_STATES = {
11
+ hover: "hover",
12
+ active: "active",
13
+ focusVisible: "focus-visible",
14
+ focusWithin: "focus-within",
15
+ focus: "focus",
16
+ // must come after its alternatives
17
+ visited: "visited",
18
+ link: "link",
19
+ target: "target"
20
+ };
@@ -0,0 +1,16 @@
1
+ import { addons, types } from "@storybook/addons";
2
+ import { ADDON_ID, TOOL_ID } from "../constants";
3
+ import { PseudoStateTool } from "../PseudoStateTool";
4
+ addons.register(ADDON_ID, () => {
5
+ addons.add(TOOL_ID, {
6
+ type: types.TOOL,
7
+ title: "CSS pseudo states",
8
+ match: _ref => {
9
+ let {
10
+ viewMode
11
+ } = _ref;
12
+ return viewMode === "story";
13
+ },
14
+ render: PseudoStateTool
15
+ });
16
+ });
@@ -0,0 +1,2 @@
1
+ import { withPseudoState } from "../withPseudoState";
2
+ export const decorators = [withPseudoState];
@@ -0,0 +1,67 @@
1
+ import { PSEUDO_STATES, EXCLUDED_PSEUDO_ELEMENTS } from "./constants";
2
+ import { splitSelectors } from "./splitSelectors";
3
+ const pseudoStates = Object.values(PSEUDO_STATES);
4
+ const matchOne = new RegExp(`:(${pseudoStates.join("|")})`);
5
+ const matchAll = new RegExp(`:(${pseudoStates.join("|")})`, "g");
6
+ const warnings = new Set();
7
+ const warnOnce = message => {
8
+ if (warnings.has(message)) return;
9
+ // eslint-disable-next-line no-console
10
+ console.warn(message);
11
+ warnings.add(message);
12
+ };
13
+ const isExcludedPseudoElement = (selector, pseudoState) => EXCLUDED_PSEUDO_ELEMENTS.some(element => selector.endsWith(`${element}:${pseudoState}`));
14
+ const rewriteRule = (cssText, selectorText, shadowRoot) => {
15
+ return cssText.replace(selectorText, splitSelectors(selectorText).flatMap(selector => {
16
+ if (selector.includes(".pseudo-")) {
17
+ return [];
18
+ }
19
+ if (!matchOne.test(selector)) {
20
+ return [selector];
21
+ }
22
+ const states = [];
23
+ const plainSelector = selector.replace(matchAll, (_, state) => {
24
+ states.push(state);
25
+ return "";
26
+ });
27
+ const classSelector = states.reduce((acc, state) => !isExcludedPseudoElement(selector, state) && acc.replace(new RegExp(`(?<!Y):${state}`, "g"), `.pseudo-${state}`), selector);
28
+ if (selector.startsWith(":host(") || selector.startsWith("::slotted(")) {
29
+ return [selector, classSelector].filter(Boolean);
30
+ }
31
+ const ancestorSelector = shadowRoot ? `:host(${states.map(s => `.pseudo-${s}`).join("")}) ${plainSelector}` : `${states.map(s => `.pseudo-${s}`).join("")} ${plainSelector}`;
32
+ return [selector, classSelector, ancestorSelector].filter(selector => selector && !selector.includes(":not()"));
33
+ }).join(", "));
34
+ };
35
+
36
+ // Rewrites the style sheet to add alternative selectors for any rule that targets a pseudo state.
37
+ // A sheet can only be rewritten once, and may carry over between stories.
38
+ export const rewriteStyleSheet = (sheet, shadowRoot, shadowHosts) => {
39
+ if (sheet.__pseudoStatesRewritten) return;
40
+ sheet.__pseudoStatesRewritten = true;
41
+ try {
42
+ let index = 0;
43
+ for (const {
44
+ cssText,
45
+ selectorText
46
+ } of sheet.cssRules) {
47
+ if (matchOne.test(selectorText)) {
48
+ const newRule = rewriteRule(cssText, selectorText, shadowRoot);
49
+ sheet.deleteRule(index);
50
+ sheet.insertRule(newRule, index);
51
+ if (shadowRoot) shadowHosts.add(shadowRoot.host);
52
+ }
53
+ index++;
54
+ if (index > 1000) {
55
+ warnOnce("Reached maximum of 1000 pseudo selectors per sheet, skipping the rest.");
56
+ break;
57
+ }
58
+ }
59
+ } catch (e) {
60
+ if (e.toString().includes("cssRules")) {
61
+ warnOnce(`Can't access cssRules, likely due to CORS restrictions: ${sheet.href}`);
62
+ } else {
63
+ // eslint-disable-next-line no-console
64
+ console.error(e, sheet.href);
65
+ }
66
+ }
67
+ };