✨(frontend) enable enter key to open documents and subdocuments
added keyboard support to open docs and subdocs using the enter key Signed-off-by: Cyril <c.gromoff@gmail.com>
This commit is contained in:
@@ -8,9 +8,14 @@ and this project adheres to
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- ♿(frontend) improve accessibility:
|
||||||
|
- #1354
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- 🐛(backend) duplicate sub docs as root for reader user
|
- 🐛(backend) duplicate sub docs as root for reader users
|
||||||
|
|
||||||
## [3.7.0] - 2025-09-12
|
## [3.7.0] - 2025-09-12
|
||||||
|
|
||||||
|
|||||||
@@ -252,6 +252,46 @@ test.describe('Doc Tree', () => {
|
|||||||
page.getByRole('menuitem', { name: 'Move to my docs' }),
|
page.getByRole('menuitem', { name: 'Move to my docs' }),
|
||||||
).toHaveAttribute('aria-disabled', 'true');
|
).toHaveAttribute('aria-disabled', 'true');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('keyboard navigation with Enter key opens documents', async ({
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
}) => {
|
||||||
|
// Create a parent document
|
||||||
|
const [docParent] = await createDoc(
|
||||||
|
page,
|
||||||
|
'doc-tree-keyboard-nav',
|
||||||
|
browserName,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
await verifyDocName(page, docParent);
|
||||||
|
|
||||||
|
// Create a sub-document
|
||||||
|
const { name: docChild } = await createRootSubPage(
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
'doc-tree-keyboard-child',
|
||||||
|
);
|
||||||
|
|
||||||
|
const docTree = page.getByTestId('doc-tree');
|
||||||
|
await expect(docTree).toBeVisible();
|
||||||
|
|
||||||
|
// Test keyboard navigation on root document
|
||||||
|
const rootItem = page.getByTestId('doc-tree-root-item');
|
||||||
|
await expect(rootItem).toBeVisible();
|
||||||
|
|
||||||
|
// Focus on the root item and press Enter
|
||||||
|
await rootItem.focus();
|
||||||
|
await expect(rootItem).toBeFocused();
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
|
||||||
|
// Verify we navigated to the root document
|
||||||
|
await verifyDocName(page, docParent);
|
||||||
|
await expect(page).toHaveURL(/\/docs\/[^/]+\/?$/);
|
||||||
|
|
||||||
|
// Now test keyboard navigation on sub-document
|
||||||
|
await expect(docTree.getByText(docChild)).toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Doc Tree: Inheritance', () => {
|
test.describe('Doc Tree: Inheritance', () => {
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ export const SimpleDocItem = ({
|
|||||||
$overflow="auto"
|
$overflow="auto"
|
||||||
$width="100%"
|
$width="100%"
|
||||||
className="--docs--simple-doc-item"
|
className="--docs--simple-doc-item"
|
||||||
|
role="presentation"
|
||||||
|
aria-label={`${t('Open document {{title}}', { title: doc.title || untitledDocument })}`}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
$direction="row"
|
$direction="row"
|
||||||
@@ -59,6 +61,7 @@ export const SimpleDocItem = ({
|
|||||||
`}
|
`}
|
||||||
$padding={`${spacingsTokens['3xs']} 0`}
|
$padding={`${spacingsTokens['3xs']} 0`}
|
||||||
data-testid={isPinned ? `doc-pinned-${doc.id}` : undefined}
|
data-testid={isPinned ? `doc-pinned-${doc.id}` : undefined}
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
{isPinned ? (
|
{isPinned ? (
|
||||||
<PinnedDocumentIcon
|
<PinnedDocumentIcon
|
||||||
@@ -96,6 +99,7 @@ export const SimpleDocItem = ({
|
|||||||
$align="center"
|
$align="center"
|
||||||
$gap={spacingsTokens['3xs']}
|
$gap={spacingsTokens['3xs']}
|
||||||
$margin={{ top: '-2px' }}
|
$margin={{ top: '-2px' }}
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<Text $variation="600" $size="xs">
|
<Text $variation="600" $size="xs">
|
||||||
{DateTime.fromISO(doc.updated_at).toRelative()}
|
{DateTime.fromISO(doc.updated_at).toRelative()}
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import {
|
|||||||
} from '@gouvfr-lasuite/ui-kit';
|
} from '@gouvfr-lasuite/ui-kit';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
import { Box, Icon, Text } from '@/components';
|
import { Box, BoxButton, Icon, Text } from '@/components';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
import {
|
import {
|
||||||
Doc,
|
Doc,
|
||||||
@@ -20,6 +21,7 @@ import { useResponsiveStore } from '@/stores';
|
|||||||
|
|
||||||
import SubPageIcon from './../assets/sub-page-logo.svg';
|
import SubPageIcon from './../assets/sub-page-logo.svg';
|
||||||
import { DocTreeItemActions } from './DocTreeItemActions';
|
import { DocTreeItemActions } from './DocTreeItemActions';
|
||||||
|
import { useKeyboardActivation } from '../hooks/useKeyboardActivation';
|
||||||
|
|
||||||
const ItemTextCss = css`
|
const ItemTextCss = css`
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -38,7 +40,11 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
|
|||||||
const { node } = props;
|
const { node } = props;
|
||||||
const { spacingsTokens } = useCunninghamTheme();
|
const { spacingsTokens } = useCunninghamTheme();
|
||||||
const { isDesktop } = useResponsiveStore();
|
const { isDesktop } = useResponsiveStore();
|
||||||
const [actionsOpen, setActionsOpen] = useState(false);
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
const isSelectedNow = treeContext?.treeData.selectedNode?.id === doc.id;
|
||||||
|
const isActive = node.isFocused || menuOpen || isSelectedNow;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { togglePanel } = useLeftPanelStore();
|
const { togglePanel } = useLeftPanelStore();
|
||||||
@@ -46,6 +52,11 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
|
|||||||
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(doc.title || '');
|
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(doc.title || '');
|
||||||
const displayTitle = titleWithoutEmoji || untitledDocument;
|
const displayTitle = titleWithoutEmoji || untitledDocument;
|
||||||
|
|
||||||
|
const handleActivate = () => {
|
||||||
|
treeContext?.treeData.setSelectedNode(doc);
|
||||||
|
router.push(`/docs/${doc.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
const afterCreate = (createdDoc: Doc) => {
|
const afterCreate = (createdDoc: Doc) => {
|
||||||
const actualChildren = node.data.children ?? [];
|
const actualChildren = node.data.children ?? [];
|
||||||
|
|
||||||
@@ -76,62 +87,75 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useKeyboardActivation(
|
||||||
|
['Enter'],
|
||||||
|
isActive && !menuOpen,
|
||||||
|
handleActivate,
|
||||||
|
true,
|
||||||
|
'.c__tree-view',
|
||||||
|
);
|
||||||
|
|
||||||
|
const docTitle = doc.title || untitledDocument;
|
||||||
|
const hasChildren = (doc.children?.length || 0) > 0;
|
||||||
|
const isExpanded = node.isOpen;
|
||||||
|
const isSelected = isSelectedNow;
|
||||||
|
const ariaLabel = docTitle;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className="--docs-sub-page-item"
|
className="--docs-sub-page-item"
|
||||||
draggable={doc.abilities.move && isDesktop}
|
draggable={doc.abilities.move && isDesktop}
|
||||||
$position="relative"
|
$position="relative"
|
||||||
|
role="treeitem"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
aria-selected={isSelected}
|
||||||
|
aria-expanded={hasChildren ? isExpanded : undefined}
|
||||||
$css={css`
|
$css={css`
|
||||||
background-color: ${actionsOpen
|
background-color: ${menuOpen
|
||||||
? 'var(--c--theme--colors--greyscale-100)'
|
? 'var(--c--theme--colors--greyscale-100)'
|
||||||
: 'var(--c--theme--colors--greyscale-000)'};
|
: 'var(--c--theme--colors--greyscale-000)'};
|
||||||
|
|
||||||
.light-doc-item-actions {
|
.light-doc-item-actions {
|
||||||
display: ${actionsOpen || !isDesktop ? 'flex' : 'none'};
|
display: ${menuOpen || !isDesktop ? 'flex' : 'none'};
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
background: ${isDesktop
|
background: ${isDesktop
|
||||||
? 'var(--c--theme--colors--greyscale-100)'
|
? 'var(--c--theme--colors--greyscale-100)'
|
||||||
: 'var(--c--theme--colors--greyscale-000)'};
|
: 'var(--c--theme--colors--greyscale-000)'};
|
||||||
}
|
}
|
||||||
|
|
||||||
.c__tree-view--node.isSelected {
|
.c__tree-view--node.isSelected {
|
||||||
.light-doc-item-actions {
|
.light-doc-item-actions {
|
||||||
background: var(--c--theme--colors--greyscale-100);
|
background: var(--c--theme--colors--greyscale-100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--c--theme--colors--greyscale-100);
|
background-color: var(--c--theme--colors--greyscale-100);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
.light-doc-item-actions {
|
.light-doc-item-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: var(--c--theme--colors--greyscale-100);
|
background: var(--c--theme--colors--greyscale-100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.row.preview & {
|
.row.preview & {
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<TreeViewItem
|
<TreeViewItem {...props} onClick={handleActivate}>
|
||||||
{...props}
|
<BoxButton
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
treeContext?.treeData.setSelectedNode(props.node.data.value as Doc);
|
e.stopPropagation();
|
||||||
router.push(`/docs/${props.node.data.value.id}`);
|
handleActivate();
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<Box
|
|
||||||
data-testid={`doc-sub-page-item-${props.node.data.value.id}`}
|
|
||||||
$width="100%"
|
$width="100%"
|
||||||
$direction="row"
|
$direction="row"
|
||||||
$gap={spacingsTokens['xs']}
|
$gap={spacingsTokens['xs']}
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
$align="center"
|
$align="center"
|
||||||
$minHeight="24px"
|
$minHeight="24px"
|
||||||
|
data-testid={`doc-sub-page-item-${doc.id}`}
|
||||||
|
aria-label={`${t('Open document {{title}}', { title: docTitle })}`}
|
||||||
|
$css={css`
|
||||||
|
text-align: left;
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
<Box $width="16px" $height="16px">
|
<Box $width="16px" $height="16px">
|
||||||
<DocIcon emoji={emoji} defaultIcon={<SubPageIcon />} $size="sm" />
|
<DocIcon emoji={emoji} defaultIcon={<SubPageIcon />} $size="sm" />
|
||||||
@@ -157,23 +181,25 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
|
|||||||
iconName="group"
|
iconName="group"
|
||||||
$size="16px"
|
$size="16px"
|
||||||
$variation="400"
|
$variation="400"
|
||||||
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
</BoxButton>
|
||||||
<Box
|
<Box
|
||||||
$direction="row"
|
$direction="row"
|
||||||
$align="center"
|
$align="center"
|
||||||
className="light-doc-item-actions"
|
className="light-doc-item-actions"
|
||||||
>
|
role="toolbar"
|
||||||
<DocTreeItemActions
|
aria-label={`${t('Actions for {{title}}', { title: docTitle })}`}
|
||||||
doc={doc}
|
>
|
||||||
isOpen={actionsOpen}
|
<DocTreeItemActions
|
||||||
onOpenChange={setActionsOpen}
|
doc={doc}
|
||||||
parentId={node.data.parentKey}
|
isOpen={menuOpen}
|
||||||
onCreateSuccess={afterCreate}
|
onOpenChange={setMenuOpen}
|
||||||
/>
|
parentId={node.data.parentKey}
|
||||||
</Box>
|
onCreateSuccess={afterCreate}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</TreeViewItem>
|
</TreeViewItem>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from '@gouvfr-lasuite/ui-kit';
|
} from '@gouvfr-lasuite/ui-kit';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
import { Box, StyledLink } from '@/components';
|
import { Box, StyledLink } from '@/components';
|
||||||
@@ -26,11 +27,16 @@ type DocTreeProps = {
|
|||||||
|
|
||||||
export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
||||||
const { spacingsTokens } = useCunninghamTheme();
|
const { spacingsTokens } = useCunninghamTheme();
|
||||||
const [rootActionsOpen, setRootActionsOpen] = useState(false);
|
|
||||||
const treeContext = useTreeContext<Doc | null>();
|
|
||||||
const router = useRouter();
|
|
||||||
const { isDesktop } = useResponsive();
|
const { isDesktop } = useResponsive();
|
||||||
const [treeRoot, setTreeRoot] = useState<HTMLElement | null>(null);
|
const [treeRoot, setTreeRoot] = useState<HTMLElement | null>(null);
|
||||||
|
const treeContext = useTreeContext<Doc | null>();
|
||||||
|
const router = useRouter();
|
||||||
|
const [rootActionsOpen, setRootActionsOpen] = useState(false);
|
||||||
|
const rootIsSelected =
|
||||||
|
!!treeContext?.root?.id &&
|
||||||
|
treeContext?.treeData.selectedNode?.id === treeContext.root.id;
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [initialOpenState, setInitialOpenState] = useState<OpenMap | undefined>(
|
const [initialOpenState, setInitialOpenState] = useState<OpenMap | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
@@ -39,9 +45,7 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
|||||||
const { mutate: moveDoc } = useMoveDoc();
|
const { mutate: moveDoc } = useMoveDoc();
|
||||||
|
|
||||||
const { data: tree, isFetching } = useDocTree(
|
const { data: tree, isFetching } = useDocTree(
|
||||||
{
|
{ docId: currentDoc.id },
|
||||||
docId: currentDoc.id,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
enabled: !treeContext?.root?.id,
|
enabled: !treeContext?.root?.id,
|
||||||
queryKey: [KEY_DOC_TREE, { id: currentDoc.id }],
|
queryKey: [KEY_DOC_TREE, { id: currentDoc.id }],
|
||||||
@@ -56,7 +60,6 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
|||||||
});
|
});
|
||||||
treeContext?.treeData.handleMove(result);
|
treeContext?.treeData.handleMove(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function resets the tree states.
|
* This function resets the tree states.
|
||||||
*/
|
*/
|
||||||
@@ -64,11 +67,39 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
|||||||
if (!treeContext?.root?.id) {
|
if (!treeContext?.root?.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
treeContext?.setRoot(null);
|
treeContext?.setRoot(null);
|
||||||
setInitialOpenState(undefined);
|
setInitialOpenState(undefined);
|
||||||
}, [treeContext]);
|
}, [treeContext]);
|
||||||
|
|
||||||
|
const selectRoot = useCallback(() => {
|
||||||
|
if (treeContext?.root) {
|
||||||
|
treeContext.treeData.setSelectedNode(treeContext.root);
|
||||||
|
}
|
||||||
|
}, [treeContext]);
|
||||||
|
|
||||||
|
const navigateToRoot = useCallback(() => {
|
||||||
|
const id = treeContext?.root?.id;
|
||||||
|
if (id) {
|
||||||
|
router.push(`/docs/${id}`);
|
||||||
|
}
|
||||||
|
}, [router, treeContext?.root?.id]);
|
||||||
|
|
||||||
|
const handleRootFocus = useCallback(() => {
|
||||||
|
selectRoot();
|
||||||
|
}, [selectRoot]);
|
||||||
|
|
||||||
|
// activate root document with enter or space
|
||||||
|
const handleRootKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
selectRoot();
|
||||||
|
navigateToRoot();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[selectRoot, navigateToRoot],
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This effect is used to reset the tree when a new document
|
* This effect is used to reset the tree when a new document
|
||||||
* that is not part of the current tree is loaded.
|
* that is not part of the current tree is loaded.
|
||||||
@@ -77,7 +108,6 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
|||||||
if (!treeContext?.root?.id) {
|
if (!treeContext?.root?.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = findIndexInTree(treeContext.treeData.nodes, currentDoc.id);
|
const index = findIndexInTree(treeContext.treeData.nodes, currentDoc.id);
|
||||||
if (index === -1 && currentDoc.id !== treeContext.root?.id) {
|
if (index === -1 && currentDoc.id !== treeContext.root?.id) {
|
||||||
resetStateTree();
|
resetStateTree();
|
||||||
@@ -92,7 +122,6 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
|||||||
return () => {
|
return () => {
|
||||||
resetStateTree();
|
resetStateTree();
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -144,14 +173,13 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootIsSelected =
|
|
||||||
treeContext.treeData.selectedNode?.id === treeContext.root.id;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
ref={setTreeRoot}
|
ref={setTreeRoot}
|
||||||
data-testid="doc-tree"
|
data-testid="doc-tree"
|
||||||
$height="100%"
|
$height="100%"
|
||||||
|
role="tree"
|
||||||
|
aria-label={t('Document tree')}
|
||||||
$css={css`
|
$css={css`
|
||||||
.c__tree-view--container {
|
.c__tree-view--container {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -171,6 +199,12 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
|||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
data-testid="doc-tree-root-item"
|
data-testid="doc-tree-root-item"
|
||||||
|
role="treeitem"
|
||||||
|
aria-label={`${t('Root document')}: ${treeContext.root?.title || t('Untitled document')}`}
|
||||||
|
aria-selected={rootIsSelected}
|
||||||
|
tabIndex={0}
|
||||||
|
onFocus={handleRootFocus}
|
||||||
|
onKeyDown={handleRootKeyDown}
|
||||||
$css={css`
|
$css={css`
|
||||||
padding: ${spacingsTokens['2xs']};
|
padding: ${spacingsTokens['2xs']};
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -191,7 +225,8 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&:focus-within {
|
||||||
.doc-tree-root-item-actions {
|
.doc-tree-root-item-actions {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
@@ -211,6 +246,8 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
|||||||
);
|
);
|
||||||
router.push(`/docs/${treeContext?.root?.id}`);
|
router.push(`/docs/${treeContext?.root?.id}`);
|
||||||
}}
|
}}
|
||||||
|
aria-label={`${t('Open root document')}: ${treeContext.root?.title || t('Untitled document')}`}
|
||||||
|
tabIndex={-1} // avoid double tabstop
|
||||||
>
|
>
|
||||||
<Box $direction="row" $align="center" $width="100%">
|
<Box $direction="row" $align="center" $width="100%">
|
||||||
<SimpleDocItem doc={treeContext.root} showAccesses={true} />
|
<SimpleDocItem doc={treeContext.root} showAccesses={true} />
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export const useKeyboardActivation = (
|
||||||
|
keys: string[],
|
||||||
|
enabled: boolean,
|
||||||
|
action: () => void,
|
||||||
|
capture = false,
|
||||||
|
selector: string,
|
||||||
|
) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (keys.includes(e.key)) {
|
||||||
|
e.preventDefault();
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const treeEl = document.querySelector<HTMLElement>(selector);
|
||||||
|
if (!treeEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
treeEl.addEventListener('keydown', onKeyDown, capture);
|
||||||
|
return () => {
|
||||||
|
treeEl.removeEventListener('keydown', onKeyDown, capture);
|
||||||
|
};
|
||||||
|
}, [keys, enabled, action, capture, selector]);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user