Use a 12-hour clock with AM/PM selects in the block modal

This commit is contained in:
NeilClack
2026-09-11 18:30:26 -05:00
parent 15bb51582b
commit e934a98f00
2 changed files with 74 additions and 13 deletions
+48 -13
View File
@@ -56,12 +56,15 @@
let step = $state<'details' | 'browse'>('details');
let startHour = $state('9');
let startMinute = $state('0');
let startPeriod = $state<'AM' | 'PM'>('AM');
let endHour = $state('10');
let endMinute = $state('0');
let endPeriod = $state<'AM' | 'PM'>('AM');
let selectedIds = $state<string[]>([]);
let error = $state('');
const hourOptions = Array.from({ length: 24 }, (_, i) => String(i));
const hourOptions = Array.from({ length: 12 }, (_, i) => String(i + 1));
const periodOptions: ('AM' | 'PM')[] = ['AM', 'PM'];
function minuteOptions(current: string): string[] {
const base = Array.from({ length: 60 / MINUTE_STEP }, (_, i) => String(i * MINUTE_STEP));
@@ -69,20 +72,38 @@
return [...base, current].sort((a, b) => Number(a) - Number(b));
}
function to12Hour(totalMinutes: number): { hour: number; minute: number; period: 'AM' | 'PM' } {
const hours24 = Math.floor(totalMinutes / 60) % 24;
return {
hour: hours24 % 12 === 0 ? 12 : hours24 % 12,
minute: totalMinutes % 60,
period: hours24 < 12 ? 'AM' : 'PM'
};
}
function to24Hour(hour12: string, minute: string, period: 'AM' | 'PM'): number {
const hours = (Number(hour12) % 12) + (period === 'PM' ? 12 : 0);
return (hours * 60 + Number(minute)) | 0;
}
// Re-seed the fields whenever the modal is (re)opened.
$effect(() => {
if (!open) return;
startHour = String(Math.floor(initialStartMinutes / 60));
startMinute = String(initialStartMinutes % 60);
endHour = String(Math.floor(initialEndMinutes / 60));
endMinute = String(initialEndMinutes % 60);
const start = to12Hour(initialStartMinutes);
startHour = String(start.hour);
startMinute = String(start.minute);
startPeriod = start.period;
const end = to12Hour(initialEndMinutes);
endHour = String(end.hour);
endMinute = String(end.minute);
endPeriod = end.period;
selectedIds = [...initialProclamationIds];
error = '';
step = 'details';
});
const startMinutes = $derived((Number(startHour) * 60 + Number(startMinute)) | 0);
const endMinutes = $derived((Number(endHour) * 60 + Number(endMinute)) | 0);
const startMinutes = $derived(to24Hour(startHour, startMinute, startPeriod));
const endMinutes = $derived(to24Hour(endHour, endMinute, endPeriod));
const overnight = $derived(
startMinutes !== endMinutes && isOvernight({ startMinutes, endMinutes })
);
@@ -213,9 +234,7 @@
class="rounded-md border border-slate-300 bg-white px-2 py-2 text-sm text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
>
{#each hourOptions as hour (hour)}
<option value={hour} selected={startHour === hour}>
{hour.padStart(2, '0')}
</option>
<option value={hour} selected={startHour === hour}>{hour}</option>
{/each}
</select>
<span class="text-slate-400">:</span>
@@ -230,6 +249,15 @@
</option>
{/each}
</select>
<select
aria-label="Start period"
onchange={(e) => (startPeriod = e.currentTarget.value as 'AM' | 'PM')}
class="rounded-md border border-slate-300 bg-white px-2 py-2 text-sm text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
>
{#each periodOptions as period (period)}
<option value={period} selected={startPeriod === period}>{period}</option>
{/each}
</select>
</div>
</div>
@@ -244,9 +272,7 @@
class="rounded-md border border-slate-300 bg-white px-2 py-2 text-sm text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
>
{#each hourOptions as hour (hour)}
<option value={hour} selected={endHour === hour}>
{hour.padStart(2, '0')}
</option>
<option value={hour} selected={endHour === hour}>{hour}</option>
{/each}
</select>
<span class="text-slate-400">:</span>
@@ -261,6 +287,15 @@
</option>
{/each}
</select>
<select
aria-label="End period"
onchange={(e) => (endPeriod = e.currentTarget.value as 'AM' | 'PM')}
class="rounded-md border border-slate-300 bg-white px-2 py-2 text-sm text-slate-900 shadow-sm focus:border-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
>
{#each periodOptions as period (period)}
<option value={period} selected={endPeriod === period}>{period}</option>
{/each}
</select>
</div>
</div>
@@ -67,6 +67,32 @@ describe('BlockModal', () => {
await expect.element(page.getByRole('combobox', { name: /^End minute$/ })).toHaveValue('15');
});
it('uses a 12-hour clock with an AM/PM select for each time', async () => {
mount({ initialStartMinutes: 21 * 60, initialEndMinutes: 0 });
await expect.element(page.getByRole('combobox', { name: /^Start hour$/ })).toHaveValue('9');
await expect.element(page.getByRole('combobox', { name: /^Start period$/ })).toHaveValue('PM');
await expect.element(page.getByRole('combobox', { name: /^End hour$/ })).toHaveValue('12');
await expect.element(page.getByRole('combobox', { name: /^End period$/ })).toHaveValue('AM');
// No 24-hour option exists any more.
await expect
.element(page.getByRole('combobox', { name: /^Start hour$/ }))
.not.toHaveValue('21');
});
it('recomputes the time when the AM/PM switch changes', async () => {
mount({ initialStartMinutes: 9 * 60, initialEndMinutes: 10 * 60 });
const period = (await page
.getByRole('combobox', { name: /^End period$/ })
.element()) as HTMLSelectElement;
period.value = 'PM';
period.dispatchEvent(new Event('change', { bubbles: true }));
await expect.element(page.getByText('9:00 AM – 10:00 PM')).toBeInTheDocument();
});
it('shows the review times and selected proclamation titles', async () => {
mount();