Use 'role' = button for chip navigation (#8011)

Closes #7817
Added role attribute to the div element of the Chip component. This
assigns the role of "button" to the container, which is important for
accessibility. It indicates that this div should be treated as a button
by assistive technologies like screen readers.

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Balaji Krishnamurthy 2024-10-24 17:50:45 +05:30 committed by GitHub
parent 445ab83c14
commit 4e59f00e3f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 49 additions and 37 deletions

View File

@ -1,4 +1,4 @@
import { AvatarChip, AvatarChipVariant, UndecoratedLink } from 'twenty-ui';
import { AvatarChip, AvatarChipVariant } from 'twenty-ui';
import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
import { useRecordChipData } from '@/object-record/hooks/useRecordChipData';
@ -23,24 +23,20 @@ export const RecordChip = ({
record,
});
const handleClick = (e: MouseEvent<HTMLAnchorElement>) => {
const handleClick = (e: MouseEvent<Element>) => {
e.stopPropagation();
};
return (
<UndecoratedLink
<AvatarChip
placeholderColorSeed={record.id}
name={recordChipData.name}
avatarType={recordChipData.avatarType}
avatarUrl={recordChipData.avatarUrl ?? ''}
className={className}
variant={variant}
onClick={handleClick}
to={getLinkToShowPage(objectNameSingular, record)}
>
<AvatarChip
placeholderColorSeed={record.id}
name={recordChipData.name}
avatarType={recordChipData.avatarType}
avatarUrl={recordChipData.avatarUrl ?? ''}
className={className}
variant={variant}
onClick={() => {}}
/>
</UndecoratedLink>
/>
);
};

View File

@ -18,16 +18,12 @@ export const RecordIdentifierChip = ({
variant,
size,
}: RecordIdentifierChipProps) => {
const { onIndexIdentifierClick } = useContext(RecordIndexRootPropsContext);
const { indexIdentifierUrl } = useContext(RecordIndexRootPropsContext);
const { recordChipData } = useRecordChipData({
objectNameSingular,
record,
});
const handleAvatarChipClick = () => {
onIndexIdentifierClick(record.id);
};
const { Icon: LeftIcon, IconColor: LeftIconColor } =
useGetStandardObjectIcon(objectNameSingular);
return (
@ -36,7 +32,7 @@ export const RecordIdentifierChip = ({
name={recordChipData.name}
avatarType={recordChipData.avatarType}
avatarUrl={recordChipData.avatarUrl ?? ''}
onClick={handleAvatarChipClick}
to={indexIdentifierUrl(record.id)}
variant={variant}
LeftIcon={LeftIcon}
LeftIconColor={LeftIconColor}

View File

@ -2,7 +2,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { createRootPropsContext } from '~/utils/createRootPropsContext';
export type RecordIndexRootPropsContextProps = {
onIndexIdentifierClick: (recordId: string) => void;
indexIdentifierUrl: (recordId: string) => string;
onIndexRecordsLoaded: () => void;
onCreateRecord: () => void;
objectNamePlural: string;

View File

@ -2,7 +2,6 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { buildShowPageURL } from '@/object-record/record-show/utils/buildShowPageURL';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
import { useNavigate } from 'react-router-dom';
export const useHandleIndexIdentifierClick = ({
objectMetadataItem,
@ -11,22 +10,20 @@ export const useHandleIndexIdentifierClick = ({
recordIndexId: string;
objectMetadataItem: ObjectMetadataItem;
}) => {
const navigate = useNavigate();
const currentViewId = useRecoilComponentValueV2(
currentViewIdComponentState,
recordIndexId,
);
const handleIndexIdentifierClick = (recordId: string) => {
const indexIdentifierUrl = (recordId: string) => {
const showPageURL = buildShowPageURL(
objectMetadataItem.nameSingular,
recordId,
currentViewId,
);
navigate(showPageURL);
return showPageURL;
};
return { handleIndexIdentifierClick };
return { indexIdentifierUrl };
};

View File

@ -22,6 +22,7 @@ import { isDefined } from '~/utils/isDefined';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
export const DEFAULT_CELL_SCOPE: HotkeyScope = {
@ -41,7 +42,7 @@ export type OpenTableCellArgs = {
};
export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
const { onIndexIdentifierClick } = useContext(RecordIndexRootPropsContext);
const { indexIdentifierUrl } = useContext(RecordIndexRootPropsContext);
const moveEditModeToTableCellPosition =
useMoveEditModeToTableCellPosition(tableScopeId);
@ -61,6 +62,8 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
viewableRecordNameSingularState,
);
const navigate = useNavigate();
const openTableCell = useRecoilCallback(
({ snapshot }) =>
({
@ -95,7 +98,7 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
if (isFirstColumnCell && !isEmpty && !isActionButtonClick) {
leaveTableFocus();
onIndexIdentifierClick(recordId);
navigate(indexIdentifierUrl(recordId));
return;
}
@ -143,7 +146,8 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
openRightDrawer,
setViewableRecordId,
setViewableRecordNameSingular,
onIndexIdentifierClick,
indexIdentifierUrl,
navigate,
],
);

View File

@ -10,13 +10,13 @@ import {
IconDoorEnter,
IconFunction,
IconHierarchy2,
IconKey,
IconMail,
IconRocket,
IconSettings,
IconTool,
IconUserCircle,
IconUsers,
IconKey,
MAIN_COLORS,
} from 'twenty-ui';

View File

@ -44,7 +44,7 @@ export const RecordIndexPage = () => {
createNewTableRecord();
};
const { handleIndexIdentifierClick } = useHandleIndexIdentifierClick({
const { indexIdentifierUrl } = useHandleIndexIdentifierClick({
objectMetadataItem,
recordIndexId,
});
@ -67,7 +67,7 @@ export const RecordIndexPage = () => {
objectNameSingular,
objectMetadataItem,
onIndexRecordsLoaded: handleIndexRecordsLoaded,
onIndexIdentifierClick: handleIndexIdentifierClick,
indexIdentifierUrl,
onCreateRecord: handleCreateRecord,
}}
>

View File

@ -8,6 +8,9 @@ import { isDefined } from '@ui/utilities/isDefined';
import { Nullable } from '@ui/utilities/types/Nullable';
import { MouseEvent, useContext } from 'react';
// Import Link from react-router-dom instead of UndecoratedLink
import { Link } from 'react-router-dom';
export type AvatarChipProps = {
name: string;
avatarUrl?: string;
@ -20,6 +23,7 @@ export type AvatarChipProps = {
className?: string;
placeholderColorSeed?: string;
onClick?: (event: MouseEvent) => void;
to?: string;
};
export enum AvatarChipVariant {
@ -37,6 +41,12 @@ const StyledInvertedIconContainer = styled.div<{ backgroundColor: string }>`
background-color: ${({ backgroundColor }) => backgroundColor};
`;
// Ideally we would use the UndecoratedLink component from @ui/navigation
// but it led to a bug probably linked to circular dependencies, which was hard to solve
const StyledLink = styled(Link)`
text-decoration: none;
`;
export const AvatarChip = ({
name,
avatarUrl,
@ -48,15 +58,16 @@ export const AvatarChip = ({
className,
placeholderColorSeed,
onClick,
to,
size = ChipSize.Small,
}: AvatarChipProps) => {
const { theme } = useContext(ThemeContext);
return (
const chip = (
<Chip
label={name}
variant={
isDefined(onClick)
isDefined(onClick) || isDefined(to)
? variant === AvatarChipVariant.Regular
? ChipVariant.Highlighted
: ChipVariant.Regular
@ -92,9 +103,17 @@ export const AvatarChip = ({
/>
)
}
clickable={isDefined(onClick)}
onClick={onClick}
clickable={isDefined(onClick) || isDefined(to)}
onClick={to ? undefined : onClick}
className={className}
/>
);
return to ? (
<StyledLink to={to} onClick={onClick}>
{chip}
</StyledLink>
) : (
chip
);
};