Skip to content
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
4 changes: 4 additions & 0 deletions code/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,11 @@ const decorators = [
] satisfies Decorator[];

const parameters = {
// Redundant but useful for testing lang parameter propagation in stories.
htmlLang: 'en-CA',
docs: {
// Redundant but useful for testing lang parameter propagation in docs.
lang: 'en-US',
theme: themes.light,
codePanel: true,
source: {
Expand Down
8 changes: 6 additions & 2 deletions code/addons/docs/src/blocks/blocks/ArgTypes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,18 +107,20 @@ function renderArgTypesTables({
include,
exclude,
sort,
docsLang,
}: {
mainName?: string;
mainRows: StrictArgTypes;
subcomponentRows: Record<string, StrictArgTypes>;
include?: PropDescriptor;
exclude?: PropDescriptor;
sort?: SortType;
docsLang?: string;
}) {
const filteredMainRows = filterArgTypes(mainRows, include, exclude);

if (Object.keys(subcomponentRows).length === 0) {
return <PureArgsTable rows={filteredMainRows as any} sort={sort} />;
return <PureArgsTable rows={filteredMainRows as any} sort={sort} docsLang={docsLang} />;
}

const tabs = {
Expand All @@ -134,7 +136,7 @@ function renderArgTypesTables({
),
};

return <TabbedArgsTable tabs={tabs as any} sort={sort} />;
return <TabbedArgsTable tabs={tabs as any} sort={sort} docsLang={docsLang} />;
}

const LegacyArgTypes: FC<ArgTypesProps> = (props) => {
Expand All @@ -148,6 +150,7 @@ const LegacyArgTypes: FC<ArgTypesProps> = (props) => {
mainName: getComponentName(component),
mainRows: argTypes,
subcomponentRows: extractSubcomponentArgTypes(subcomponents, parameters),
docsLang: parameters?.docs?.lang,
...filterProps,
});
};
Expand Down Expand Up @@ -175,6 +178,7 @@ const DocgenServiceArgTypes: FC<ArgTypesProps> = (props) => {
mainName: getComponentName(component) ?? serviceRows.serviceComponentName,
mainRows: serviceRows.mainRows,
subcomponentRows: serviceRows.subcomponentRows,
docsLang: parameters?.docs?.lang,
...filterProps,
});
};
Expand Down
8 changes: 7 additions & 1 deletion code/addons/docs/src/blocks/blocks/Controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { useId } from '@react-aria/utils';
import type { Args, Globals, Renderer, StrictArgTypes } from 'storybook/internal/csf';
import type { DocsContextProps, ModuleExports, PreparedStory } from 'storybook/internal/types';

import { filterArgTypes } from 'storybook/preview-api';
import type { PropDescriptor } from 'storybook/preview-api';
import { filterArgTypes } from 'storybook/preview-api';

import type { SortType } from '../components';
import { ArgsTable as PureArgsTable, TabbedArgsTable } from '../components';
Expand Down Expand Up @@ -50,6 +50,7 @@ type ControlsTablesProps = ControlsInteractiveState & {
exclude?: PropDescriptor;
sort?: SortType;
storyId: string;
docsLang?: string;
};

function getControlsFilterProps(story: PreparedStory, props: ControlsProps): ControlsParameters {
Expand Down Expand Up @@ -89,6 +90,7 @@ const ControlsTables: FC<ControlsTablesProps> = ({
globals,
updateArgs,
resetArgs,
docsLang,
}) => {
const filteredMainRows = filterArgTypes(mainRows, include, exclude);

Expand All @@ -107,6 +109,7 @@ const ControlsTables: FC<ControlsTablesProps> = ({
globals={globals}
updateArgs={updateArgs}
resetArgs={resetArgs}
docsLang={docsLang}
/>
);
}
Expand Down Expand Up @@ -134,6 +137,7 @@ const ControlsTables: FC<ControlsTablesProps> = ({
resetArgs={resetArgs}
storyId={storyId}
controlsId={controlsId}
docsLang={docsLang}
/>
);
};
Expand All @@ -155,6 +159,7 @@ const LegacyControls: FC<ControlsStoryProps> = ({ story, context, ...props }) =>
{...filterProps}
storyId={story.id}
{...interactiveState}
docsLang={parameters?.docs?.lang}
/>
);
};
Expand Down Expand Up @@ -187,6 +192,7 @@ const DocgenServiceControls: FC<ControlsStoryProps> = ({ story, context, ...prop
{...filterProps}
storyId={story.id}
{...interactiveState}
docsLang={parameters?.docs?.lang}
/>
);
};
Expand Down
40 changes: 30 additions & 10 deletions code/addons/docs/src/blocks/blocks/Description.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { InvalidBlockOfPropError } from 'storybook/internal/preview-errors';

import { DocsContext } from './DocsContext';
import { Markdown } from './Markdown';
import type { Of } from './useOf';
import { useOf } from './useOf';
import { useServiceDocgen } from './use-service-docgen.ts';
import { useServiceStoryDoc } from './use-service-story-docs.ts';
import type { Of } from './useOf';
import { useOf } from './useOf';
import { withMdxComponentOverride } from './with-mdx-component-override';

export enum DescriptionType {
Expand Down Expand Up @@ -78,45 +78,58 @@ type ResolvedOf = ReturnType<typeof useOf>;
const DescriptionBody: FC<{
resolvedOf: ResolvedOf;
serviceComponentDescription?: string;
lang?: string;
storyDocsDescription?: string | null;
}> = ({ resolvedOf, serviceComponentDescription, storyDocsDescription }) => {
}> = ({ resolvedOf, serviceComponentDescription, lang, storyDocsDescription }) => {
const markdown = getDescriptionFromResolvedOf(
resolvedOf,
serviceComponentDescription,
storyDocsDescription
);

return markdown ? <Markdown>{markdown}</Markdown> : null;
return markdown ? <Markdown lang={lang}>{markdown}</Markdown> : null;
};

const DescriptionStoryWithServices: FC<{ resolvedOf: Extract<ResolvedOf, { type: 'story' }> }> = ({
resolvedOf,
}) => {
const DescriptionStoryWithServices: FC<{
resolvedOf: Extract<ResolvedOf, { type: 'story' }>;
}> = ({ resolvedOf }) => {
const storyDocsDescription = useServiceStoryDoc(resolvedOf.story.id).data?.description;
return <DescriptionBody resolvedOf={resolvedOf} storyDocsDescription={storyDocsDescription} />;
const lang = resolvedOf.story.parameters.docs?.lang || 'en';
return (
<DescriptionBody
resolvedOf={resolvedOf}
storyDocsDescription={storyDocsDescription}
lang={lang}
/>
);
};

const DescriptionComponentWithServices: FC<{
resolvedOf: Extract<ResolvedOf, { type: 'meta' | 'component' }>;
componentId: string;
}> = ({ resolvedOf, componentId }) => {
const serviceComponentDescription = useServiceDocgen(componentId).data?.description || undefined;
const lang =
resolvedOf.type === 'meta'
? resolvedOf.preparedMeta.parameters.docs?.lang || 'en'
: resolvedOf.projectAnnotations.parameters?.docs?.lang || 'en';
return (
<DescriptionBody
resolvedOf={resolvedOf}
serviceComponentDescription={serviceComponentDescription}
lang={lang}
/>
);
};

const DescriptionImpl: FC<DescriptionProps> = (props) => {
const { of } = props;
const context = useContext(DocsContext);

if ('of' in props && of === undefined) {
throw new InvalidBlockOfPropError();
}
const resolvedOf = useOf(of || 'meta');
const context = useContext(DocsContext);

// The docgen service contributes a fallback description, but its two sources need different hooks
// (story-docs by story id, docgen by component id), so each lives in its own child component to
Expand All @@ -137,7 +150,14 @@ const DescriptionImpl: FC<DescriptionProps> = (props) => {
}
}

return <DescriptionBody resolvedOf={resolvedOf} />;
const parameters =
resolvedOf.type === 'story'
? resolvedOf.story.parameters
: resolvedOf.type === 'meta'
? resolvedOf.preparedMeta.parameters
: resolvedOf.projectAnnotations.parameters;
const lang = parameters?.docs?.lang || 'en';
return <DescriptionBody resolvedOf={resolvedOf} lang={lang} />;
};

export const Description = withMdxComponentOverride('Description', DescriptionImpl);
10 changes: 8 additions & 2 deletions code/addons/docs/src/blocks/blocks/DocsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React, { useEffect, useMemo } from 'react';
import type { Renderer } from 'storybook/internal/types';

import type { ThemeVars } from 'storybook/theming';
import { ThemeProvider, ensure as ensureTheme } from 'storybook/theming';
import { ensure as ensureTheme, ThemeProvider } from 'storybook/theming';

import { DocsPageWrapper } from '../components';
import { TableOfContents } from '../components/TableOfContents';
Expand All @@ -28,13 +28,18 @@ export const DocsContainer: FC<PropsWithChildren<DocsContainerProps>> = ({
}) => {
const slugger = useMemo(() => createDocsSlugger(), []);
let toc;
// Language of docs prose (descriptions, ArgTypes description cells, free MDX prose).
let lang;

try {
const meta = context.resolveOf('meta', ['meta']);
toc = meta.preparedMeta.parameters?.docs?.toc;
const metaParameters = meta.preparedMeta.parameters;
toc = metaParameters?.docs?.toc;
lang = metaParameters?.docs?.lang || 'en';
} catch (err) {
// No meta, falling back to project annotations
toc = context?.projectAnnotations?.parameters?.docs?.toc;
lang = context?.projectAnnotations?.parameters?.docs?.lang || 'en';
}

useEffect(() => {
Expand All @@ -61,6 +66,7 @@ export const DocsContainer: FC<PropsWithChildren<DocsContainerProps>> = ({
<SourceContainer channel={context.channel}>
<ThemeProvider theme={ensureTheme(theme as ThemeVars)}>
<DocsPageWrapper
lang={lang}
toc={
toc ? (
<TableOfContents
Expand Down
14 changes: 14 additions & 0 deletions code/addons/docs/src/blocks/blocks/Story.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,20 @@ export const Inline: Story = {
},
};

export const InlineHtmlLang: Story = {
...Inline,
args: {
of: StoryParametersStories.HtmlLang,
inline: true,
},
play: async ({ canvasElement }) => {
await waitFor(() => {
const inner = canvasElement.querySelector('[id$="-inner"]');
expect(inner).toHaveAttribute('lang', 'fr');
});
},
};

export const InlineWithHeightProps: Story = {
...Inline,
args: {
Expand Down
5 changes: 3 additions & 2 deletions code/addons/docs/src/blocks/components/ArgsTable/ArgRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface ArgRowProps {
initialExpandedArgs?: boolean;
storyId?: string;
controlsId?: string;
docsLang?: string;
}

const Name = styled.span({ fontWeight: 'bold' });
Expand Down Expand Up @@ -105,7 +106,7 @@ const toSummary = (value: any) => {
};

export const ArgRow: FC<ArgRowProps> = (props) => {
const { row, updateArgs, compact, expandable, initialExpandedArgs } = props;
const { row, updateArgs, compact, expandable, initialExpandedArgs, docsLang } = props;
const { name, description } = row;
const table = (row.table || {}) as TableAnnotation;
const type = table.type || toSummary(row.type);
Expand All @@ -126,7 +127,7 @@ export const ArgRow: FC<ArgRowProps> = (props) => {
{compact ? null : (
<td>
{hasDescription && (
<Description>
<Description lang={docsLang}>
<Markdown>{description}</Markdown>
</Description>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// @vitest-environment happy-dom
import { cleanup, render } from '@testing-library/react';
import { afterEach, describe, expect, it } from 'vitest';

import React from 'react';

import { ThemeProvider, convert, themes } from 'storybook/theming';

import * as ArgRow from './ArgRow.stories';
import { ArgsTable } from './ArgsTable';

const renderTable = (docsLang?: string) =>
render(
<ThemeProvider theme={convert(themes.light)}>
{/* Wrap in a non-en region to prove chrome re-asserts English */}
<div lang="de">
<ArgsTable rows={{ stringType: ArgRow.String.args.row }} docsLang={docsLang} />
</div>
</ThemeProvider>
);

describe('ArgsTable chrome stays English', () => {
afterEach(() => cleanup());

it('declares English on the table so the chrome stays English under a non-en region', () => {
const { container } = renderTable();
expect(container.querySelector('.docblock-argstable')?.getAttribute('lang')).toBe('en');
});

it('keeps the arg-name cell announced in English', () => {
const { container } = renderTable();
const nameCell = container.querySelector('tbody tr td:first-of-type');
// The cell inherits English from the table wrapper rather than declaring its own lang.
expect(nameCell?.closest('[lang]')?.getAttribute('lang')).toBe('en');
});

it('languages argument descriptions with docs.lang', () => {
const { container } = renderTable('de');
const description = container.querySelector('tbody tr td:nth-of-type(2) > div');
expect(description?.getAttribute('lang')).toBe('de');
Comment thread
Sidnioulz marked this conversation as resolved.
});
});
12 changes: 11 additions & 1 deletion code/addons/docs/src/blocks/components/ArgsTable/ArgsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ export interface ArgsTableOptionProps {
sort?: SortType;
storyId?: string;
controlsId?: string;
docsLang?: string;
}
interface ArgsTableDataProps {
rows: ArgTypes;
Expand Down Expand Up @@ -342,6 +343,7 @@ export const ArgsTable: FC<ArgsTableProps> = (props) => {
isLoading,
storyId,
controlsId,
docsLang,
} = props;

const { rows, args, globals } =
Expand Down Expand Up @@ -431,6 +433,7 @@ export const ArgsTable: FC<ArgsTableProps> = (props) => {
onClick={handleResetClick}
disabled={isResetting}
ariaLabel={isResetting ? 'Resetting controls...' : 'Reset controls'}
lang="en"
>
<UndoIcon />
</StyledButton>
Expand All @@ -440,6 +443,7 @@ export const ArgsTable: FC<ArgsTableProps> = (props) => {
<TableWrapper
{...{ compact, inAddonPanel, inTabPanel }}
className="docblock-argstable sb-unstyled"
lang="en"
>
<thead className="docblock-argstable-head">
<tr>
Expand All @@ -465,7 +469,13 @@ export const ArgsTable: FC<ArgsTableProps> = (props) => {
</thead>
<tbody className="docblock-argstable-body">
{groups.ungrouped.map((row) => (
<ArgRow key={row.key} row={row} arg={args && args[row.key]} {...common} />
<ArgRow
key={row.key}
row={row}
arg={args && args[row.key]}
docsLang={docsLang}
{...common}
/>
))}

{Object.entries(groups.ungroupedSubsections).map(([subcategory, subsection]) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const TabbedArgsTable: FC<TabbedArgsTableProps> = ({ tabs, ...props }) =>
// are subcomponents, never controllable, so we filter out the props indicating
// controllability. Pass a stable element (not an inline function component) so React
// reconciles the tab panel instead of remounting it on every args update.
const argsTableProps = index === 0 ? props : { sort: props.sort };
const argsTableProps = index === 0 ? props : { sort: props.sort, docsLang: props.docsLang };

return {
id: `prop_table_div_${label}`,
Expand Down
Loading
Loading