diff --git a/.changeset/calendar-deselect-month.md b/.changeset/calendar-deselect-month.md new file mode 100644 index 0000000..2d69b39 --- /dev/null +++ b/.changeset/calendar-deselect-month.md @@ -0,0 +1,7 @@ +--- +'@wirecore/wirelayer-vue': patch +'@wirecore/wirelayer-react': patch +--- + +`Calendar` and `DatePicker` with `allowDeselect`: clearing the chosen day keeps its month in view and the focus on that +day. Before, the view jumped to today's month. diff --git a/.changeset/wizard-next-loading.md b/.changeset/wizard-next-loading.md new file mode 100644 index 0000000..0288adc --- /dev/null +++ b/.changeset/wizard-next-loading.md @@ -0,0 +1,8 @@ +--- +'@wirecore/wirelayer-vue': patch +'@wirecore/wirelayer-react': patch +--- + +`Stepper` and `Wizard`: while the current step is busy – an async `validate` in the `Wizard`, `loading` on the item in +the `Stepper` – "Next" shows a spinner and ignores clicks (`aria-busy`, `aria-disabled`) but keeps the focus. Before, +only the step's number turned into a spinner, and a second click on the unchanged button looked like nothing happened. diff --git a/packages/parity/e2e/date-time.spec.ts b/packages/parity/e2e/date-time.spec.ts index abd01c7..9f1cb8f 100644 --- a/packages/parity/e2e/date-time.spec.ts +++ b/packages/parity/e2e/date-time.spec.ts @@ -35,12 +35,22 @@ test.describe('DateRangePicker', () => { }); test('the calendar picks start, then end, and marks the days between', async ({ page }) => { + // Without a value the calendar opens on today's month – so the days are picked there + const now = new Date(); + const day = (date: number) => new Date(now.getFullYear(), now.getMonth(), date); + const label = (date: Date) => + new RegExp( + `^${date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}` + ); + const iso = (date: Date) => + `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; + const [start, end] = [day(7), day(9)]; await openStory(page, 'forms-daterangepicker--playground'); await page.getByRole('button', { name: 'Clear range' }).click(); await page.getByRole('button', { name: 'Open calendar' }).click(); - await page.getByRole('button', { name: /^Wednesday, October 7, 2026/ }).click(); - await page.getByRole('button', { name: /^Friday, October 9, 2026/ }).click(); - await expect(page.getByText('From: 2026-10-07 · To: 2026-10-09')).toBeVisible(); + await page.getByRole('button', { name: label(start) }).click(); + await page.getByRole('button', { name: label(end) }).click(); + await expect(page.getByText(`From: ${iso(start)} · To: ${iso(end)}`)).toBeVisible(); }); test('with time: start and end each get hour and minute', async ({ page }) => { diff --git a/packages/parity/e2e/visual/__screenshots__/react-navigation-stepper--vertical.png b/packages/parity/e2e/visual/__screenshots__/react-navigation-stepper--vertical.png index 76d9051..b6bb1a2 100644 Binary files a/packages/parity/e2e/visual/__screenshots__/react-navigation-stepper--vertical.png and b/packages/parity/e2e/visual/__screenshots__/react-navigation-stepper--vertical.png differ diff --git a/packages/parity/src/patterns.test.ts b/packages/parity/src/patterns.test.ts index 81c6187..df244d9 100644 --- a/packages/parity/src/patterns.test.ts +++ b/packages/parity/src/patterns.test.ts @@ -669,6 +669,38 @@ describe.each(frameworks)('%s', (framework) => { await userEvent.click(screen.getByRole('button', { name: 'Finish' })); await waitFor(() => expect(onComplete).toHaveBeenCalled()); }); + + it('"Next" shows a running check and ignores clicks meanwhile, keeping the focus', async () => { + let done: (valid: boolean) => void = () => {}; + const validate = vi.fn(() => new Promise((resolve) => (done = resolve))); + await render( + framework, + el('Wizard', { + steps: [{ label: 'Contact', validate }, { label: 'Address' }], + $scoped: { + step: { + react: 'renderStep', + args: (index: number) => ({ index }), + render: ({ index }: { index: number }) => el('p', {}, `Content ${index + 1}`), + }, + }, + }) + ); + const next = screen.getByRole('button', { name: 'Next' }); + expect(next).not.toHaveAttribute('aria-busy'); + await userEvent.click(next); + await waitFor(() => expect(next).toHaveAttribute('aria-busy', 'true')); + expect(next).toHaveAttribute('aria-disabled', 'true'); + expect(next).toHaveFocus(); + await userEvent.click(next); + expect(validate).toHaveBeenCalledTimes(1); + + done(true); + await waitFor(() => + expect(screen.getByRole('tab', { name: /Address/ })).toHaveAttribute('aria-selected', 'true') + ); + expect(screen.getByRole('button', { name: 'Finish' })).not.toHaveAttribute('aria-busy'); + }); }); describe('Persona, Statistic, AiLabel, ConfigProvider', () => { diff --git a/packages/react/src/components/DatePicker/DatePickerBase.tsx b/packages/react/src/components/DatePicker/DatePickerBase.tsx index 3e93f9d..27aa83a 100644 --- a/packages/react/src/components/DatePicker/DatePickerBase.tsx +++ b/packages/react/src/components/DatePicker/DatePickerBase.tsx @@ -675,6 +675,8 @@ function DayTable({ if (api.selectionMode !== 'single' || !api.getDayTableCellState({ value: day, visibleRange }).selected) return; event.preventDefault(); api.clearValue({ focus: false }); + // Clearing moves Ark's focused day to today – and the view with it to today's month + api.setFocusedValue(day); }; // On the cell, before Ark's own handlers (capture): own weekend days and deselecting. Nothing extra on the // default calendar – same DOM and handlers as before diff --git a/packages/react/src/components/Stepper/Stepper.tsx b/packages/react/src/components/Stepper/Stepper.tsx index 6244da4..7303a33 100644 --- a/packages/react/src/components/Stepper/Stepper.tsx +++ b/packages/react/src/components/Stepper/Stepper.tsx @@ -9,7 +9,10 @@ import { useRecipe } from '../RecipesProvider/RecipesProvider.js'; export interface StepperItem { label: string; description?: string; - /** The step is busy (e.g. saving): a spinner instead of its number, aria-busy on the step */ + /** + * The step is busy (e.g. saving): a spinner instead of its number, aria-busy on the step – and while it is the + * current one, "Next" shows the spinner too and ignores clicks + */ loading?: boolean; } @@ -121,9 +124,15 @@ export function Stepper({ - {/* With linear, an invalid step blocks going on – shown as a disabled button */} + {/* + With linear, an invalid step blocks going on – shown as a disabled button. While the + current step is busy, the button shows it and ignores clicks, but keeps the focus + */} - diff --git a/packages/vue/src/components/DatePicker/DatePickerDayTable.vue b/packages/vue/src/components/DatePicker/DatePickerDayTable.vue index 77d0f9f..14b9fcf 100644 --- a/packages/vue/src/components/DatePicker/DatePickerDayTable.vue +++ b/packages/vue/src/components/DatePicker/DatePickerDayTable.vue @@ -55,6 +55,8 @@ function deselect(event: Event, day: DateValue) { if (!canDeselect(day)) return; event.preventDefault(); api.value.clearValue({ focus: false }); + // Clearing moves Ark's focused day to today – and the view with it to today's month + api.value.setFocusedValue(day); } // On the cell, before Ark's own handlers (capture): own weekend days and deselecting. Nothing extra on the // default calendar – same DOM and handlers as before diff --git a/packages/vue/src/components/Stepper/Stepper.vue b/packages/vue/src/components/Stepper/Stepper.vue index d25cecc..fc867dc 100644 --- a/packages/vue/src/components/Stepper/Stepper.vue +++ b/packages/vue/src/components/Stepper/Stepper.vue @@ -12,7 +12,10 @@ const translations = useTranslations(); export interface StepperItem { label: string; description?: string; - /** The step is busy (e.g. saving): a spinner instead of its number, aria-busy on the step */ + /** + * The step is busy (e.g. saving): a spinner instead of its number, aria-busy on the step – and while it is the + * current one, "Next" shows the spinner too and ignores clicks + */ loading?: boolean; } @@ -104,9 +107,12 @@ const noCurrent = null as unknown as undefined; - + -