@fohte/storybook-addon 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -35,6 +35,16 @@ export default config
35
35
 
36
36
  This wires up `overflow-check` and `external-resource-check` — no further setup needed.
37
37
 
38
+ ### overflow-check
39
+
40
+ To exempt a selector across every story (e.g. a component whose oversized hit target never visibly clips anything), call `configureOverflowCheck()` once in `.storybook/preview.ts`. It's additive with a story's own `parameters.overflowCheck.ignoreSelectors` — both apply, since Storybook merges story-level parameters by replacing arrays rather than deep-merging them, and a story-level `ignoreSelectors` would otherwise silently drop the global list:
41
+
42
+ ```ts
43
+ import { configureOverflowCheck } from '@fohte/storybook-addon/preview'
44
+
45
+ configureOverflowCheck({ ignoreSelectors: ['[data-slot="checkbox"]'] })
46
+ ```
47
+
38
48
  ### unhandled-api-request-check
39
49
 
40
50
  This check needs to know which paths count as "API requests" (that's app-specific), and needs to be fed unhandled requests from your MSW setup. Configure it once and call `reportUnhandledApiRequest()` from `onUnhandledRequest` in `.storybook/preview.ts`:
@@ -1,6 +1,6 @@
1
1
  export type StorybookCheck = {
2
2
  reset: () => void;
3
- assert: (storyParameters?: unknown) => void;
3
+ assert: (storyParameters?: unknown, canvasElement?: Element) => void;
4
4
  };
5
5
  export declare function throwIfNotEmpty(urls: string[], message: string): void;
6
6
  //# sourceMappingURL=check.d.ts.map
@@ -1,3 +1,6 @@
1
1
  import type { StorybookCheck } from '#checks/check.js';
2
+ export declare function configureOverflowCheck(options: {
3
+ ignoreSelectors: string[];
4
+ }): void;
2
5
  export declare const overflowCheck: StorybookCheck;
3
6
  //# sourceMappingURL=overflow-check.d.ts.map
@@ -1,32 +1,4 @@
1
1
  import { throwIfNotEmpty } from '#checks/check.js';
2
- // The consuming app's preview.ts calls reset()/assert() around each story
3
- // render (see preview.ts's beforeEach/afterEach). The story's container is a
4
- // bare <div> appended to document.body before anything is mounted into it —
5
- // reset() never gets `context.canvasElement` itself, so this is the only way
6
- // to reach the same element. Portal-based components (dialogs, popovers,
7
- // selects, etc.) append their own <div>s directly to document.body too, but
8
- // only *after* the story has mounted — so the container is reliably the
9
- // *first* element appended to body once a story starts, not the last. A
10
- // MutationObserver set up in reset() (which runs before the story mounts)
11
- // captures it.
12
- let storyRoot = null;
13
- let bodyObserver = null;
14
- function watchStoryRoot() {
15
- storyRoot = null;
16
- bodyObserver?.disconnect();
17
- bodyObserver = new MutationObserver((mutations) => {
18
- for (const mutation of mutations) {
19
- for (const node of mutation.addedNodes) {
20
- if (node instanceof Element) {
21
- storyRoot = node;
22
- bodyObserver?.disconnect();
23
- return;
24
- }
25
- }
26
- }
27
- });
28
- bodyObserver.observe(document.body, { childList: true });
29
- }
30
2
  function describeElement(el) {
31
3
  const id = el.id ? `#${el.id}` : '';
32
4
  const classAttr = el.getAttribute('class');
@@ -111,6 +83,16 @@ function findOverflows(root, ignoreSelectors) {
111
83
  }
112
84
  return groupByAncestor(entries);
113
85
  }
86
+ // Exempted everywhere, not per-story: configured once by the consuming app
87
+ // for markup that recurs across many stories (e.g. a component whose
88
+ // oversized hit target never visibly clips anything). Kept separate from
89
+ // parameters.overflowCheck.ignoreSelectors — which storybook's
90
+ // combineParameters replaces wholesale rather than merges — so a story
91
+ // setting its own ignoreSelectors can't silently drop this list.
92
+ let globalIgnoreSelectors = [];
93
+ export function configureOverflowCheck(options) {
94
+ globalIgnoreSelectors = options.ignoreSelectors;
95
+ }
114
96
  function overflowCheckParameters(storyParameters) {
115
97
  if (typeof storyParameters !== 'object' || storyParameters === null) {
116
98
  return undefined;
@@ -144,19 +126,23 @@ function ignoreSelectorsOf(overflowCheck) {
144
126
  return ignoreSelectors.filter((s) => typeof s === 'string');
145
127
  }
146
128
  export const overflowCheck = {
147
- reset: watchStoryRoot,
148
- assert: (storyParameters) => {
129
+ reset: () => { },
130
+ assert: (storyParameters, canvasElement) => {
149
131
  const params = overflowCheckParameters(storyParameters);
150
132
  if (isDisabled(params))
151
133
  return;
152
- if (storyRoot == null) {
153
- // The MutationObserver in watchStoryRoot() hasn't seen an element
154
- // appended to body yet (e.g. the story renders nothing). Skip rather
155
- // than fail, since there is no root to scan.
156
- console.warn('[overflow-check] story root not detected; skipping overflow scan');
134
+ if (canvasElement == null) {
135
+ // Real Storybook runs always pass canvasElement (it's the story's
136
+ // mount container); this only triggers a caller invoking assert()
137
+ // directly without one. Skip rather than fail, since there is no root
138
+ // to scan.
139
+ console.warn('[overflow-check] no canvasElement in story context; skipping overflow scan');
157
140
  return;
158
141
  }
159
- throwIfNotEmpty(findOverflows(storyRoot, ignoreSelectorsOf(params)), 'Story has element(s) overflowing their container (clipped and invisible)');
142
+ throwIfNotEmpty(findOverflows(canvasElement, [
143
+ ...globalIgnoreSelectors,
144
+ ...ignoreSelectorsOf(params),
145
+ ]), 'Story has element(s) overflowing their container (clipped and invisible)');
160
146
  },
161
147
  };
162
148
  //# sourceMappingURL=overflow-check.js.map
package/lib/preview.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { AfterEach, BeforeEach, Parameters, WebRenderer } from 'storybook/internal/types';
2
+ export { configureOverflowCheck } from '#checks/overflow-check.js';
2
3
  export { configureUnhandledApiRequestCheck, reportUnhandledApiRequest, } from '#checks/unhandled-api-request-check.js';
3
4
  export declare const parameters: Parameters;
4
5
  export declare const beforeEach: BeforeEach<WebRenderer>;
package/lib/preview.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { externalResourceCheck } from '#checks/external-resource-check.js';
2
2
  import { overflowCheck } from '#checks/overflow-check.js';
3
3
  import { unhandledApiRequestCheck } from '#checks/unhandled-api-request-check.js';
4
+ export { configureOverflowCheck } from '#checks/overflow-check.js';
4
5
  export { configureUnhandledApiRequestCheck, reportUnhandledApiRequest, } from '#checks/unhandled-api-request-check.js';
5
6
  function injectStyle(css) {
6
7
  const style = document.createElement('style');
@@ -37,7 +38,8 @@ export const beforeEach = () => {
37
38
  check.reset();
38
39
  };
39
40
  export const afterEach = (context) => {
40
- for (const check of checks)
41
- check.assert(context.parameters);
41
+ for (const check of checks) {
42
+ check.assert(context.parameters, context.canvasElement);
43
+ }
42
44
  };
43
45
  //# sourceMappingURL=preview.js.map
package/package.json CHANGED
@@ -51,7 +51,7 @@
51
51
  "typescript": "6.0.3",
52
52
  "vitest": "4.1.10"
53
53
  },
54
- "version": "0.1.0",
54
+ "version": "0.1.1",
55
55
  "scripts": {
56
56
  "clean": "rimraf lib tsconfig.tsbuildinfo",
57
57
  "prebuild": "pnpm run clean",