Use a 12-hour clock with AM/PM selects in the block modal
This commit is contained in:
@@ -56,12 +56,15 @@
|
|||||||
let step = $state<'details' | 'browse'>('details');
|
let step = $state<'details' | 'browse'>('details');
|
||||||
let startHour = $state('9');
|
let startHour = $state('9');
|
||||||
let startMinute = $state('0');
|
let startMinute = $state('0');
|
||||||
|
let startPeriod = $state<'AM' | 'PM'>('AM');
|
||||||
let endHour = $state('10');
|
let endHour = $state('10');
|
||||||
let endMinute = $state('0');
|
let endMinute = $state('0');
|
||||||
|
let endPeriod = $state<'AM' | 'PM'>('AM');
|
||||||
let selectedIds = $state<string[]>([]);
|
let selectedIds = $state<string[]>([]);
|
||||||
let error = $state('');
|
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[] {
|
function minuteOptions(current: string): string[] {
|
||||||
const base = Array.from({ length: 60 / MINUTE_STEP }, (_, i) => String(i * MINUTE_STEP));
|
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));
|
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.
|
// Re-seed the fields whenever the modal is (re)opened.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
startHour = String(Math.floor(initialStartMinutes / 60));
|
const start = to12Hour(initialStartMinutes);
|
||||||
startMinute = String(initialStartMinutes % 60);
|
startHour = String(start.hour);
|
||||||
endHour = String(Math.floor(initialEndMinutes / 60));
|
startMinute = String(start.minute);
|
||||||
endMinute = String(initialEndMinutes % 60);
|
startPeriod = start.period;
|
||||||
|
const end = to12Hour(initialEndMinutes);
|
||||||
|
endHour = String(end.hour);
|
||||||
|
endMinute = String(end.minute);
|
||||||
|
endPeriod = end.period;
|
||||||
selectedIds = [...initialProclamationIds];
|
selectedIds = [...initialProclamationIds];
|
||||||
error = '';
|
error = '';
|
||||||
step = 'details';
|
step = 'details';
|
||||||
});
|
});
|
||||||
|
|
||||||
const startMinutes = $derived((Number(startHour) * 60 + Number(startMinute)) | 0);
|
const startMinutes = $derived(to24Hour(startHour, startMinute, startPeriod));
|
||||||
const endMinutes = $derived((Number(endHour) * 60 + Number(endMinute)) | 0);
|
const endMinutes = $derived(to24Hour(endHour, endMinute, endPeriod));
|
||||||
const overnight = $derived(
|
const overnight = $derived(
|
||||||
startMinutes !== endMinutes && isOvernight({ startMinutes, endMinutes })
|
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"
|
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)}
|
{#each hourOptions as hour (hour)}
|
||||||
<option value={hour} selected={startHour === hour}>
|
<option value={hour} selected={startHour === hour}>{hour}</option>
|
||||||
{hour.padStart(2, '0')}
|
|
||||||
</option>
|
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
<span class="text-slate-400">:</span>
|
<span class="text-slate-400">:</span>
|
||||||
@@ -230,6 +249,15 @@
|
|||||||
</option>
|
</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</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>
|
||||||
</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"
|
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)}
|
{#each hourOptions as hour (hour)}
|
||||||
<option value={hour} selected={endHour === hour}>
|
<option value={hour} selected={endHour === hour}>{hour}</option>
|
||||||
{hour.padStart(2, '0')}
|
|
||||||
</option>
|
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
<span class="text-slate-400">:</span>
|
<span class="text-slate-400">:</span>
|
||||||
@@ -261,6 +287,15 @@
|
|||||||
</option>
|
</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,32 @@ describe('BlockModal', () => {
|
|||||||
await expect.element(page.getByRole('combobox', { name: /^End minute$/ })).toHaveValue('15');
|
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 () => {
|
it('shows the review times and selected proclamation titles', async () => {
|
||||||
mount();
|
mount();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user