feat(accessibility): improve dose and reminder cards
This commit is contained in:
@@ -59,6 +59,13 @@ const statusStyles = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const statusLabels: Record<DoseStatus, string> = {
|
||||||
|
[DoseStatus.UPCOMING]: 'Upcoming dose',
|
||||||
|
[DoseStatus.TAKEN]: 'Dose taken',
|
||||||
|
[DoseStatus.MISSED]: 'Dose missed',
|
||||||
|
[DoseStatus.SNOOZED]: 'Dose snoozed',
|
||||||
|
};
|
||||||
|
|
||||||
const DoseCard: React.FC<DoseCardProps> = ({
|
const DoseCard: React.FC<DoseCardProps> = ({
|
||||||
dose,
|
dose,
|
||||||
medication,
|
medication,
|
||||||
@@ -68,6 +75,7 @@ const DoseCard: React.FC<DoseCardProps> = ({
|
|||||||
snoozedUntil,
|
snoozedUntil,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = statusStyles[status];
|
const styles = statusStyles[status];
|
||||||
|
const statusLabel = statusLabels[status];
|
||||||
const timeString = dose.scheduledTime.toLocaleTimeString([], {
|
const timeString = dose.scheduledTime.toLocaleTimeString([], {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
@@ -85,17 +93,48 @@ const DoseCard: React.FC<DoseCardProps> = ({
|
|||||||
})
|
})
|
||||||
: '';
|
: '';
|
||||||
const MedicationIcon = getMedicationIcon(medication.icon);
|
const MedicationIcon = getMedicationIcon(medication.icon);
|
||||||
|
const cardTitleId = `dose-${dose.id}-title`;
|
||||||
|
const statusId = `dose-${dose.id}-status`;
|
||||||
|
|
||||||
|
const handleCardKeyDown = (
|
||||||
|
event: React.KeyboardEvent<HTMLLIElement>
|
||||||
|
): void => {
|
||||||
|
if (event.target !== event.currentTarget) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
onToggleDose(dose.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(event.key.toLowerCase() === 's' || event.key === 'S') &&
|
||||||
|
status === DoseStatus.UPCOMING
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
onSnooze(dose.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className={`shadow-md rounded-lg p-4 flex flex-col justify-between transition-all duration-300 ${styles.bg} ${styles.ring} ring-4 ring-transparent border border-slate-200 dark:border-slate-700`}
|
role='group'
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={handleCardKeyDown}
|
||||||
|
aria-labelledby={cardTitleId}
|
||||||
|
aria-describedby={statusId}
|
||||||
|
className={`shadow-md rounded-lg p-4 flex flex-col justify-between transition-all duration-300 ${styles.bg} ${styles.ring} ring-4 ring-transparent border border-slate-200 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-400 dark:focus:ring-offset-slate-900`}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className='flex justify-between items-start'>
|
<div className='flex justify-between items-start'>
|
||||||
<div className='flex items-center space-x-3'>
|
<div className='flex items-center space-x-3'>
|
||||||
<MedicationIcon className='w-7 h-7 text-indigo-500 dark:text-indigo-400 flex-shrink-0' />
|
<MedicationIcon className='w-7 h-7 text-indigo-500 dark:text-indigo-400 flex-shrink-0' />
|
||||||
<div>
|
<div>
|
||||||
<h4 className='font-bold text-lg text-slate-800 dark:text-slate-100'>
|
<h4
|
||||||
|
id={cardTitleId}
|
||||||
|
className='font-bold text-lg text-slate-800 dark:text-slate-100'
|
||||||
|
>
|
||||||
{medication.name}
|
{medication.name}
|
||||||
</h4>
|
</h4>
|
||||||
<p className='text-slate-600 dark:text-slate-300'>
|
<p className='text-slate-600 dark:text-slate-300'>
|
||||||
@@ -106,9 +145,14 @@ const DoseCard: React.FC<DoseCardProps> = ({
|
|||||||
{styles.icon}
|
{styles.icon}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
id={statusId}
|
||||||
|
role='status'
|
||||||
|
aria-live='polite'
|
||||||
|
aria-atomic='true'
|
||||||
className={`flex items-center space-x-2 mt-4 font-semibold text-lg ${styles.text}`}
|
className={`flex items-center space-x-2 mt-4 font-semibold text-lg ${styles.text}`}
|
||||||
>
|
>
|
||||||
<ClockIcon className='w-5 h-5' />
|
<ClockIcon className='w-5 h-5' />
|
||||||
|
<span className='sr-only'>{statusLabel}</span>
|
||||||
<span>{timeString}</span>
|
<span>{timeString}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -12,15 +12,23 @@ const ReminderCard: React.FC<ReminderCardProps> = ({ reminder }) => {
|
|||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
});
|
});
|
||||||
const ReminderIcon = getReminderIcon(reminder.icon);
|
const ReminderIcon = getReminderIcon(reminder.icon);
|
||||||
|
const titleId = `reminder-${reminder.id}-title`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className='shadow-md rounded-lg p-4 flex flex-col justify-between transition-all duration-300 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700'>
|
<li
|
||||||
|
tabIndex={0}
|
||||||
|
aria-labelledby={titleId}
|
||||||
|
className='shadow-md rounded-lg p-4 flex flex-col justify-between transition-all duration-300 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-400 dark:focus:ring-offset-slate-900'
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className='flex justify-between items-start'>
|
<div className='flex justify-between items-start'>
|
||||||
<div className='flex items-center space-x-3'>
|
<div className='flex items-center space-x-3'>
|
||||||
<ReminderIcon className='w-7 h-7 text-sky-500 dark:text-sky-400 flex-shrink-0' />
|
<ReminderIcon className='w-7 h-7 text-sky-500 dark:text-sky-400 flex-shrink-0' />
|
||||||
<div>
|
<div>
|
||||||
<h4 className='font-bold text-lg text-slate-800 dark:text-slate-100'>
|
<h4
|
||||||
|
id={titleId}
|
||||||
|
className='font-bold text-lg text-slate-800 dark:text-slate-100'
|
||||||
|
>
|
||||||
{reminder.title}
|
{reminder.title}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user