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
7 changes: 7 additions & 0 deletions .changeset/calendar-deselect-month.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 8 additions & 0 deletions .changeset/wizard-next-loading.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 13 additions & 3 deletions packages/parity/e2e/date-time.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions packages/parity/src/patterns.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@
/** A component with shortcuts (useHotkeys) and a text field */
async function mountShortcuts(handlers: { save: () => void; help: () => void; inbox: () => void }) {
if (framework === 'vue') {
const Comp = defineComponent({

Check warning on line 163 in packages/parity/src/patterns.test.ts

View workflow job for this annotation

GitHub Actions / lint

There is more than one component in this file
setup() {
Vue.useHotkeys([
['mod+s', handlers.save, { description: 'Save', group: 'File' }],
Expand Down Expand Up @@ -349,7 +349,7 @@
const current = [...filters];
const view = {
vue: () => {
const Comp = defineComponent({

Check warning on line 352 in packages/parity/src/patterns.test.ts

View workflow job for this annotation

GitHub Actions / lint

There is more than one component in this file
data: () => ({ items: current }),
render() {
return h(Vue.FilterPanel, {
Expand Down Expand Up @@ -669,6 +669,38 @@
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<boolean>((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', () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/components/DatePicker/DatePickerBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 12 additions & 3 deletions packages/react/src/components/Stepper/Stepper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -121,9 +124,15 @@ export function Stepper({
<Steps.PrevTrigger asChild>
<Button variant="outline">{t.prev}</Button>
</Steps.PrevTrigger>
{/* 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
*/}
<Steps.NextTrigger asChild>
<Button disabled={linear && !api.isStepValid(api.value)}>
<Button
disabled={linear && !api.isStepValid(api.value)}
loading={items[api.value]?.loading}
>
{api.value === api.count - 1 ? (finishLabel ?? t.finish) : t.next}
</Button>
</Steps.NextTrigger>
Expand Down
2 changes: 2 additions & 0 deletions packages/vue/src/components/DatePicker/DatePickerDayTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 9 additions & 3 deletions packages/vue/src/components/Stepper/Stepper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -104,9 +107,12 @@ const noCurrent = null as unknown as undefined;
<Steps.PrevTrigger as-child>
<Button variant="outline">{{ t.prev }}</Button>
</Steps.PrevTrigger>
<!-- 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
-->
<Steps.NextTrigger as-child>
<Button :disabled="linear && !api.isStepValid(api.value)">{{
<Button :disabled="linear && !api.isStepValid(api.value)" :loading="items[api.value]?.loading">{{
api.value === api.count - 1 ? (finishLabel ?? t.finish) : t.next
}}</Button>
</Steps.NextTrigger>
Expand Down
Loading