mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-08 05:51:48 +08:00
Refactor a11y addon panel, update its layout and implement deeplinking
This commit is contained in:
parent
6c51cada9a
commit
3e8abc7798
@ -2,11 +2,11 @@ import React from 'react';
|
|||||||
|
|
||||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
|
||||||
import type axe from 'axe-core';
|
|
||||||
import { ManagerContext } from 'storybook/manager-api';
|
import { ManagerContext } from 'storybook/manager-api';
|
||||||
import { fn } from 'storybook/test';
|
import { fn } from 'storybook/test';
|
||||||
import { styled } from 'storybook/theming';
|
import { styled } from 'storybook/theming';
|
||||||
|
|
||||||
|
import { results } from '../results.mock';
|
||||||
import { A11YPanel } from './A11YPanel';
|
import { A11YPanel } from './A11YPanel';
|
||||||
import { A11yContext } from './A11yContext';
|
import { A11yContext } from './A11yContext';
|
||||||
import type { A11yContextStore } from './A11yContext';
|
import type { A11yContextStore } from './A11yContext';
|
||||||
@ -52,61 +52,6 @@ export default meta;
|
|||||||
|
|
||||||
type Story = StoryObj<typeof meta>;
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
const violations: axe.Result[] = [
|
|
||||||
{
|
|
||||||
id: 'aria-command-name',
|
|
||||||
impact: 'serious',
|
|
||||||
tags: ['cat.aria', 'wcag2a', 'wcag412', 'TTv5', 'TT6.a', 'EN-301-549', 'EN-9.4.1.2', 'ACT'],
|
|
||||||
description: 'Ensures every ARIA button, link and menuitem has an accessible name',
|
|
||||||
help: 'ARIA commands must have an accessible name',
|
|
||||||
helpUrl: 'https://dequeuniversity.com/rules/axe/4.8/aria-command-name?application=axeAPI',
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
any: [
|
|
||||||
{
|
|
||||||
id: 'has-visible-text',
|
|
||||||
data: null,
|
|
||||||
relatedNodes: [],
|
|
||||||
impact: 'serious',
|
|
||||||
message: 'Element does not have text that is visible to screen readers',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'aria-label',
|
|
||||||
data: null,
|
|
||||||
relatedNodes: [],
|
|
||||||
impact: 'serious',
|
|
||||||
message: 'aria-label attribute does not exist or is empty',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'aria-labelledby',
|
|
||||||
data: null,
|
|
||||||
relatedNodes: [],
|
|
||||||
impact: 'serious',
|
|
||||||
message:
|
|
||||||
'aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'non-empty-title',
|
|
||||||
data: {
|
|
||||||
messageKey: 'noAttr',
|
|
||||||
},
|
|
||||||
relatedNodes: [],
|
|
||||||
impact: 'serious',
|
|
||||||
message: 'Element has no title attribute',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
all: [],
|
|
||||||
none: [],
|
|
||||||
impact: 'serious',
|
|
||||||
html: '<div role="button" class="css-12jpz5t">',
|
|
||||||
target: ['.css-12jpz5t'],
|
|
||||||
failureSummary:
|
|
||||||
'Fix any of the following:\n Element does not have text that is visible to screen readers\n aria-label attribute does not exist or is empty\n aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n Element has no title attribute',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const Template = (args: Pick<A11yContextStore, 'results' | 'error' | 'status' | 'discrepancy'>) => (
|
const Template = (args: Pick<A11yContextStore, 'results' | 'error' | 'status' | 'discrepancy'>) => (
|
||||||
<A11yContext.Provider
|
<A11yContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@ -116,6 +61,8 @@ const Template = (args: Pick<A11yContextStore, 'results' | 'error' | 'status' |
|
|||||||
tab: 0,
|
tab: 0,
|
||||||
setTab: fn(),
|
setTab: fn(),
|
||||||
setStatus: fn(),
|
setStatus: fn(),
|
||||||
|
selection: null,
|
||||||
|
handleCopyLink: fn().mockName('handleCopyLink'),
|
||||||
...args,
|
...args,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -177,18 +124,7 @@ export const Running: Story = {
|
|||||||
|
|
||||||
export const ReadyWithResults: Story = {
|
export const ReadyWithResults: Story = {
|
||||||
render: () => {
|
render: () => {
|
||||||
return (
|
return <Template results={results} status="ready" error={null} discrepancy={null} />;
|
||||||
<Template
|
|
||||||
results={{
|
|
||||||
passes: [],
|
|
||||||
incomplete: [],
|
|
||||||
violations,
|
|
||||||
}}
|
|
||||||
status="ready"
|
|
||||||
error={null}
|
|
||||||
discrepancy={null}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -196,11 +132,7 @@ export const ReadyWithResultsDiscrepancyCLIPassedBrowserFailed: Story = {
|
|||||||
render: () => {
|
render: () => {
|
||||||
return (
|
return (
|
||||||
<Template
|
<Template
|
||||||
results={{
|
results={results}
|
||||||
passes: [],
|
|
||||||
incomplete: [],
|
|
||||||
violations,
|
|
||||||
}}
|
|
||||||
status="ready"
|
status="ready"
|
||||||
error={null}
|
error={null}
|
||||||
discrepancy={'cliPassedBrowserFailed'}
|
discrepancy={'cliPassedBrowserFailed'}
|
||||||
|
@ -1,20 +1,22 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { ActionBar, Badge, ScrollArea } from 'storybook/internal/components';
|
import { ActionBar, Badge, ScrollArea } from 'storybook/internal/components';
|
||||||
|
import { useStorybookApi } from 'storybook/internal/manager-api';
|
||||||
|
|
||||||
import { CheckIcon, SyncIcon } from '@storybook/icons';
|
import { CheckIcon, SyncIcon } from '@storybook/icons';
|
||||||
|
|
||||||
|
import type { Result } from 'axe-core';
|
||||||
import { styled } from 'storybook/theming';
|
import { styled } from 'storybook/theming';
|
||||||
|
|
||||||
import { useA11yContext } from './A11yContext';
|
import { useA11yContext } from './A11yContext';
|
||||||
import { Report } from './Report';
|
import { Report } from './Report/Report';
|
||||||
import { Tabs } from './Tabs';
|
import { Tabs } from './Tabs';
|
||||||
import { TestDiscrepancyMessage } from './TestDiscrepancyMessage';
|
import { TestDiscrepancyMessage } from './TestDiscrepancyMessage';
|
||||||
|
|
||||||
export enum RuleType {
|
export enum RuleType {
|
||||||
VIOLATION,
|
VIOLATION = 'violations',
|
||||||
PASS,
|
PASS = 'passes',
|
||||||
INCOMPLETION,
|
INCOMPLETION = 'incomplete',
|
||||||
}
|
}
|
||||||
|
|
||||||
const Icon = styled(SyncIcon)({
|
const Icon = styled(SyncIcon)({
|
||||||
@ -44,8 +46,34 @@ const Count = styled(Badge)({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const A11YPanel: React.FC = () => {
|
export const A11YPanel: React.FC = () => {
|
||||||
|
const api = useStorybookApi();
|
||||||
const { results, status, handleManual, error, discrepancy } = useA11yContext();
|
const { results, status, handleManual, error, discrepancy } = useA11yContext();
|
||||||
|
|
||||||
|
const [selectedItems, setSelectedItems] = useState<Map<string, string>>(() => {
|
||||||
|
const initialValue = new Map();
|
||||||
|
const a11ySelection = api.getQueryParam('a11ySelection');
|
||||||
|
if (a11ySelection && /^[a-z]+.[a-z-]+.[0-9]+$/.test(a11ySelection)) {
|
||||||
|
const [type, id] = a11ySelection.split('.');
|
||||||
|
initialValue.set(`${type}.${id}`, a11ySelection);
|
||||||
|
}
|
||||||
|
return initialValue;
|
||||||
|
});
|
||||||
|
console.log('selectedItems', selectedItems);
|
||||||
|
|
||||||
|
const toggleOpen = useCallback(
|
||||||
|
(event: React.SyntheticEvent<Element>, type: RuleType, item: Result) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
const key = `${type}.${item.id}`;
|
||||||
|
setSelectedItems((prev) => new Map(prev.delete(key) ? prev : prev.set(key, `${key}.1`)));
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectionChange = useCallback((key: string) => {
|
||||||
|
const [type, id] = key.split('.');
|
||||||
|
setSelectedItems((prev) => new Map(prev.set(`${type}.${id}`, key)));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const manualActionItems = useMemo(
|
const manualActionItems = useMemo(
|
||||||
() => [{ title: 'Run test', onClick: handleManual }],
|
() => [{ title: 'Run test', onClick: handleManual }],
|
||||||
[handleManual]
|
[handleManual]
|
||||||
@ -82,6 +110,9 @@ export const A11YPanel: React.FC = () => {
|
|||||||
items={violations}
|
items={violations}
|
||||||
type={RuleType.VIOLATION}
|
type={RuleType.VIOLATION}
|
||||||
empty="No accessibility violations found."
|
empty="No accessibility violations found."
|
||||||
|
onSelectionChange={onSelectionChange}
|
||||||
|
selectedItems={selectedItems}
|
||||||
|
toggleOpen={toggleOpen}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
items: violations,
|
items: violations,
|
||||||
@ -95,7 +126,14 @@ export const A11YPanel: React.FC = () => {
|
|||||||
</Tab>
|
</Tab>
|
||||||
),
|
),
|
||||||
panel: (
|
panel: (
|
||||||
<Report items={passes} type={RuleType.PASS} empty="No accessibility checks passed." />
|
<Report
|
||||||
|
items={passes}
|
||||||
|
type={RuleType.PASS}
|
||||||
|
empty="No accessibility checks passed."
|
||||||
|
onSelectionChange={onSelectionChange}
|
||||||
|
selectedItems={selectedItems}
|
||||||
|
toggleOpen={toggleOpen}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
items: passes,
|
items: passes,
|
||||||
type: RuleType.PASS,
|
type: RuleType.PASS,
|
||||||
@ -112,13 +150,16 @@ export const A11YPanel: React.FC = () => {
|
|||||||
items={incomplete}
|
items={incomplete}
|
||||||
type={RuleType.INCOMPLETION}
|
type={RuleType.INCOMPLETION}
|
||||||
empty="No accessibility checks incomplete."
|
empty="No accessibility checks incomplete."
|
||||||
|
onSelectionChange={onSelectionChange}
|
||||||
|
selectedItems={selectedItems}
|
||||||
|
toggleOpen={toggleOpen}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
items: incomplete,
|
items: incomplete,
|
||||||
type: RuleType.INCOMPLETION,
|
type: RuleType.INCOMPLETION,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [results]);
|
}, [results, onSelectionChange, selectedItems, toggleOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -21,9 +21,10 @@ import {
|
|||||||
import type { Report } from 'storybook/preview-api';
|
import type { Report } from 'storybook/preview-api';
|
||||||
import { convert, themes } from 'storybook/theming';
|
import { convert, themes } from 'storybook/theming';
|
||||||
|
|
||||||
import { ADDON_ID, EVENTS, TEST_PROVIDER_ID } from '../constants';
|
import { ADDON_ID, EVENTS, PANEL_ID, TEST_PROVIDER_ID } from '../constants';
|
||||||
import type { A11yParameters } from '../params';
|
import type { A11yParameters } from '../params';
|
||||||
import type { A11YReport } from '../types';
|
import type { A11YReport } from '../types';
|
||||||
|
import { RuleType } from './A11YPanel';
|
||||||
import type { TestDiscrepancy } from './TestDiscrepancyMessage';
|
import type { TestDiscrepancy } from './TestDiscrepancyMessage';
|
||||||
|
|
||||||
export interface Results {
|
export interface Results {
|
||||||
@ -36,8 +37,9 @@ export interface A11yContextStore {
|
|||||||
results: Results;
|
results: Results;
|
||||||
highlighted: boolean;
|
highlighted: boolean;
|
||||||
toggleHighlight: () => void;
|
toggleHighlight: () => void;
|
||||||
tab: number;
|
tab: RuleType;
|
||||||
setTab: (index: number) => void;
|
handleCopyLink: (key: string) => void;
|
||||||
|
setTab: (type: RuleType) => void;
|
||||||
status: Status;
|
status: Status;
|
||||||
setStatus: (status: Status) => void;
|
setStatus: (status: Status) => void;
|
||||||
error: unknown;
|
error: unknown;
|
||||||
@ -45,11 +47,11 @@ export interface A11yContextStore {
|
|||||||
discrepancy: TestDiscrepancy;
|
discrepancy: TestDiscrepancy;
|
||||||
}
|
}
|
||||||
|
|
||||||
const colorsByType = [
|
const colorsByType = {
|
||||||
convert(themes.light).color.negative, // VIOLATION,
|
[RuleType.VIOLATION]: convert(themes.light).color.negative,
|
||||||
convert(themes.light).color.positive, // PASS,
|
[RuleType.PASS]: convert(themes.light).color.positive,
|
||||||
convert(themes.light).color.warning, // INCOMPLETION,
|
[RuleType.INCOMPLETION]: convert(themes.light).color.warning,
|
||||||
];
|
};
|
||||||
|
|
||||||
export const A11yContext = createContext<A11yContextStore>({
|
export const A11yContext = createContext<A11yContextStore>({
|
||||||
results: {
|
results: {
|
||||||
@ -59,7 +61,8 @@ export const A11yContext = createContext<A11yContextStore>({
|
|||||||
},
|
},
|
||||||
highlighted: false,
|
highlighted: false,
|
||||||
toggleHighlight: () => {},
|
toggleHighlight: () => {},
|
||||||
tab: 0,
|
tab: RuleType.VIOLATION,
|
||||||
|
handleCopyLink: () => {},
|
||||||
setTab: () => {},
|
setTab: () => {},
|
||||||
setStatus: () => {},
|
setStatus: () => {},
|
||||||
status: 'initial',
|
status: 'initial',
|
||||||
@ -91,15 +94,37 @@ export const A11yContextProvider: FC<PropsWithChildren> = (props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const api = useStorybookApi();
|
const api = useStorybookApi();
|
||||||
|
const a11ySelection = api.getQueryParam('a11ySelection');
|
||||||
|
|
||||||
const [results, setResults] = useAddonState<Results>(ADDON_ID, defaultResult);
|
const [results, setResults] = useAddonState<Results>(ADDON_ID, defaultResult);
|
||||||
const [tab, setTab] = useState(0);
|
const [tab, setTab] = useState(() => {
|
||||||
const [error, setError] = React.useState<unknown>(undefined);
|
const [type] = a11ySelection?.split('.') ?? [];
|
||||||
|
return type && Object.values(RuleType).includes(type as RuleType)
|
||||||
|
? (type as RuleType)
|
||||||
|
: RuleType.VIOLATION;
|
||||||
|
});
|
||||||
|
const [error, setError] = useState<unknown>(undefined);
|
||||||
const [status, setStatus] = useState<Status>(getInitialStatus(manual));
|
const [status, setStatus] = useState<Status>(getInitialStatus(manual));
|
||||||
const [highlighted, setHighlighted] = useState(false);
|
const [highlighted, setHighlighted] = useState(false);
|
||||||
|
|
||||||
const { storyId } = useStorybookState();
|
const { storyId } = useStorybookState();
|
||||||
const storyStatus = api.getCurrentStoryStatus();
|
const storyStatus = api.getCurrentStoryStatus();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status !== 'ran') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a11ySelection) {
|
||||||
|
const currentUrl = api.getUrlState();
|
||||||
|
const queryParams = new URLSearchParams(
|
||||||
|
(currentUrl.queryParams as Record<string, string>) || {}
|
||||||
|
);
|
||||||
|
queryParams.delete('a11ySelection');
|
||||||
|
api.setQueryParams({ a11ySelection: undefined });
|
||||||
|
}
|
||||||
|
}, [api, results, status, a11ySelection]);
|
||||||
|
|
||||||
const handleToggleHighlight = useCallback(
|
const handleToggleHighlight = useCallback(
|
||||||
() => setHighlighted((prevHighlighted) => !prevHighlighted),
|
() => setHighlighted((prevHighlighted) => !prevHighlighted),
|
||||||
[]
|
[]
|
||||||
@ -170,6 +195,12 @@ export const A11yContextProvider: FC<PropsWithChildren> = (props) => {
|
|||||||
emit(EVENTS.MANUAL, storyId, parameters);
|
emit(EVENTS.MANUAL, storyId, parameters);
|
||||||
}, [emit, parameters, storyId]);
|
}, [emit, parameters, storyId]);
|
||||||
|
|
||||||
|
const handleCopyLink = useCallback(async (key: string) => {
|
||||||
|
const link = `${window.location.origin}${window.location.pathname}${window.location.search}&addonPanel=${PANEL_ID}&a11ySelection=${key}`;
|
||||||
|
const { createCopyToClipboardFunction } = await import('storybook/internal/components');
|
||||||
|
await createCopyToClipboardFunction()(link);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setStatus(getInitialStatus(manual));
|
setStatus(getInitialStatus(manual));
|
||||||
}, [getInitialStatus, manual]);
|
}, [getInitialStatus, manual]);
|
||||||
@ -211,6 +242,7 @@ export const A11yContextProvider: FC<PropsWithChildren> = (props) => {
|
|||||||
toggleHighlight: handleToggleHighlight,
|
toggleHighlight: handleToggleHighlight,
|
||||||
tab,
|
tab,
|
||||||
setTab,
|
setTab,
|
||||||
|
handleCopyLink,
|
||||||
status,
|
status,
|
||||||
setStatus,
|
setStatus,
|
||||||
error,
|
error,
|
||||||
|
206
code/addons/a11y/src/components/Report/Details.tsx
Normal file
206
code/addons/a11y/src/components/Report/Details.tsx
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import React, { Fragment, useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { Button, Link, SyntaxHighlighter } from 'storybook/internal/components';
|
||||||
|
|
||||||
|
import { CheckIcon, CopyIcon, LocationIcon } from '@storybook/icons';
|
||||||
|
|
||||||
|
import * as Tabs from '@radix-ui/react-tabs';
|
||||||
|
import type { NodeResult, Result } from 'axe-core';
|
||||||
|
import { styled } from 'storybook/theming';
|
||||||
|
|
||||||
|
import type { RuleType } from '../A11YPanel';
|
||||||
|
import { useA11yContext } from '../A11yContext';
|
||||||
|
|
||||||
|
const StyledSyntaxHighlighter = styled(SyntaxHighlighter)(
|
||||||
|
({ theme }) => ({
|
||||||
|
fontSize: theme.typography.size.s1,
|
||||||
|
}),
|
||||||
|
({ language }) =>
|
||||||
|
// We appended ' {}' to the selector in order to get proper syntax highlighting.
|
||||||
|
// This hides them in the displayed output so people can copy the selector without the brackets.
|
||||||
|
language === 'css' && {
|
||||||
|
'.selector ~ span:nth-last-of-type(-n+3)': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Info = styled.div({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
});
|
||||||
|
|
||||||
|
const Description = styled.p({
|
||||||
|
margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Wrapper = styled.div({
|
||||||
|
containerType: 'inline-size',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
padding: '0 15px 20px 15px',
|
||||||
|
gap: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Columns = styled.div({
|
||||||
|
gap: 15,
|
||||||
|
|
||||||
|
'@container (min-width: 800px)': {
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '50% 50%',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Content = styled.div<{ side: 'left' | 'right' }>(({ theme, side }) => ({
|
||||||
|
display: side === 'left' ? 'flex' : 'none',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 15,
|
||||||
|
margin: side === 'left' ? '15px 0 20px 0' : 0,
|
||||||
|
'&:focus-visible': {
|
||||||
|
outline: 'none',
|
||||||
|
borderRadius: 4,
|
||||||
|
boxShadow: `0 0 0 1px inset ${theme.color.secondary}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
'@container (min-width: 800px)': {
|
||||||
|
display: side === 'left' ? 'none' : 'flex',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Item = styled(Button)(({ theme }) => ({
|
||||||
|
fontFamily: theme.typography.fonts.mono,
|
||||||
|
fontWeight: theme.typography.weight.regular,
|
||||||
|
color: theme.textMutedColor,
|
||||||
|
height: 40,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
display: 'block',
|
||||||
|
width: '100%',
|
||||||
|
textAlign: 'left',
|
||||||
|
padding: '0 12px',
|
||||||
|
'&[data-state="active"]': {
|
||||||
|
color: theme.color.secondary,
|
||||||
|
backgroundColor: theme.background.hoverable,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Messages = styled.div({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Actions = styled.div({
|
||||||
|
display: 'flex',
|
||||||
|
gap: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
const CopyButton = ({ onClick }: { onClick: () => void }) => {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
onClick();
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(setCopied, 2000, false);
|
||||||
|
}, [onClick]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onClick={handleClick}>
|
||||||
|
{copied ? <CheckIcon /> : <CopyIcon />} {copied ? 'Copied' : 'Copy link'}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DetailsProps {
|
||||||
|
item: Result;
|
||||||
|
type: RuleType;
|
||||||
|
selection: string | undefined;
|
||||||
|
onSelectionChange: (key: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Details = ({ item, type, selection, onSelectionChange }: DetailsProps) => (
|
||||||
|
<Wrapper>
|
||||||
|
<Info>
|
||||||
|
<Description>
|
||||||
|
{item.description.endsWith('.') ? item.description : `${item.description}.`}
|
||||||
|
</Description>
|
||||||
|
<Link href={item.helpUrl} target="_blank" withArrow>
|
||||||
|
Learn how to resolve this violation
|
||||||
|
</Link>
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
<Tabs.Root
|
||||||
|
defaultValue="tab0"
|
||||||
|
orientation="vertical"
|
||||||
|
value={selection}
|
||||||
|
onValueChange={onSelectionChange}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Columns>
|
||||||
|
<Tabs.List aria-label={type}>
|
||||||
|
{item.nodes.map((node, index) => {
|
||||||
|
const key = `${type}.${item.id}.${index + 1}`;
|
||||||
|
return (
|
||||||
|
<Fragment key={key}>
|
||||||
|
<Tabs.Trigger value={key} asChild>
|
||||||
|
<Item variant="ghost" size="medium">
|
||||||
|
{index + 1}. {node.html}
|
||||||
|
</Item>
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Content value={key} asChild>
|
||||||
|
<Content side="left">{getContent(node, key)}</Content>
|
||||||
|
</Tabs.Content>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
|
{item.nodes.map((node, index) => {
|
||||||
|
const key = `${type}.${item.id}.${index + 1}`;
|
||||||
|
return (
|
||||||
|
<Tabs.Content key={key} value={key} asChild>
|
||||||
|
<Content side="right">{getContent(node, key)}</Content>
|
||||||
|
</Tabs.Content>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Columns>
|
||||||
|
</Tabs.Root>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
function getContent(node: NodeResult, key: string) {
|
||||||
|
const { handleCopyLink } = useA11yContext();
|
||||||
|
const { any, all, none, html, target } = node;
|
||||||
|
const rules = [...any, ...all, ...none];
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Messages>
|
||||||
|
{rules.map((rule, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
{`${rule.message}${/(\.|: [^.]+\.*)$/.test(rule.message) ? '' : '.'}`}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Messages>
|
||||||
|
|
||||||
|
<Actions>
|
||||||
|
<Button>
|
||||||
|
<LocationIcon /> Jump to element
|
||||||
|
</Button>
|
||||||
|
<CopyButton onClick={() => handleCopyLink(key)} />
|
||||||
|
</Actions>
|
||||||
|
|
||||||
|
{/* Technically this is HTML but we use JSX to avoid using an HTML comment */}
|
||||||
|
<StyledSyntaxHighlighter
|
||||||
|
language="jsx"
|
||||||
|
wrapLongLines
|
||||||
|
>{`/* element */\n${html}`}</StyledSyntaxHighlighter>
|
||||||
|
|
||||||
|
{/* See note about the appended {} in the StyledSyntaxHighlighter component */}
|
||||||
|
<StyledSyntaxHighlighter
|
||||||
|
language="css"
|
||||||
|
wrapLongLines
|
||||||
|
>{`/* selector */\n${target} {}`}</StyledSyntaxHighlighter>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,65 +0,0 @@
|
|||||||
import type { FC } from 'react';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { Button } from 'storybook/internal/components';
|
|
||||||
|
|
||||||
import * as Tabs from '@radix-ui/react-tabs';
|
|
||||||
import type { NodeResult } from 'axe-core';
|
|
||||||
import { styled } from 'storybook/theming';
|
|
||||||
|
|
||||||
import type { RuleType } from '../A11YPanel';
|
|
||||||
import { Instances } from './Instances';
|
|
||||||
|
|
||||||
const Columns = styled.div({
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: '50% 50%',
|
|
||||||
gap: 15,
|
|
||||||
});
|
|
||||||
|
|
||||||
const Item = styled(Button)(({ theme }) => ({
|
|
||||||
fontWeight: theme.typography.weight.regular,
|
|
||||||
color: theme.textMutedColor,
|
|
||||||
height: 40,
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
display: 'block',
|
|
||||||
width: '100%',
|
|
||||||
textAlign: 'left',
|
|
||||||
padding: '0 12px',
|
|
||||||
'&[data-state="active"]': {
|
|
||||||
color: theme.color.secondary,
|
|
||||||
backgroundColor: theme.background.hoverable,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
interface ElementsProps {
|
|
||||||
elements: NodeResult[];
|
|
||||||
type: RuleType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Elements: FC<ElementsProps> = ({ elements, type }) => (
|
|
||||||
<Tabs.Root defaultValue="tab0" orientation="vertical" asChild>
|
|
||||||
<Columns>
|
|
||||||
<Tabs.List aria-label="tabs example">
|
|
||||||
{elements.map((element, index) => (
|
|
||||||
<Tabs.Trigger key={`tab${index}`} value={`tab${index}`} asChild>
|
|
||||||
<Item variant="ghost" size="medium">
|
|
||||||
{index + 1}. {element.target[0]}
|
|
||||||
</Item>
|
|
||||||
</Tabs.Trigger>
|
|
||||||
))}
|
|
||||||
</Tabs.List>
|
|
||||||
|
|
||||||
{elements.map((element, index) => {
|
|
||||||
const { any, all, none } = element;
|
|
||||||
const rules = [...any, ...all, ...none];
|
|
||||||
return (
|
|
||||||
<Tabs.Content key={`tab${index}`} value={`tab${index}`} asChild>
|
|
||||||
<Instances rules={rules} />
|
|
||||||
</Tabs.Content>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Columns>
|
|
||||||
</Tabs.Root>
|
|
||||||
);
|
|
@ -1,32 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import type { NodeResult } from 'axe-core';
|
|
||||||
import { styled } from 'storybook/theming';
|
|
||||||
|
|
||||||
import { useA11yContext } from '../A11yContext';
|
|
||||||
|
|
||||||
interface ToggleProps {
|
|
||||||
elementsToHighlight: NodeResult[];
|
|
||||||
toggleId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Checkbox = styled.input<{ disabled: boolean }>(({ disabled }) => ({
|
|
||||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const HighlightToggle: React.FC<ToggleProps> = ({ toggleId, elementsToHighlight = [] }) => {
|
|
||||||
const { toggleHighlight, highlighted } = useA11yContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Checkbox
|
|
||||||
id={toggleId}
|
|
||||||
type="checkbox"
|
|
||||||
aria-label="Highlight results"
|
|
||||||
disabled={!elementsToHighlight.length}
|
|
||||||
onChange={toggleHighlight}
|
|
||||||
checked={highlighted}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HighlightToggle;
|
|
@ -1,33 +0,0 @@
|
|||||||
import type { FC } from 'react';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { Link } from 'storybook/internal/components';
|
|
||||||
|
|
||||||
import type { Result } from 'axe-core';
|
|
||||||
import { styled } from 'storybook/theming';
|
|
||||||
|
|
||||||
const Wrapper = styled.div({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
});
|
|
||||||
|
|
||||||
const Description = styled.p({
|
|
||||||
margin: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
interface InfoProps {
|
|
||||||
item: Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Info: FC<InfoProps> = ({ item }) => {
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<Description>
|
|
||||||
{item.description.endsWith('.') ? item.description : `${item.description}.`}
|
|
||||||
</Description>
|
|
||||||
<Link href={item.helpUrl} target="_blank" withArrow>
|
|
||||||
Learn how to resolve this violation
|
|
||||||
</Link>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,62 +0,0 @@
|
|||||||
import React, { type FC } from 'react';
|
|
||||||
|
|
||||||
import { Button, SyntaxHighlighter } from 'storybook/internal/components';
|
|
||||||
|
|
||||||
import { CopyIcon, LocationIcon } from '@storybook/icons';
|
|
||||||
|
|
||||||
import type { CheckResult } from 'axe-core';
|
|
||||||
import { styled } from 'storybook/theming';
|
|
||||||
|
|
||||||
const List = styled.div({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
paddingBottom: 4,
|
|
||||||
paddingRight: 4,
|
|
||||||
paddingTop: 4,
|
|
||||||
fontWeight: 400,
|
|
||||||
});
|
|
||||||
|
|
||||||
const Item = styled.div(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
gap: 15,
|
|
||||||
flexDirection: 'column',
|
|
||||||
fontSize: theme.typography.size.s2,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const Actions = styled.div({
|
|
||||||
display: 'flex',
|
|
||||||
gap: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
interface RuleProps {
|
|
||||||
rule: CheckResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Instance: FC<RuleProps> = ({ rule }) => (
|
|
||||||
<Item>
|
|
||||||
<div>{rule.message}</div>
|
|
||||||
<Actions>
|
|
||||||
<Button>
|
|
||||||
<LocationIcon /> Jump to element
|
|
||||||
</Button>
|
|
||||||
<Button>
|
|
||||||
<CopyIcon /> Copy link
|
|
||||||
</Button>
|
|
||||||
</Actions>
|
|
||||||
<SyntaxHighlighter language="html">{`// element`}</SyntaxHighlighter>
|
|
||||||
</Item>
|
|
||||||
);
|
|
||||||
|
|
||||||
interface InstancesProps {
|
|
||||||
rules: CheckResult[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Instances: FC<InstancesProps> = ({ rules }) => {
|
|
||||||
return (
|
|
||||||
<List>
|
|
||||||
{rules.map((rule, index) => (
|
|
||||||
<Instance rule={rule} key={index} />
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,76 +0,0 @@
|
|||||||
import React, { Fragment, useState } from 'react';
|
|
||||||
|
|
||||||
import { IconButton } from 'storybook/internal/components';
|
|
||||||
|
|
||||||
import { ChevronSmallDownIcon } from '@storybook/icons';
|
|
||||||
|
|
||||||
import type { Result } from 'axe-core';
|
|
||||||
import { styled } from 'storybook/theming';
|
|
||||||
|
|
||||||
import type { RuleType } from '../A11YPanel';
|
|
||||||
import { Elements } from './Elements';
|
|
||||||
import { Info } from './Info';
|
|
||||||
|
|
||||||
const Wrapper = styled.div(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
width: '100%',
|
|
||||||
borderBottom: `1px solid ${theme.appBorderColor}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const Icon = styled(ChevronSmallDownIcon)({
|
|
||||||
transition: 'transform 0.1s ease-in-out',
|
|
||||||
});
|
|
||||||
|
|
||||||
const HeaderBar = styled.div(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: 5,
|
|
||||||
paddingLeft: 15,
|
|
||||||
minHeight: 40,
|
|
||||||
background: 'none',
|
|
||||||
color: 'inherit',
|
|
||||||
textAlign: 'left',
|
|
||||||
cursor: 'pointer',
|
|
||||||
width: '100%',
|
|
||||||
'&:hover': {
|
|
||||||
color: theme.color.secondary,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const Content = styled.div({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
padding: '0 15px 20px 15px',
|
|
||||||
gap: 20,
|
|
||||||
});
|
|
||||||
|
|
||||||
interface ItemProps {
|
|
||||||
item: Result;
|
|
||||||
type: RuleType;
|
|
||||||
}
|
|
||||||
|
|
||||||
// export class Item extends Component<ItemProps, ItemState> {
|
|
||||||
export const Item = (props: ItemProps) => {
|
|
||||||
const [open, onToggle] = useState(false);
|
|
||||||
|
|
||||||
const { item, type } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<HeaderBar onClick={() => onToggle(!open)} role="button">
|
|
||||||
<strong>{item.help}</strong>
|
|
||||||
<IconButton onClick={() => onToggle(!open)}>
|
|
||||||
<Icon style={{ transform: `rotate(${open ? -180 : 0}deg)` }} />
|
|
||||||
</IconButton>
|
|
||||||
</HeaderBar>
|
|
||||||
{open ? (
|
|
||||||
<Content>
|
|
||||||
<Info item={item} key="info" />
|
|
||||||
<Elements elements={item.nodes} type={type} key="elements" />
|
|
||||||
</Content>
|
|
||||||
) : null}
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
@ -6,9 +6,9 @@ import { ManagerContext } from 'storybook/manager-api';
|
|||||||
import { fn } from 'storybook/test';
|
import { fn } from 'storybook/test';
|
||||||
import { styled } from 'storybook/theming';
|
import { styled } from 'storybook/theming';
|
||||||
|
|
||||||
import { Report } from '.';
|
import { results } from '../../results.mock';
|
||||||
import { RuleType } from '../A11YPanel';
|
import { RuleType } from '../A11YPanel';
|
||||||
import { results } from './results';
|
import { Report } from './Report';
|
||||||
|
|
||||||
const StyledWrapper = styled.div(({ theme }) => ({
|
const StyledWrapper = styled.div(({ theme }) => ({
|
||||||
backgroundColor: theme.background.content,
|
backgroundColor: theme.background.content,
|
||||||
@ -49,6 +49,9 @@ const meta: Meta = {
|
|||||||
items: [],
|
items: [],
|
||||||
empty: 'No issues found',
|
empty: 'No issues found',
|
||||||
type: RuleType.VIOLATION,
|
type: RuleType.VIOLATION,
|
||||||
|
onSelectionChange: fn().mockName('onSelectionChange'),
|
||||||
|
selectedItems: new Map(),
|
||||||
|
toggleOpen: fn().mockName('toggleOpen'),
|
||||||
},
|
},
|
||||||
} satisfies Meta<typeof Report>;
|
} satisfies Meta<typeof Report>;
|
||||||
|
|
86
code/addons/a11y/src/components/Report/Report.tsx
Normal file
86
code/addons/a11y/src/components/Report/Report.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { EmptyTabContent, IconButton } from 'storybook/internal/components';
|
||||||
|
|
||||||
|
import { ChevronSmallDownIcon } from '@storybook/icons';
|
||||||
|
|
||||||
|
import type { Result } from 'axe-core';
|
||||||
|
import { styled } from 'storybook/theming';
|
||||||
|
|
||||||
|
import type { RuleType } from '../A11YPanel';
|
||||||
|
import { Details } from './Details';
|
||||||
|
|
||||||
|
const Wrapper = styled.div(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
width: '100%',
|
||||||
|
borderBottom: `1px solid ${theme.appBorderColor}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Icon = styled(ChevronSmallDownIcon)({
|
||||||
|
transition: 'transform 0.1s ease-in-out',
|
||||||
|
});
|
||||||
|
|
||||||
|
const HeaderBar = styled.div(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 5,
|
||||||
|
paddingLeft: 15,
|
||||||
|
minHeight: 40,
|
||||||
|
background: 'none',
|
||||||
|
color: 'inherit',
|
||||||
|
textAlign: 'left',
|
||||||
|
cursor: 'pointer',
|
||||||
|
width: '100%',
|
||||||
|
'&:hover': {
|
||||||
|
color: theme.color.secondary,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export interface ReportProps {
|
||||||
|
items: Result[];
|
||||||
|
empty: string;
|
||||||
|
type: RuleType;
|
||||||
|
onSelectionChange: (key: string) => void;
|
||||||
|
selectedItems: Map<Result['id'], string>;
|
||||||
|
toggleOpen: (event: React.SyntheticEvent<Element>, type: RuleType, item: Result) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Report: FC<ReportProps> = ({
|
||||||
|
items,
|
||||||
|
empty,
|
||||||
|
type,
|
||||||
|
onSelectionChange,
|
||||||
|
selectedItems,
|
||||||
|
toggleOpen,
|
||||||
|
}) => (
|
||||||
|
<>
|
||||||
|
{items && items.length ? (
|
||||||
|
items.map((item) => {
|
||||||
|
const selection = selectedItems.get(`${type}.${item.id}`);
|
||||||
|
return (
|
||||||
|
<Wrapper key={`${type}.${item.id}`}>
|
||||||
|
<HeaderBar onClick={(e) => toggleOpen(e, type, item)} role="button">
|
||||||
|
<strong>{item.help}</strong>
|
||||||
|
<IconButton onClick={(ev) => toggleOpen(ev, type, item)}>
|
||||||
|
<Icon style={{ transform: `rotate(${selection ? -180 : 0}deg)` }} />
|
||||||
|
</IconButton>
|
||||||
|
</HeaderBar>
|
||||||
|
{selection ? (
|
||||||
|
<Details
|
||||||
|
item={item}
|
||||||
|
type={type}
|
||||||
|
selection={selection}
|
||||||
|
onSelectionChange={onSelectionChange}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<EmptyTabContent title={empty} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
@ -1,25 +0,0 @@
|
|||||||
import type { FC } from 'react';
|
|
||||||
import React, { Fragment } from 'react';
|
|
||||||
|
|
||||||
import { EmptyTabContent } from 'storybook/internal/components';
|
|
||||||
|
|
||||||
import type { Result } from 'axe-core';
|
|
||||||
|
|
||||||
import type { RuleType } from '../A11YPanel';
|
|
||||||
import { Item } from './Item';
|
|
||||||
|
|
||||||
export interface ReportProps {
|
|
||||||
items: Result[];
|
|
||||||
empty: string;
|
|
||||||
type: RuleType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Report: FC<ReportProps> = ({ items, empty, type }) => (
|
|
||||||
<Fragment>
|
|
||||||
{items && items.length ? (
|
|
||||||
items.map((item) => <Item item={item} key={`${type}:${item.id}`} type={type} />)
|
|
||||||
) : (
|
|
||||||
<EmptyTabContent title={empty} />
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
@ -97,7 +97,7 @@ export const Tabs: React.FC<TabsProps> = ({ tabs }) => {
|
|||||||
|
|
||||||
const handleToggle = React.useCallback(
|
const handleToggle = React.useCallback(
|
||||||
(event: React.SyntheticEvent) => {
|
(event: React.SyntheticEvent) => {
|
||||||
setTab(parseInt(event.currentTarget.getAttribute('data-index') || '', 10));
|
setTab(event.currentTarget.getAttribute('data-type'));
|
||||||
},
|
},
|
||||||
[setTab]
|
[setTab]
|
||||||
);
|
);
|
||||||
@ -109,8 +109,8 @@ export const Tabs: React.FC<TabsProps> = ({ tabs }) => {
|
|||||||
{tabs.map((tab, index) => (
|
{tabs.map((tab, index) => (
|
||||||
<Item
|
<Item
|
||||||
key={index}
|
key={index}
|
||||||
data-index={index}
|
data-type={tab.type}
|
||||||
active={activeTab === index}
|
active={activeTab === tab.type}
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
@ -143,7 +143,7 @@ export const Tabs: React.FC<TabsProps> = ({ tabs }) => {
|
|||||||
</WithTooltip>
|
</WithTooltip>
|
||||||
</ActionsWrapper>
|
</ActionsWrapper>
|
||||||
</List>
|
</List>
|
||||||
{tabs[activeTab].panel}
|
{tabs.find((t) => t.type === activeTab)?.panel}
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -9627,4 +9627,4 @@ export const results = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
} as any;
|
124
code/yarn.lock
124
code/yarn.lock
@ -5276,6 +5276,35 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@radix-ui/primitive@npm:1.1.1":
|
||||||
|
version: 1.1.1
|
||||||
|
resolution: "@radix-ui/primitive@npm:1.1.1"
|
||||||
|
checksum: 10c0/6457bd8d1aa4ecb948e5d2a2484fc570698b2ab472db6d915a8f1eec04823f80423efa60b5ba840f0693bec2ca380333cc5f3b52586b40f407d9f572f9261f8d
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@radix-ui/react-collection@npm:1.1.2":
|
||||||
|
version: 1.1.2
|
||||||
|
resolution: "@radix-ui/react-collection@npm:1.1.2"
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-compose-refs": "npm:1.1.1"
|
||||||
|
"@radix-ui/react-context": "npm:1.1.1"
|
||||||
|
"@radix-ui/react-primitive": "npm:2.0.2"
|
||||||
|
"@radix-ui/react-slot": "npm:1.1.2"
|
||||||
|
peerDependencies:
|
||||||
|
"@types/react": "*"
|
||||||
|
"@types/react-dom": "*"
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
"@types/react":
|
||||||
|
optional: true
|
||||||
|
"@types/react-dom":
|
||||||
|
optional: true
|
||||||
|
checksum: 10c0/8376aa0c0f38efbb45e5c0a2e8724b0ca2ccdab511f5aee4c3eb62a89959b20be0d4dd410b7068bc13d722751cbc88e916e10573784fb26b084c43f930818715
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@radix-ui/react-compose-refs@npm:1.1.0":
|
"@radix-ui/react-compose-refs@npm:1.1.0":
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
resolution: "@radix-ui/react-compose-refs@npm:1.1.0"
|
resolution: "@radix-ui/react-compose-refs@npm:1.1.0"
|
||||||
@ -5505,6 +5534,26 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@radix-ui/react-presence@npm:1.1.2":
|
||||||
|
version: 1.1.2
|
||||||
|
resolution: "@radix-ui/react-presence@npm:1.1.2"
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-compose-refs": "npm:1.1.1"
|
||||||
|
"@radix-ui/react-use-layout-effect": "npm:1.1.0"
|
||||||
|
peerDependencies:
|
||||||
|
"@types/react": "*"
|
||||||
|
"@types/react-dom": "*"
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
"@types/react":
|
||||||
|
optional: true
|
||||||
|
"@types/react-dom":
|
||||||
|
optional: true
|
||||||
|
checksum: 10c0/0c6fa281368636308044df3be4c1f02733094b5e35ba04f26e610dd1c4315a245ffc758e0e176c444742a7a46f4328af1a9d8181e860175ec39338d06525a78d
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@radix-ui/react-primitive@npm:2.0.0":
|
"@radix-ui/react-primitive@npm:2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "@radix-ui/react-primitive@npm:2.0.0"
|
resolution: "@radix-ui/react-primitive@npm:2.0.0"
|
||||||
@ -5524,6 +5573,52 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@radix-ui/react-primitive@npm:2.0.2":
|
||||||
|
version: 2.0.2
|
||||||
|
resolution: "@radix-ui/react-primitive@npm:2.0.2"
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-slot": "npm:1.1.2"
|
||||||
|
peerDependencies:
|
||||||
|
"@types/react": "*"
|
||||||
|
"@types/react-dom": "*"
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
"@types/react":
|
||||||
|
optional: true
|
||||||
|
"@types/react-dom":
|
||||||
|
optional: true
|
||||||
|
checksum: 10c0/1af7a33a86f8bd2467f2300b1bb6ca9af67cae3950953ba543d2a625c17f341dff05d19056ece7b03e5ced8b9f8de99c74f806710ce0da6b9a000f2af063fffe
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@radix-ui/react-roving-focus@npm:1.1.2":
|
||||||
|
version: 1.1.2
|
||||||
|
resolution: "@radix-ui/react-roving-focus@npm:1.1.2"
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/primitive": "npm:1.1.1"
|
||||||
|
"@radix-ui/react-collection": "npm:1.1.2"
|
||||||
|
"@radix-ui/react-compose-refs": "npm:1.1.1"
|
||||||
|
"@radix-ui/react-context": "npm:1.1.1"
|
||||||
|
"@radix-ui/react-direction": "npm:1.1.0"
|
||||||
|
"@radix-ui/react-id": "npm:1.1.0"
|
||||||
|
"@radix-ui/react-primitive": "npm:2.0.2"
|
||||||
|
"@radix-ui/react-use-callback-ref": "npm:1.1.0"
|
||||||
|
"@radix-ui/react-use-controllable-state": "npm:1.1.0"
|
||||||
|
peerDependencies:
|
||||||
|
"@types/react": "*"
|
||||||
|
"@types/react-dom": "*"
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
"@types/react":
|
||||||
|
optional: true
|
||||||
|
"@types/react-dom":
|
||||||
|
optional: true
|
||||||
|
checksum: 10c0/80e378e1156d5b8af14995e908fe2358c8f4757fbf274e30d2ee3c1cedc3a0c7192524df7e3bb1d5011ee9ab8ab7445b60eff06617370e58abcd1ae97e0e40f6
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@radix-ui/react-scroll-area@npm:1.2.0-rc.7":
|
"@radix-ui/react-scroll-area@npm:1.2.0-rc.7":
|
||||||
version: 1.2.0-rc.7
|
version: 1.2.0-rc.7
|
||||||
resolution: "@radix-ui/react-scroll-area@npm:1.2.0-rc.7"
|
resolution: "@radix-ui/react-scroll-area@npm:1.2.0-rc.7"
|
||||||
@ -5566,7 +5661,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@radix-ui/react-slot@npm:^1.0.2":
|
"@radix-ui/react-slot@npm:1.1.2, @radix-ui/react-slot@npm:^1.0.2":
|
||||||
version: 1.1.2
|
version: 1.1.2
|
||||||
resolution: "@radix-ui/react-slot@npm:1.1.2"
|
resolution: "@radix-ui/react-slot@npm:1.1.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -5581,6 +5676,32 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@radix-ui/react-tabs@npm:^1.1.3":
|
||||||
|
version: 1.1.3
|
||||||
|
resolution: "@radix-ui/react-tabs@npm:1.1.3"
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/primitive": "npm:1.1.1"
|
||||||
|
"@radix-ui/react-context": "npm:1.1.1"
|
||||||
|
"@radix-ui/react-direction": "npm:1.1.0"
|
||||||
|
"@radix-ui/react-id": "npm:1.1.0"
|
||||||
|
"@radix-ui/react-presence": "npm:1.1.2"
|
||||||
|
"@radix-ui/react-primitive": "npm:2.0.2"
|
||||||
|
"@radix-ui/react-roving-focus": "npm:1.1.2"
|
||||||
|
"@radix-ui/react-use-controllable-state": "npm:1.1.0"
|
||||||
|
peerDependencies:
|
||||||
|
"@types/react": "*"
|
||||||
|
"@types/react-dom": "*"
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
"@types/react":
|
||||||
|
optional: true
|
||||||
|
"@types/react-dom":
|
||||||
|
optional: true
|
||||||
|
checksum: 10c0/2f621c43a8e1dd0d54c828f8b4d88414c9114af6b720a650ad9587cc0a7a7536da778f2fe5181a38494cc2956f2b238fbe64790f6daad1d058b34f4acaee520e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@radix-ui/react-use-callback-ref@npm:1.1.0":
|
"@radix-ui/react-use-callback-ref@npm:1.1.0":
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
resolution: "@radix-ui/react-use-callback-ref@npm:1.1.0"
|
resolution: "@radix-ui/react-use-callback-ref@npm:1.1.0"
|
||||||
@ -6048,6 +6169,7 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@storybook/addon-a11y@workspace:addons/a11y"
|
resolution: "@storybook/addon-a11y@workspace:addons/a11y"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@radix-ui/react-tabs": "npm:^1.1.3"
|
||||||
"@storybook/addon-highlight": "workspace:*"
|
"@storybook/addon-highlight": "workspace:*"
|
||||||
"@storybook/global": "npm:^5.0.0"
|
"@storybook/global": "npm:^5.0.0"
|
||||||
"@storybook/icons": "npm:^1.2.12"
|
"@storybook/icons": "npm:^1.2.12"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user