nicolium: icon picker accessibility

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-02-21 16:40:18 +01:00
parent 4c04672ea5
commit 7d7444c2a5

View File

@ -1,5 +1,5 @@
import clsx from 'clsx';
import React from 'react';
import React, { useEffect } from 'react';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import Text from '@/components/ui/text';
@ -14,25 +14,72 @@ interface IIconPickerMenu {
style?: React.CSSProperties;
}
const IconPickerMenu: React.FC<IIconPickerMenu> = ({ icons, onPick, style }) => {
const IconPickerMenu: React.FC<IIconPickerMenu> = ({ icons, onPick }) => {
const intl = useIntl();
const containerNode = React.useRef<HTMLDivElement>(null);
const setRef = (c: HTMLDivElement) => {
if (!c) return;
// Nice and dirty hack to display the icons
c.querySelectorAll('button.emoji-mart-emoji > img').forEach((elem) => {
const newIcon = document.createElement('span');
newIcon.innerHTML = `<i class="fa fa-${(elem.parentNode as any).getAttribute('title')} fa-hack"></i>`;
(elem.parentNode as any).replaceChild(newIcon, elem);
});
};
useEffect(() => {
const firstButton = containerNode.current?.querySelector('button') as HTMLButtonElement;
firstButton?.focus();
}, [containerNode.current]);
const handleClick = (icon: string) => {
onPick(icon);
};
const renderIcon = (icon: string) => {
const handleKeyDown = (e: React.KeyboardEvent) => {
const target = e.target as HTMLButtonElement;
const icon = target.dataset.index;
const focus = (index: number) => {
if (index < 0) {
index = Object.values(icons).flat().length - 1;
} else if (index >= Object.values(icons).flat().length) {
index = 0;
}
const button = target.parentElement?.parentElement?.querySelector(
`button[data-index="${index}"]`,
) as HTMLButtonElement;
button?.focus();
e.preventDefault();
e.stopPropagation();
};
switch (e.key) {
case 'Enter':
if (icon) {
handleClick(icon);
e.preventDefault();
e.stopPropagation();
}
break;
case 'ArrowLeft':
focus(Number(target.dataset.index) - 1);
break;
case 'ArrowRight':
focus(Number(target.dataset.index) + 1);
break;
case 'ArrowUp':
focus(Number(target.dataset.index) - 8);
break;
case 'ArrowDown':
focus(Number(target.dataset.index) + 8);
break;
case 'Tab':
focus(Number(target.dataset.index) + (e.shiftKey ? -1 : 1));
break;
case 'Home':
focus(0);
break;
case 'End':
focus(Object.values(icons).flat().length - 1);
break;
default:
break;
}
};
const renderIcon = (icon: string, index: number) => {
const name = icon.replace('fa fa-', '');
return (
@ -44,6 +91,7 @@ const IconPickerMenu: React.FC<IIconPickerMenu> = ({ icons, onPick, style }) =>
onClick={() => {
handleClick(name);
}}
data-index={index}
>
<i className={clsx(icon, 'size-[1.375rem] text-lg leading-[1.15]')} />
</button>
@ -57,15 +105,15 @@ const IconPickerMenu: React.FC<IIconPickerMenu> = ({ icons, onPick, style }) =>
<div
className='h-[270px] overflow-x-hidden overflow-y-scroll rounded bg-white p-1.5 text-gray-900 dark:bg-primary-900 dark:text-gray-100'
aria-label={title}
ref={setRef}
ref={containerNode}
>
<Text className='px-1.5 py-1'>
<FormattedMessage id='icon_button.icons' defaultMessage='Icons' />
</Text>
<ul className='grid grid-cols-8'>
<ul className='grid grid-cols-8' onKeyDown={handleKeyDown}>
{Object.values(icons)
.flat()
.map((icon) => renderIcon(icon))}
.map((icon, index) => renderIcon(icon, index))}
</ul>
</div>
);