Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 10.1.12

- AddonVitest: Improve perf & fix loading incorrect `.env` file - [#33469](https://github.com/storybookjs/storybook/pull/33469), thanks @ndelangen!
- Core: Fix onboarding visual bugs, survey telemetry and modal dismissal - [#33326](https://github.com/storybookjs/storybook/pull/33326), thanks @ghengeveld!
- UI: Fix regression in select close handler focus - [#33470](https://github.com/storybookjs/storybook/pull/33470), thanks @Sidnioulz!
- UI: Improve landmark navigation - [#33457](https://github.com/storybookjs/storybook/pull/33457), thanks @Sidnioulz!

## 10.1.11

- React: Fix several CSF factory bugs - [#33354](https://github.com/storybookjs/storybook/pull/33354), thanks @kasperpeulen!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const TableWrapper = styled.table<{
// End Resets

fontSize: theme.typography.size.s2 - 1,
lineHeight: '20px',
lineHeight: '19px',
textAlign: 'left',
width: '100%',

Expand Down
18 changes: 13 additions & 5 deletions code/addons/onboarding/src/Onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,7 @@ export default function Onboarding({
userAgent,
});
}
// remove onboarding query parameter from current url
const url = new URL(window.location.href);
url.searchParams.set('onboarding', 'false');
history.replaceState({}, '', url.href);
api.setQueryParams({ onboarding: 'false' });
api.applyQueryParams({ onboarding: undefined }, { replace: true });
setEnabled(false);
},
[api, setEnabled, userAgent]
Expand All @@ -113,12 +109,23 @@ export default function Onboarding({
[api, selectStory, userAgent]
);

useEffect(() => {
if (step === '6:IntentSurvey' && !hasCompletedSurvey) {
api.emit(ADDON_ONBOARDING_CHANNEL, {
from: 'onboarding',
type: 'openSurvey',
userAgent,
});
}
}, [api, hasCompletedSurvey, step, userAgent]);

useEffect(() => {
api.setQueryParams({ onboarding: 'true' });
selectStory('example-button--primary');
api.togglePanel(true);
api.togglePanelPosition('bottom');
api.setSelectedPanel(ADDON_CONTROLS_ID);
api.setSizes({ bottomPanelHeight: 300 });
}, [api, selectStory]);

useEffect(() => {
Expand Down Expand Up @@ -304,6 +311,7 @@ export default function Onboarding({
<SplashScreen onDismiss={() => setStep('2:Controls')} />
) : step === '6:IntentSurvey' ? (
<IntentSurvey
isOpen={enabled}
onComplete={completeSurvey}
onDismiss={() => disableOnboarding('6:IntentSurvey')}
/>
Expand Down
21 changes: 14 additions & 7 deletions code/addons/onboarding/src/Survey.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect, useState } from 'react';

import { type API } from 'storybook/manager-api';
import { ThemeProvider, convert } from 'storybook/theming';
Expand All @@ -12,12 +12,19 @@ export default function Survey({ api }: { api: API }) {
// eslint-disable-next-line compat/compat
const userAgent = globalThis?.navigator?.userAgent;

const [isOpen, setIsOpen] = useState(true);

useEffect(() => {
api.emit(ADDON_ONBOARDING_CHANNEL, {
from: 'guide',
type: 'openSurvey',
userAgent,
});
}, [api, userAgent]);

const disableOnboarding = useCallback(() => {
// remove onboarding query parameter from current url
const url = new URL(window.location.href);
url.searchParams.set('onboarding', 'false');
history.replaceState({}, '', url.href);
api.setQueryParams({ onboarding: 'false' });
setIsOpen(false);
api.applyQueryParams({ onboarding: undefined }, { replace: true });
}, [api]);

const complete = useCallback(
Expand All @@ -41,7 +48,7 @@ export default function Survey({ api }: { api: API }) {

return (
<ThemeProvider theme={theme}>
<IntentSurvey onComplete={complete} onDismiss={dismiss} />
<IntentSurvey isOpen={isOpen} onComplete={complete} onDismiss={dismiss} />
</ThemeProvider>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { IntentSurvey } from './IntentSurvey';
const meta = {
component: IntentSurvey,
args: {
isOpen: true,
onComplete: fn(),
onDismiss: fn(),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,11 @@ const Checkbox = styled(Form.Checkbox)({
});

export const IntentSurvey = ({
isOpen,
onComplete,
onDismiss,
}: {
isOpen: boolean;
onComplete: (formData: Record<string, Record<string, boolean>>) => void;
onDismiss: () => void;
}) => {
Expand Down Expand Up @@ -172,7 +174,7 @@ export const IntentSurvey = ({
return (
<Modal
ariaLabel="Storybook user survey"
defaultOpen
open={isOpen}
width={420}
onOpenChange={(isOpen) => {
if (!isOpen) {
Expand Down
1 change: 1 addition & 0 deletions code/core/src/components/components/Modal/Modal.styled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export const Close = ({ asChild, children, onClick, ...props }: CloseProps) => {

return (
<Button
type="button"
padding="small"
ariaLabel="Close modal"
variant="ghost"
Expand Down
109 changes: 108 additions & 1 deletion code/core/src/components/components/Select/Select.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';

import { Button } from 'storybook/internal/components';
import { Button, Toolbar } from 'storybook/internal/components';

import { LinuxIcon } from '@storybook/icons';

Expand Down Expand Up @@ -199,6 +199,113 @@ export const WithSiblings = meta.story({
<Button ariaLabel={false}>After</Button>
</Row>
),
play: async ({ canvas, step }) => {
const user = userEvent.setup();

await step('Open select and select an option', async () => {
const select = canvas.getByRole('button', { name: /Animal/i });
await user.click(select);

const listbox = await screen.findByRole('listbox');
expect(listbox).toBeInTheDocument();

const option = within(listbox).getByRole('option', { name: 'Frog' });
await user.click(option);
});

await step('Tab should land on sibling after select', async () => {
const select = canvas.getByRole('button', { name: /Frog/i });
expect(select).toHaveFocus();

await user.tab();

const afterButton = canvas.getByRole('button', { name: 'After' });
expect(afterButton).toHaveFocus();
});

await step('Navigate back and reopen select', async () => {
await user.tab({ shift: true });

const select = canvas.getByRole('button', { name: /Frog/i });
expect(select).toHaveFocus();

await user.keyboard('{Enter}');

const listbox = await screen.findByRole('listbox');
expect(listbox).toBeInTheDocument();
});

await step('Escape should return to select trigger', async () => {
await user.keyboard('{Escape}');

const select = canvas.getByRole('button', { name: /Frog/i });
expect(select).toHaveFocus();
});
},
});

export const WithSiblingsInToolbar = meta.story({
name: 'With Siblings in Toolbar',
render: (args) => (
<Toolbar aria-label="Test toolbar">
<Button ariaLabel="Before button">Before</Button>
<Select {...args} />
<Button ariaLabel="After button">After</Button>
</Toolbar>
),
play: async ({ canvas, step }) => {
const user = userEvent.setup();

await step('Navigate to select with ArrowRight', async () => {
const beforeButton = canvas.getByRole('button', { name: 'Before button' });
beforeButton.focus();
expect(beforeButton).toHaveFocus();

await user.keyboard('{ArrowRight}');

const select = canvas.getByRole('button', { name: /Animal/i });
expect(select).toHaveFocus();
});

await step('Open select and select an option', async () => {
await user.keyboard('{Enter}');

const listbox = await screen.findByRole('listbox');
expect(listbox).toBeInTheDocument();

const option = within(listbox).getByRole('option', { name: 'Frog' });
await user.click(option);
});

await step('ArrowRight should land on sibling after select', async () => {
const select = canvas.getByRole('button', { name: /Frog/i });
expect(select).toHaveFocus();

await user.keyboard('{ArrowRight}');

const afterButton = canvas.getByRole('button', { name: 'After button' });
expect(afterButton).toHaveFocus();
});

await step('Navigate back with ArrowLeft and reopen select', async () => {
await user.keyboard('{ArrowLeft}');

const select = canvas.getByRole('button', { name: /Frog/i });
expect(select).toHaveFocus();

await user.keyboard('{Enter}');

const listbox = await screen.findByRole('listbox');
expect(listbox).toBeInTheDocument();
});

await step('Escape should return to select trigger', async () => {
await user.keyboard('{Escape}');

const select = canvas.getByRole('button', { name: /Frog/i });
expect(select).toHaveFocus();
});
},
});

export const DefaultOption = meta.story({
Expand Down
14 changes: 12 additions & 2 deletions code/core/src/components/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
ref
) => {
const [isOpen, setIsOpen] = useState(props.defaultOpen || false);
const [shouldRefocusTrigger, setShouldRefocusTrigger] = useState(false);
const triggerRef = useObjectRef(ref);

const id = useMemo(() => {
Expand All @@ -226,8 +227,17 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(

const handleClose = useCallback(() => {
setIsOpen(false);
triggerRef.current?.focus();
}, [triggerRef]);
setShouldRefocusTrigger(true);
}, []);

// We must delay refocusing the trigger because we first need the listbox to close,
// and @react-aria/overlays to remove the inert attribute set up by MinimalistPopover.
useEffect(() => {
if (!otState.isOpen && shouldRefocusTrigger) {
triggerRef.current?.focus();
setShouldRefocusTrigger(false);
}
}, [otState.isOpen, shouldRefocusTrigger, triggerRef]);

// The last selected option(s), which will be used by the app.
const [selectedOptions, setSelectedOptions] = useState<InternalOption[]>(
Expand Down
37 changes: 19 additions & 18 deletions code/core/src/controls/components/ControlsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, { useEffect, useMemo, useState } from 'react';

import { ScrollArea } from 'storybook/internal/components';
import type { ArgTypes } from 'storybook/internal/types';

import { global } from '@storybook/global';
Expand Down Expand Up @@ -31,11 +30,15 @@ const clean = (obj: { [key: string]: any }) =>
{} as typeof obj
);

const AddonWrapper = styled.div<{ showSaveFromUI: boolean }>(({ showSaveFromUI }) => ({
display: 'grid',
gridTemplateRows: showSaveFromUI ? '1fr 41px' : '1fr',
const AddonWrapper = styled.div<{ showSaveFromUI: boolean }>(({ showSaveFromUI, theme }) => ({
height: '100%',
maxHeight: '100vh',
paddingBottom: showSaveFromUI ? 41 : 0,
backgroundColor: theme.background.content,

table: {
backgroundColor: theme.background.app,
},
}));

interface ControlsParameters {
Expand Down Expand Up @@ -101,20 +104,18 @@ export const ControlsPanel = ({ saveStory, createStory }: ControlsPanelProps) =>

return (
<AddonWrapper showSaveFromUI={showSaveFromUI}>
<ScrollArea vertical>
<ArgsTable
key={path} // resets state when switching stories
compact={!expanded && hasControls}
rows={withPresetColors}
args={args}
globals={globals}
updateArgs={updateArgs}
resetArgs={resetArgs}
inAddonPanel
sort={sort}
isLoading={isLoading}
/>
</ScrollArea>
<ArgsTable
key={path} // resets state when switching stories
compact={!expanded && hasControls}
rows={withPresetColors}
args={args}
globals={globals}
updateArgs={updateArgs}
resetArgs={resetArgs}
inAddonPanel
sort={sort}
isLoading={isLoading}
/>
{showSaveFromUI && <SaveStory {...{ resetArgs, saveStory, createStory }} />}
</AddonWrapper>
);
Expand Down
4 changes: 3 additions & 1 deletion code/core/src/controls/components/SaveStory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ const highlight = keyframes({

const Container = styled.div({
containerType: 'size',
position: 'sticky',
position: 'absolute',
bottom: 0,
width: '100%',
height: 41,
overflow: 'hidden',
zIndex: 1,
Expand All @@ -43,6 +44,7 @@ const Info = styled.div({
display: 'flex',
flex: '99 0 auto',
alignItems: 'center',
marginInlineStart: 7,
marginInlineEnd: 10,
gap: 6,
});
Expand Down
2 changes: 1 addition & 1 deletion code/core/src/controls/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export default addons.register(ADDON_ID, (api) => {
return null;
}
return (
<AddonPanel active={active} hasScrollbar={false}>
<AddonPanel active={active}>
<ControlsPanel saveStory={saveStory} createStory={createStory} />
</AddonPanel>
);
Expand Down
5 changes: 4 additions & 1 deletion code/core/src/theming/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,12 @@ export const createGlobal = memoize(1)(({
...resetStyles,
body: {
...resetStyles.body,
position: 'fixed',
width: '100vw',
height: '100vh',
overflow: 'hidden',
color: color.defaultText,
background: background.app,
overflow: 'hidden',
},

hr: {
Expand Down
Loading