Skip to content
Open
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
1 change: 0 additions & 1 deletion semcore/d3-chart/__tests__/bar-chart.browser-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,6 @@ test.describe(`${TAG.VISUAL}`, () => {
// Disable all items and verify chart appearance
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Space');

const box = await chart.boundingBox();
if (!box) throw new Error('Bounding box not found');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ test.describe(`${TAG.VISUAL}`, () => {
// Navigate with keyboard to verify focus state
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await expect(page).toHaveScreenshot();
});
});
Expand Down
3 changes: 1 addition & 2 deletions semcore/d3-chart/__tests__/d3-chart-base.browser-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,6 @@ test.describe(`${TAG.FUNCTIONAL}`, () => {
await test.step('Verify plot svg with aria-label attributes', async () => {
const svg = svgs.first();
const svgAttributes = [
['tabindex', '0'],
['aria-label', 'Last market trends with pattern'],
['width', '300'],
['height', '200'],
Expand All @@ -307,6 +306,7 @@ test.describe(`${TAG.FUNCTIONAL}`, () => {
await expect(dialog).not.toBeVisible();

await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
await expect(dialog).toBeVisible();

await expect(dialog).toHaveAttribute('tabindex', '0');
Expand Down Expand Up @@ -349,7 +349,6 @@ test.describe(`${TAG.FUNCTIONAL}`, () => {
await test.step('Verify plot svg without aria-label attributes', async () => {
const svg = svgs.nth(1);
const svgAttributes = [
['tabindex', '0'],
['aria-label', 'Chart'],
['data-ui-name', 'Plot'],
];
Expand Down
8 changes: 6 additions & 2 deletions semcore/d3-chart/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ describe('Focus skip to content after plot', () => {
const hints = makeDataHintsContainer();

const PlotComponent: React.FC = () => {
const triggerRef = React.useRef(null);
const plotRef = React.useRef<HTMLDivElement>(null);

return (
Expand All @@ -312,6 +313,8 @@ describe('Focus skip to content after plot', () => {
locale='en'
config={{}}
hints={hints}
triggerRef={triggerRef}
onCloseHandler={() => {}}
/>
</div>
<div className='one'>
Expand All @@ -331,7 +334,6 @@ describe('Focus skip to content after plot', () => {

const { getByTestId } = render(<PlotComponent />);

await userEvent.keyboard('[Tab]');
await userEvent.keyboard('[Tab]');
await userEvent.keyboard('[Tab]');
await userEvent.keyboard('[Enter]');
Expand All @@ -349,6 +351,7 @@ describe('Focus skip to content after plot', () => {
const hints = makeDataHintsContainer();

const PlotComponent: React.FC = () => {
const triggerRef = React.useRef(null);
const plotRef = React.useRef<HTMLDivElement>(null);

return (
Expand All @@ -362,6 +365,8 @@ describe('Focus skip to content after plot', () => {
locale='en'
config={{}}
hints={hints}
triggerRef={triggerRef}
onCloseHandler={() => {}}
/>
</div>
<div className='one'>
Expand All @@ -385,7 +390,6 @@ describe('Focus skip to content after plot', () => {

const { getByTestId } = render(<PlotComponent />);

await userEvent.keyboard('[Tab]');
await userEvent.keyboard('[Tab]');
await userEvent.keyboard('[Tab]');
await userEvent.keyboard('[Enter]');
Expand Down
1 change: 0 additions & 1 deletion semcore/d3-chart/src/Plot.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ class PlotRoot extends Component {
onMouseMove={this.handlerMouseMove}
onMouseLeave={this.handlerMouseLeave}
aria-label={ariaLabel}
tabIndex={0}
data-plot-id={this.plotId}
>
<Children />
Expand Down
104 changes: 28 additions & 76 deletions semcore/d3-chart/src/a11y/PlotA11yModule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,20 @@ import { Root, sstyled } from '@semcore/core';
import { Context as I18nContext, useI18n } from '@semcore/core/lib/utils/enhances/WithI18n';
import React from 'react';

import type { DataStructureHints, PartialDataSummarizationConfig } from './hints';
import { normalizeLocale } from './locale';
import type { A11yViewProps } from './PlotA11yView';
import styles from '../style/plotA11yModule.shadow.css';
import { localizedMessages } from './translations/module/__intergalactic-dynamic-locales';

let globalWasFocused = false;
let globalNavWithKeyboard = false;
type A11yModuleProps = Omit<A11yViewProps, 'onBlurHandler' | 'triggerRef'>;

export type A11yViewProps = {
id: string;
data: Record<string, unknown>[];
hints: DataStructureHints;
plotLabel: string;
locale: NavigatorLanguage['language'];
config: PartialDataSummarizationConfig;

plotRef: React.RefObject<HTMLElement>;
};

export function PlotA11yModule(props: A11yViewProps) {
export function PlotA11yModule(props: A11yModuleProps) {
const SPlotA11yModule = Root;
const [wasFocused, setWasFocused] = React.useState(globalWasFocused);
const [navWithKeyboard, setNavWithKeyboard] = React.useState(globalNavWithKeyboard);
const [isOpened, setIsOpened] = React.useState(false);
const [plotA11yView, setPlotA11yView] = React.useState<{
Component: React.FC<A11yViewProps>;
} | null>(null);

const hadnleHiddenElementsFocus = React.useCallback(() => {
setWasFocused(true);
setNavWithKeyboard(true);
}, []);
const srButtonRef = React.useRef<HTMLButtonElement>(null);

const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<Error | null>(null);
Expand All @@ -46,40 +29,7 @@ export function PlotA11yModule(props: A11yViewProps) {
const t = useI18n(localizedMessages, locale!);

React.useEffect(() => {
if (wasFocused) return;
const focusListener = () => {
globalWasFocused = true;
setWasFocused(true);
};

props.plotRef.current?.addEventListener('focus', focusListener);
return () => props.plotRef.current?.removeEventListener('focus', focusListener);
}, [wasFocused, props.plotRef]);
React.useEffect(() => {
if (navWithKeyboard) return;
const keyboardListener = (event: Event) => {
const navigationKeys = [
'Tab',
'ArrowUp',
'ArrowLeft',
'ArrowDown',
'ArrowRight',
'ArrowUp',
'ArrowLeft',
];
if ('key' in event && navigationKeys.includes((event as KeyboardEvent).key)) {
setNavWithKeyboard(true);
globalNavWithKeyboard = true;
}
};
document.body?.addEventListener('keydown', keyboardListener);
return () => document.body?.removeEventListener('keydown', keyboardListener);
}, [navWithKeyboard]);

const shouldDisplayView = wasFocused && navWithKeyboard;

React.useEffect(() => {
if (!shouldDisplayView) return;
if (!isOpened) return;
if (plotA11yView) return;
if (loading) return;

Expand All @@ -95,33 +45,35 @@ export function PlotA11yModule(props: A11yViewProps) {
// eslint-disable-next-line no-console
console.error(error);
setError(error);
setLoading(false);
});
}, [plotA11yView, shouldDisplayView, loading, setLoading]);
}, [plotA11yView, isOpened, loading, setLoading]);

if (plotA11yView) {
return sstyled(styles)(
<plotA11yView.Component {...props} locale={locale!} />,
) as React.ReactElement;
}

if (error) {
return sstyled(styles)(
<SPlotA11yModule render={Box} tabIndex={0} aria-live='assertive'>
{t('failed')}
</SPlotA11yModule>,
) as React.ReactElement;
}
if (loading) {
return sstyled(styles)(
<SPlotA11yModule render={Box} tabIndex={0} aria-live='polite'>
{t('loading')}
</SPlotA11yModule>,
<plotA11yView.Component
{...props}
onCloseHandler={() => {
setIsOpened(false);
setPlotA11yView(null);
}}
triggerRef={srButtonRef}
locale={locale!}
/>,
) as React.ReactElement;
}

return sstyled(styles)(
<SPlotA11yModule render={Box} tabIndex={0} onFocus={hadnleHiddenElementsFocus}>
{t('disabled')}
<SPlotA11yModule render={Box}>
<button
ref={srButtonRef}
onClick={() => setIsOpened(true)}
aria-label={t('PlotA11yModule.ScreenReaderOnlyButton.Label')}
/>
<Box role='status'>
{loading && t('loading')}
{error && t('failed')}
</Box>
</SPlotA11yModule>,
) as React.ReactElement;
);
}
46 changes: 44 additions & 2 deletions semcore/d3-chart/src/a11y/PlotA11yView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,35 @@ import React from 'react';
import { DataAccessibilityTable } from './DataAccessibilityTable';
import { heavyFindNextFocusableElement } from './focus';
import { makeDataSummarizationConfig } from './hints';
import type { DataStructureHints, PartialDataSummarizationConfig } from './hints';
import { getIntl } from './intl';
import type { A11yViewProps } from './PlotA11yModule';
import { summarize } from './summarize';
import styles from '../style/plotA11yView.shadow.css';
import { localizedMessages } from './translations/view/__intergalactic-dynamic-locales';

export type A11yViewProps = {
id: string;
data: Record<string, unknown>[];
hints: DataStructureHints;
plotLabel: string;
locale: NavigatorLanguage['language'];
config: PartialDataSummarizationConfig;

plotRef: React.RefObject<HTMLElement>;
triggerRef: React.RefObject<HTMLElement>;
onCloseHandler: () => void;
};

export const PlotA11yView: React.FC<A11yViewProps> = ({
id,
data: providedData,
hints,
plotLabel,
triggerRef,
plotRef,
config: providedConfig,
locale,
onCloseHandler,
}) => {
const SPlotA11yView = Root;
const translations = useAsyncI18nMessages(localizedMessages, locale);
Expand All @@ -32,10 +47,30 @@ export const PlotA11yView: React.FC<A11yViewProps> = ({
() => (Array.isArray(providedData) ? providedData : [providedData]),
[providedData],
);
const rootRef = React.useRef<HTMLDivElement>(null);

const [summary, setSummary] = React.useState<string | null>(null);
const [generatingSummary, setGeneratingSummary] = React.useState(true);

React.useEffect(() => {
rootRef.current?.focus();

function focusOutHandler(event: FocusEvent) {
if (event.relatedTarget === null) {
return requestIdleCallback(onCloseHandler);
}

if (!(event.relatedTarget instanceof HTMLElement)) return;

if (rootRef.current?.contains(event.relatedTarget)) return;

requestIdleCallback(onCloseHandler);
}

rootRef.current?.addEventListener('focusout', focusOutHandler);
return () => rootRef.current?.removeEventListener('focusout', focusOutHandler);
}, []);

React.useEffect(() => {
if (config.disable) {
setSummary('');
Expand All @@ -54,13 +89,19 @@ export const PlotA11yView: React.FC<A11yViewProps> = ({
}, [providedData, hints, config, locale, translations, localizedMessages]);

const handleClose = React.useCallback(() => {
plotRef.current?.focus();
onCloseHandler();

requestIdleCallback(() => {
triggerRef.current?.focus();
});
}, []);
const handleSkip = React.useCallback((event: React.SyntheticEvent) => {
event.preventDefault();
if (!plotRef.current) return;

heavyFindNextFocusableElement(plotRef.current)?.focus();

onCloseHandler();
}, []);
const handleSkipKeyboard = React.useCallback(
(event: React.KeyboardEvent) => {
Expand Down Expand Up @@ -108,6 +149,7 @@ export const PlotA11yView: React.FC<A11yViewProps> = ({
aria-label={texts.label}
role='dialog'
__excludeProps={['data']}
ref={rootRef}
>
<button type='button' onClick={handleClose}>
{texts.close}
Expand Down
1 change: 0 additions & 1 deletion semcore/d3-chart/src/a11y/translations/module/de.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"disabled": "Drücken Sie die Tabulatortaste, um das Zugriffsmodul für grafische Diagramme zu aktivieren.",
"loading": "Lade Zugriffsmodul.",
"failed": "Zugriffsmodul konnte nicht geladen werden. Kontaktieren Sie den Support und helfen Sie uns, das Problem schneller zu beheben."
}
4 changes: 2 additions & 2 deletions semcore/d3-chart/src/a11y/translations/module/en.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"disabled": "Press \"Tab\" to enable graphical charts accessibility module.",
"loading": "Loading accessibility module.",
"failed": "Failed to load accessibility module. Contact service user support to help us resolve issue faster."
"failed": "Failed to load accessibility module. Contact service user support to help us resolve issue faster.",
"PlotA11yModule.ScreenReaderOnlyButton.Label": "Open data summary"
}
1 change: 0 additions & 1 deletion semcore/d3-chart/src/a11y/translations/module/es.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"disabled": "Pulse \"Tab\" para activar el módulo de accesibilidad a los gráficos.",
"loading": "Cargando módulo de accesibilidad.",
"failed": "No se ha podido cargar el módulo de accesibilidad. Contacta con el servicio de asistencia al usuario para ayudarnos a resolver el problema rápidamente."
}
1 change: 0 additions & 1 deletion semcore/d3-chart/src/a11y/translations/module/fr.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"disabled": "Appuyez sur « Onglet » pour activer le module d’accessibilité aux diagrammes.",
"loading": "Chargement du module d’accessibilité en cours.",
"failed": "Impossible de charger le module d’accessibilité. Contactez le service d’assistance aux utilisateurs pour nous permettre de résoudre le problème plus rapidement."
}
1 change: 0 additions & 1 deletion semcore/d3-chart/src/a11y/translations/module/it.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"disabled": "Premi \"Tab\" per abilitare il modulo di accessibilità dei dati per i grafici.",
"loading": "Caricamento del modulo di accessibilità in corso.",
"failed": "Caricamento del modulo di accessibilità non riuscito. Contatta il servizio di assistenza utenti per risolvere il problema più in fretta."
}
1 change: 0 additions & 1 deletion semcore/d3-chart/src/a11y/translations/module/ja.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"disabled": "アクセシビリティモジュールのグラフィカルチャートを有効化するには「Tab」キーを押してください。",
"loading": "アクセシビリティモジュールのロード中。",
"failed": "アクセシビリティモジュールの読み込みに失敗しました。サービスユーザーサポートに連絡し、問題を迅速に解決します。"
}
1 change: 0 additions & 1 deletion semcore/d3-chart/src/a11y/translations/module/ko.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"disabled": "'탭'을 누르면 그래픽 차트 접근성 모듈이 활성화됩니다.",
"loading": "접근성 모듈 로딩 중.",
"failed": "접근성 모듈을 불러오지 못했습니다. 사용자 지원 서비스 팀에 문의하시면 문제를 더 빨리 해결하실 수 있습니다."
}
1 change: 0 additions & 1 deletion semcore/d3-chart/src/a11y/translations/module/nl.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"disabled": "Druk op Tab om de toegankelijkheidsmodule voor grafieken in te schakelen.",
"loading": "De toegankelijkheidsmodule wordt geladen.",
"failed": "De toegankelijkheidsmodule kan niet worden geladen. Neem contact op met ons supportteam zodat we het probleem sneller kunnen oplossen."
}
1 change: 0 additions & 1 deletion semcore/d3-chart/src/a11y/translations/module/pl.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"disabled": "Naciśnij Tab, aby włączyć moduł dostępności wykresów graficznych.",
"loading": "Ładowanie modułu dostępności.",
"failed": "Nie udało się załadować modułu dostępności. Skontaktuj się ze wsparciem użytkowników, aby pomóc nam w szybszym rozwiązaniu problemu."
}
1 change: 0 additions & 1 deletion semcore/d3-chart/src/a11y/translations/module/pt.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"disabled": "Pressione \"Tab\" para habilitar o módulo de acessibilidade a gráficos.",
"loading": "Carregando módulo de acessibilidade.",
"failed": "Falha ao carregar o módulo de acessibilidade. Entre em contato com o suporte ao usuário do serviço para nos ajudar a resolver o problema com mais rapidez."
}
Loading
Loading