Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Addon-A11y: Fix manual run & timeline #12003

Merged
merged 2 commits into from
Aug 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions addons/a11y/src/a11yRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,66 @@ import { document, window } from 'global';
import axe from 'axe-core';
import addons from '@storybook/addons';
import { EVENTS } from './constants';
import { Setup } from './params';
import { A11yParameters } from './params';

if (module && module.hot && module.hot.decline) {
module.hot.decline();
}

const channel = addons.getChannel();
// Holds axe core running state
let active = false;
// Holds latest story we requested a run
let activeStoryId: string | undefined;

const getElement = () => {
const storyRoot = document.getElementById('story-root');
return storyRoot ? storyRoot.children : document.getElementById('root');
};

/**
* Handle A11yContext events.
* Because the event are sent without manual check, we split calls
*/
const handleRequest = (storyId: string) => {
const { manual } = getParams(storyId);
if (!manual) {
run(storyId);
}
};

const run = async (storyId: string) => {
activeStoryId = storyId;
try {
const input = getParams(storyId);

if (!active) {
active = true;
channel.emit(EVENTS.RUNNING);

const { element = getElement(), config, options } = input;
const {
element = getElement(),
config,
options = {
restoreScroll: true,
},
} = input;
axe.reset();
if (config) {
axe.configure(config);
}

const result = await axe.run(element, options);
channel.emit(EVENTS.RESULT, result);
// It's possible that we requested a new run on a different story.
// Unfortunately, axe doesn't support a cancel method to abort current run.
// We check if the story we run against is still the current one,
// if not, trigger a new run using the current story
if (activeStoryId === storyId) {
channel.emit(EVENTS.RESULT, result);
} else {
active = false;
run(activeStoryId);
}
}
} catch (error) {
channel.emit(EVENTS.ERROR, error);
Expand All @@ -41,7 +71,7 @@ const run = async (storyId: string) => {
};

/** Returns story parameters or default ones. */
const getParams = (storyId: string): Setup => {
const getParams = (storyId: string): A11yParameters => {
const { parameters } = window.__STORYBOOK_STORY_STORE__.fromId(storyId) || {};
return (
parameters.a11y || {
Expand All @@ -53,5 +83,5 @@ const getParams = (storyId: string): Setup => {
);
};

channel.on(EVENTS.REQUEST, run);
channel.on(EVENTS.REQUEST, handleRequest);
channel.on(EVENTS.MANUAL, run);
14 changes: 7 additions & 7 deletions addons/a11y/src/components/A11YPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useState } from 'react';

import { styled } from '@storybook/theming';

import { ActionBar, Icons, ScrollArea } from '@storybook/components';

import { AxeResults } from 'axe-core';
import { useChannel, useParameter, useStorybookState, useAddonState } from '@storybook/api';
import { useChannel, useParameter, useStorybookState } from '@storybook/api';
import { Report } from './Report';
import { Tabs } from './Tabs';

import { useA11yContext } from './A11yContext';
import { EVENTS, ADDON_ID } from '../constants';
import { EVENTS } from '../constants';
import { A11yParameters } from '../params';

export enum RuleType {
Expand Down Expand Up @@ -51,13 +51,13 @@ const Centered = styled.span<{}>({
type Status = 'initial' | 'manual' | 'running' | 'error' | 'ran' | 'ready';

export const A11YPanel: React.FC = () => {
const [status, setStatus] = useAddonState<Status>(ADDON_ID, 'initial');
const [error, setError] = React.useState<unknown>(undefined);
const { setResults, results } = useA11yContext();
const { storyId } = useStorybookState();
const { manual } = useParameter<Pick<A11yParameters, 'manual'>>('a11y', {
manual: false,
});
const [status, setStatus] = useState<Status>(manual ? 'manual' : 'initial');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using useAddonState on this didn't worked

  • useEffect doesn't update 'right' addon state
  • adding it to dependencies of useEffect overflows

const [error, setError] = React.useState<unknown>(undefined);
const { setResults, results } = useA11yContext();
const { storyId } = useStorybookState();

React.useEffect(() => {
setStatus(manual ? 'manual' : 'initial');
Expand Down
8 changes: 4 additions & 4 deletions addons/a11y/src/components/A11yContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ export const A11yContextProvider: React.FC<A11yContextProviderProps> = ({ active
: prevHighlighted.filter((t) => !target.includes(t))
);
}, []);
const handleRun = React.useCallback(() => {
emit(EVENTS.REQUEST, storyId);
}, [storyId]);
const handleRun = (renderedStoryId: string) => {
emit(EVENTS.REQUEST, renderedStoryId);
};
const handleClearHighlights = React.useCallback(() => setHighlighted([]), []);
const handleSetTab = React.useCallback((index: number) => {
handleClearHighlights();
Expand All @@ -90,7 +90,7 @@ export const A11yContextProvider: React.FC<A11yContextProviderProps> = ({ active

React.useEffect(() => {
if (active) {
handleRun();
handleRun(storyId);
} else {
handleClearHighlights();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,15 @@ EmptyLink.storyName = 'Empty Link';
export const LinkWithoutHref = () => <a>{`${text}...`}</a>;

LinkWithoutHref.storyName = 'Link without href';

export const Manual = () => <p>I'm a manual run</p>;
Manual.parameters = {
a11y: {
manual: true,
config: {
disableOtherRules: true,
rules: [],
},
options: {},
},
};