mirror of
https://github.com/suitenumerique/docs.git
synced 2026-05-06 07:02:03 +02:00
Compare commits
2 Commits
websocket/
...
fix/1136-h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
012b06f3b1 | ||
|
|
0c8bf4013a |
@@ -23,7 +23,9 @@ and this project adheres to
|
||||
- ♻️(frontend) redirect to doc after duplicate #1175
|
||||
- 🔧(project) change env.d system by using local files #1200
|
||||
- ⚡️(frontend) improve tree stability #1207
|
||||
- ⚡️(frontend) improve accessibility #1232
|
||||
- ⚡️(frontend) improve accessibility
|
||||
- #1232
|
||||
- #1251
|
||||
- 🛂(frontend) block drag n drop when not desktop #1239
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('Doc Editor - Heading Accessibility', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test('should filter heading options progressively (h1 -> h2 -> h3)', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.getByRole('button', { name: 'Nouveau doc' }).click();
|
||||
|
||||
await page.waitForURL('**/docs/**', {
|
||||
timeout: 10000,
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
const input = page.getByLabel('doc title input');
|
||||
await input.fill('heading-accessibility-test');
|
||||
await input.blur();
|
||||
|
||||
const editor = page.locator('.ProseMirror');
|
||||
await editor.click();
|
||||
|
||||
await page.keyboard.type('/');
|
||||
await expect(page.getByText('Titre 1')).toBeVisible();
|
||||
await expect(page.getByText('Titre 2')).toBeHidden();
|
||||
await expect(page.getByText('Titre 3')).toBeHidden();
|
||||
|
||||
await page.getByText('Titre 1').click();
|
||||
await page.keyboard.type('Main Title');
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await editor.click();
|
||||
await page.keyboard.type('/');
|
||||
|
||||
await expect(page.getByText('Titre 1')).toBeHidden();
|
||||
await expect(page.getByText('Titre 2')).toBeVisible();
|
||||
await expect(page.getByText('Titre 3')).toBeHidden();
|
||||
|
||||
await page.getByText('Titre 2').click();
|
||||
await page.keyboard.type('Sub Title');
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await editor.click();
|
||||
await page.keyboard.type('/');
|
||||
|
||||
await expect(page.getByText('Titre 1')).toBeHidden();
|
||||
await expect(page.getByText('Titre 2')).toBeVisible();
|
||||
await expect(page.getByText('Titre 3')).toBeVisible();
|
||||
|
||||
await page.getByText('Titre 3').click();
|
||||
await page.keyboard.type('Sub Sub Title');
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await editor.click();
|
||||
await page.keyboard.type('/');
|
||||
|
||||
await expect(page.getByText('Titre 1')).toBeHidden();
|
||||
await expect(page.getByText('Titre 2')).toBeHidden();
|
||||
await expect(page.getByText('Titre 3')).toBeVisible();
|
||||
|
||||
await page.getByText('Titre 3').click();
|
||||
await page.keyboard.type('Another Sub Sub Title');
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await editor.click();
|
||||
await page.keyboard.type('/');
|
||||
|
||||
await expect(page.getByText('Titre 1')).toBeHidden();
|
||||
await expect(page.getByText('Titre 2')).toBeHidden();
|
||||
await expect(page.getByText('Titre 3')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useHeadingAccessibilityFilter } from '../hook';
|
||||
import {
|
||||
DocsBlockSchema,
|
||||
DocsInlineContentSchema,
|
||||
@@ -34,6 +35,7 @@ export const BlockNoteSuggestionMenu = () => {
|
||||
const { t } = useTranslation();
|
||||
const basicBlocksName = useDictionary().slash_menu.page_break.group;
|
||||
const getInterlinkingMenuItems = useGetInterlinkingMenuItems();
|
||||
const { filterHeadingItemsByAccessibility } = useHeadingAccessibilityFilter();
|
||||
|
||||
const getSlashMenuItems = useMemo(() => {
|
||||
// We insert it after the "Code Block" item to have the interlinking block displayed after the basic blocks
|
||||
@@ -47,11 +49,16 @@ export const BlockNoteSuggestionMenu = () => {
|
||||
...defaultMenu.slice(index + 1),
|
||||
];
|
||||
|
||||
const filteredMenuItems = filterHeadingItemsByAccessibility(
|
||||
newSlashMenuItems,
|
||||
editor,
|
||||
);
|
||||
|
||||
return async (query: string) =>
|
||||
Promise.resolve(
|
||||
filterSuggestionItems(
|
||||
combineByGroup(
|
||||
newSlashMenuItems,
|
||||
filteredMenuItems,
|
||||
getCalloutReactSlashMenuItems(editor, t, basicBlocksName),
|
||||
getMultiColumnSlashMenuItems?.(editor) || [],
|
||||
getPageBreakReactSlashMenuItems(editor),
|
||||
@@ -60,7 +67,13 @@ export const BlockNoteSuggestionMenu = () => {
|
||||
query,
|
||||
),
|
||||
);
|
||||
}, [basicBlocksName, editor, getInterlinkingMenuItems, t]);
|
||||
}, [
|
||||
basicBlocksName,
|
||||
editor,
|
||||
getInterlinkingMenuItems,
|
||||
t,
|
||||
filterHeadingItemsByAccessibility,
|
||||
]);
|
||||
|
||||
return (
|
||||
<SuggestionMenuController
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './useHeadings';
|
||||
export * from './useSaveDoc';
|
||||
export * from './useShortcuts';
|
||||
export * from './useUploadFile';
|
||||
export * from './useHeadingAccessibilityFilter';
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { getDefaultReactSlashMenuItems } from '@blocknote/react';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../types';
|
||||
|
||||
export const useHeadingAccessibilityFilter = () => {
|
||||
// function to extract heading level from menu item
|
||||
const getHeadingLevel = (
|
||||
item: ReturnType<typeof getDefaultReactSlashMenuItems>[0],
|
||||
): number => {
|
||||
const title = item.title?.toLowerCase() || '';
|
||||
const aliases = item.aliases || [];
|
||||
const HEADING_2 = 'heading 2';
|
||||
const HEADING_3 = 'heading 3';
|
||||
const TITLE_2 = 'titre 2';
|
||||
const TITLE_3 = 'titre 3';
|
||||
|
||||
if (
|
||||
title.includes(HEADING_2) ||
|
||||
title.includes(TITLE_2) ||
|
||||
aliases.some(
|
||||
(alias: string) => alias.includes(HEADING_2) || alias.includes(TITLE_2),
|
||||
)
|
||||
) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (
|
||||
title.includes(HEADING_3) ||
|
||||
title.includes(TITLE_3) ||
|
||||
aliases.some(
|
||||
(alias: string) => alias.includes(HEADING_3) || alias.includes(TITLE_3),
|
||||
)
|
||||
) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
return 1;
|
||||
};
|
||||
|
||||
// function to check if item is a heading
|
||||
const isHeadingItem = (
|
||||
item: ReturnType<typeof getDefaultReactSlashMenuItems>[0],
|
||||
): boolean => {
|
||||
return item.onItemClick?.toString().includes('heading');
|
||||
};
|
||||
|
||||
const filterHeadingItemsByAccessibility = (
|
||||
items: ReturnType<typeof getDefaultReactSlashMenuItems>,
|
||||
editor: DocsBlockNoteEditor,
|
||||
) => {
|
||||
const existingLevels = editor.document
|
||||
.filter((block) => block.type === 'heading')
|
||||
.map((block) => (block.props as { level: number }).level);
|
||||
|
||||
const hasH1 = existingLevels.includes(1);
|
||||
|
||||
if (existingLevels.length === 0) {
|
||||
return items.filter(
|
||||
(item) => !isHeadingItem(item) || getHeadingLevel(item) === 1,
|
||||
);
|
||||
}
|
||||
|
||||
const maxLevel = Math.max(...existingLevels);
|
||||
const minLevel = Math.min(...existingLevels);
|
||||
|
||||
return items.filter((item) => {
|
||||
if (!isHeadingItem(item)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const headingLevel = getHeadingLevel(item);
|
||||
|
||||
// Never allow h1 if one already exists >> accessibility tells that we can only have one h1 per document
|
||||
if (headingLevel === 1 && hasH1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
headingLevel === maxLevel ||
|
||||
headingLevel === maxLevel + 1 ||
|
||||
(headingLevel === minLevel - 1 && minLevel > 1)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return { filterHeadingItemsByAccessibility };
|
||||
};
|
||||
Reference in New Issue
Block a user