Skip to content

Commit 82fbbb7

Browse files
authored
Merge pull request #163 from lambda-curry/codegen-bot/add-use-on-form-value-change-hook-f8b7b4eb
2 parents 7788aa7 + 21fe6f0 commit 82fbbb7

File tree

7 files changed

+761
-9
lines changed

7 files changed

+761
-9
lines changed

apps/docs/src/lib/storybook/react-router-stub.tsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useMemo } from 'react';
12
import type { Decorator } from '@storybook/react-vite';
23
import type { ComponentType } from 'react';
34
import {
@@ -30,27 +31,40 @@ interface RemixStubOptions {
3031
export const withReactRouterStubDecorator = (options: RemixStubOptions): Decorator => {
3132
const { routes, initialPath = '/' } = options;
3233

34+
// We define the Stub component outside the return function to ensure it's not recreated
35+
// on every render of the Story component itself.
36+
const CachedStub: ComponentType<{ initialEntries?: string[] }> | null = null;
37+
const lastMappedRoutes: StubRouteObject[] | null = null;
38+
3339
return (Story, context) => {
3440
// Map routes to include the Story component if no Component is provided
35-
const mappedRoutes = routes.map((route) => ({
36-
...route,
37-
Component: route.Component ?? (() => <Story {...context.args} />),
38-
}));
41+
const mappedRoutes = useMemo(
42+
() =>
43+
routes.map((route) => ({
44+
...route,
45+
Component: route.Component ?? Story,
46+
})),
47+
[Story],
48+
);
3949

4050
// Get the base path (without existing query params from options)
41-
const basePath = initialPath.split('?')[0];
51+
const basePath = useMemo(() => initialPath.split('?')[0], []);
4252

4353
// Get the current search string from the actual browser window, if available
4454
// If not available, use a default search string with parameters needed for the data table
4555
const currentWindowSearch = typeof window !== 'undefined' ? window.location.search : '?page=0&pageSize=10';
4656

4757
// Combine them for the initial entry
48-
const actualInitialPath = `${basePath}${currentWindowSearch}`;
58+
const actualInitialPath = useMemo(() => `${basePath}${currentWindowSearch}`, [basePath, currentWindowSearch]);
4959

5060
// Use React Router's official createRoutesStub
51-
const Stub = createRoutesStub(mappedRoutes);
61+
// We memoize the Stub component to prevent unnecessary remounts of the entire story
62+
// when the decorator re-renders.
63+
const Stub = useMemo(() => createRoutesStub(mappedRoutes), [mappedRoutes]);
64+
65+
const initialEntries = useMemo(() => [actualInitialPath], [actualInitialPath]);
5266

53-
return <Stub initialEntries={[actualInitialPath]} />;
67+
return <Stub initialEntries={initialEntries} />;
5468
};
5569
};
5670

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { userEvent, within, screen, waitFor } from '@storybook/test';
2+
3+
/**
4+
* A robust helper to select an option from a Radix-based Select/Combobox.
5+
* Handles portals, animations, and pointer-event blockers.
6+
*/
7+
export async function selectRadixOption(
8+
canvasElement: HTMLElement,
9+
options: {
10+
triggerRole?: 'combobox' | 'button';
11+
triggerName: string | RegExp;
12+
optionName: string | RegExp;
13+
optionTestId?: string;
14+
},
15+
) {
16+
const canvas = within(canvasElement);
17+
const { triggerRole = 'combobox', triggerName, optionName, optionTestId } = options;
18+
19+
// 1. Find and click the trigger within the component canvas
20+
const trigger = await canvas.findByRole(triggerRole, { name: triggerName });
21+
if (!trigger) throw new Error(`Trigger with role ${triggerRole} and name ${triggerName} not found`);
22+
23+
await userEvent.click(trigger);
24+
25+
// 2. Wait for the listbox to appear in the document body (Portal)
26+
// We use a slightly longer timeout for CI stability.
27+
const listbox = await screen.findByRole('listbox', {}, { timeout: 3000 });
28+
if (!listbox) throw new Error('Radix listbox (portal) not found after clicking trigger');
29+
30+
// 3. Find the option specifically WITHIN the listbox
31+
let option: HTMLElement | null = null;
32+
if (optionTestId) {
33+
option = await within(listbox).findByTestId(optionTestId);
34+
} else {
35+
option = await within(listbox).findByRole('option', { name: optionName });
36+
}
37+
38+
if (!option) throw new Error(`Option ${optionName || optionTestId} not found in listbox`);
39+
40+
// 4. Click the option
41+
// pointerEventsCheck: 0 is used to bypass Radix's temporary pointer-event locks during animations
42+
await userEvent.click(option, { pointerEventsCheck: 0 });
43+
44+
// 5. Verify the dropdown closed (optional but ensures stability)
45+
await waitFor(() => {
46+
const listbox = screen.queryByRole('listbox');
47+
if (listbox) throw new Error('Listbox still visible');
48+
});
49+
}

apps/docs/src/remix-hook-form/calendar-with-month-year-select.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ const ControlledCalendarWithFormExample = () => {
8686
});
8787

8888
const [dropdown, setDropdown] = React.useState<'dropdown' | 'dropdown-months' | 'dropdown-years'>('dropdown');
89-
const [date, setDate] = React.useState<Date | undefined>();
89+
const [date, setDate] = React.useState<Date | undefined>(new Date(2025, 5, 12));
9090

9191
const dropdownOptions = [
9292
{ label: 'Month and Year', value: 'dropdown' },

0 commit comments

Comments
 (0)