fixup! ✨(frontend) make components accessible to screen readers
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { BoxButton, Icon } from '@/components';
|
||||
|
||||
type ButtonAddChildDocProps = {
|
||||
onCreateChild: (params: { parentId: string }) => void;
|
||||
parentId: string;
|
||||
title?: string | null;
|
||||
};
|
||||
|
||||
export const ButtonAddChildDoc = ({
|
||||
onCreateChild,
|
||||
parentId,
|
||||
title,
|
||||
}: ButtonAddChildDocProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const preventDefaultAndStopPropagation = useCallback(
|
||||
(e: React.MouseEvent | React.KeyboardEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const isValidKeyEvent = useCallback((e: React.KeyboardEvent) => {
|
||||
return e.key === 'Enter' || e.key === ' ';
|
||||
}, []);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
preventDefaultAndStopPropagation(e);
|
||||
void onCreateChild({ parentId });
|
||||
},
|
||||
[onCreateChild, parentId, preventDefaultAndStopPropagation],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (isValidKeyEvent(e)) {
|
||||
preventDefaultAndStopPropagation(e);
|
||||
void onCreateChild({ parentId });
|
||||
}
|
||||
},
|
||||
[
|
||||
onCreateChild,
|
||||
parentId,
|
||||
preventDefaultAndStopPropagation,
|
||||
isValidKeyEvent,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<BoxButton
|
||||
as="button"
|
||||
tabIndex={-1}
|
||||
data-testid="add-child-doc"
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
color="primary"
|
||||
aria-label={t('Add child document to {{title}}', {
|
||||
title: title || t('Untitled document'),
|
||||
})}
|
||||
$hasTransition={false}
|
||||
>
|
||||
<Icon
|
||||
variant="filled"
|
||||
$variation="800"
|
||||
$theme="primary"
|
||||
iconName="add_box"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</BoxButton>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Icon } from '@/components';
|
||||
|
||||
type ButtonMoreOptionsProps = {
|
||||
isOpen?: boolean;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
title?: string | null;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const ButtonMoreOptions = ({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
title,
|
||||
className = 'icon-button',
|
||||
}: ButtonMoreOptionsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const preventDefaultAndStopPropagation = useCallback(
|
||||
(e: React.MouseEvent | React.KeyboardEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const isValidKeyEvent = useCallback((e: React.KeyboardEvent) => {
|
||||
return e.key === 'Enter' || e.key === ' ';
|
||||
}, []);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
preventDefaultAndStopPropagation(e);
|
||||
onOpenChange?.(!isOpen);
|
||||
},
|
||||
[isOpen, onOpenChange, preventDefaultAndStopPropagation],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (isValidKeyEvent(e)) {
|
||||
preventDefaultAndStopPropagation(e);
|
||||
onOpenChange?.(!isOpen);
|
||||
}
|
||||
},
|
||||
[isOpen, onOpenChange, preventDefaultAndStopPropagation, isValidKeyEvent],
|
||||
);
|
||||
|
||||
return (
|
||||
<Icon
|
||||
onClick={handleClick}
|
||||
iconName="more_horiz"
|
||||
variant="filled"
|
||||
$theme="primary"
|
||||
$variation="600"
|
||||
className={className}
|
||||
tabIndex={-1}
|
||||
role="button"
|
||||
aria-label={t('More options for {{title}}', {
|
||||
title: title || t('Untitled document'),
|
||||
})}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={isOpen}
|
||||
onKeyDown={handleKeyDown}
|
||||
$css={css`
|
||||
cursor: pointer;
|
||||
`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+13
-51
@@ -8,9 +8,7 @@ import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, BoxButton, Icon } from '@/components';
|
||||
import { useDocTreeItemHandlers } from '@/features/docs/doc-tree/hooks/useDocTreeItemHandlers';
|
||||
import { useDropdownFocusManagement } from '@/features/docs/doc-tree/hooks/useDropdownFocusManagement';
|
||||
import { Box, Icon } from '@/components';
|
||||
import {
|
||||
Doc,
|
||||
ModalRemoveDoc,
|
||||
@@ -19,6 +17,9 @@ import {
|
||||
useCreateChildDoc,
|
||||
useDuplicateDoc,
|
||||
} from '@/docs/doc-management';
|
||||
import { ButtonAddChildDoc } from '@/features/docs/doc-tree/components/ButtonAddChildDoc';
|
||||
import { ButtonMoreOptions } from '@/features/docs/doc-tree/components/ButtonMoreOptions';
|
||||
import { useDropdownFocusManagement } from '@/features/docs/doc-tree/hooks/useDropdownFocusManagement';
|
||||
|
||||
import { useDetachDoc } from '../api/useDetach';
|
||||
import MoveDocIcon from '../assets/doc-extract-bold.svg';
|
||||
@@ -149,18 +150,6 @@ export const DocTreeItemActions = ({
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
handleMoreOptionsClick,
|
||||
handleMoreOptionsKeyDown,
|
||||
handleAddChildClick,
|
||||
handleAddChildKeyDown,
|
||||
} = useDocTreeItemHandlers({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
createChildDoc,
|
||||
docId: doc.id,
|
||||
});
|
||||
|
||||
useDropdownFocusManagement({
|
||||
isOpen: isOpen || false,
|
||||
docId: doc.id,
|
||||
@@ -202,45 +191,18 @@ export const DocTreeItemActions = ({
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<Icon
|
||||
onClick={handleMoreOptionsClick}
|
||||
iconName="more_horiz"
|
||||
variant="filled"
|
||||
$theme="primary"
|
||||
$variation="600"
|
||||
className="icon-button"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label={
|
||||
t('More options for') + ` ${doc.title || t('Untitled document')}`
|
||||
}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={isOpen}
|
||||
onKeyDown={handleMoreOptionsKeyDown}
|
||||
<ButtonMoreOptions
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
title={doc.title}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
{doc.abilities.children_create && (
|
||||
<BoxButton
|
||||
as="button"
|
||||
tabIndex={0}
|
||||
data-testid="add-child-doc"
|
||||
onClick={handleAddChildClick}
|
||||
onKeyDown={handleAddChildKeyDown}
|
||||
color="primary"
|
||||
aria-label={
|
||||
t('Add child document to') +
|
||||
` ${doc.title || t('Untitled document')}`
|
||||
}
|
||||
$hasTransition={false}
|
||||
>
|
||||
<Icon
|
||||
variant="filled"
|
||||
$variation="800"
|
||||
$theme="primary"
|
||||
iconName="add_box"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</BoxButton>
|
||||
<ButtonAddChildDoc
|
||||
onCreateChild={createChildDoc}
|
||||
parentId={doc.id}
|
||||
title={doc.title}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{deleteModal.isOpen && (
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
interface UseDocTreeItemHandlersProps {
|
||||
isOpen?: boolean;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
createChildDoc: (params: { parentId: string }) => void;
|
||||
docId: string;
|
||||
}
|
||||
|
||||
export const useDocTreeItemHandlers = ({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
createChildDoc,
|
||||
docId,
|
||||
}: UseDocTreeItemHandlersProps) => {
|
||||
const preventDefaultAndStopPropagation = useCallback(
|
||||
(e: React.MouseEvent | React.KeyboardEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const isValidKeyEvent = useCallback((e: React.KeyboardEvent) => {
|
||||
return e.key === 'Enter' || e.key === ' ';
|
||||
}, []);
|
||||
|
||||
const handleMoreOptionsClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
preventDefaultAndStopPropagation(e);
|
||||
onOpenChange?.(!isOpen);
|
||||
},
|
||||
[isOpen, onOpenChange, preventDefaultAndStopPropagation],
|
||||
);
|
||||
|
||||
const handleMoreOptionsKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (isValidKeyEvent(e)) {
|
||||
preventDefaultAndStopPropagation(e);
|
||||
onOpenChange?.(!isOpen);
|
||||
}
|
||||
},
|
||||
[isOpen, onOpenChange, preventDefaultAndStopPropagation, isValidKeyEvent],
|
||||
);
|
||||
|
||||
const handleAddChildClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
preventDefaultAndStopPropagation(e);
|
||||
void createChildDoc({
|
||||
parentId: docId,
|
||||
});
|
||||
},
|
||||
[createChildDoc, docId, preventDefaultAndStopPropagation],
|
||||
);
|
||||
|
||||
const handleAddChildKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (isValidKeyEvent(e)) {
|
||||
preventDefaultAndStopPropagation(e);
|
||||
void createChildDoc({
|
||||
parentId: docId,
|
||||
});
|
||||
}
|
||||
},
|
||||
[createChildDoc, docId, preventDefaultAndStopPropagation, isValidKeyEvent],
|
||||
);
|
||||
|
||||
return {
|
||||
handleMoreOptionsClick,
|
||||
handleMoreOptionsKeyDown,
|
||||
handleAddChildClick,
|
||||
handleAddChildKeyDown,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user