Fix record board column header action accessibility (#22496)

## Summary
- Keep Record Board column header actions mounted instead of rendering
them only on mouse hover
- Show actions on hover and focus-within so keyboard users can reach
them
- Avoid header layout shifts when actions appear

## Context
This is a small follow-up found while reviewing #22323. It does not
duplicate the Kanban column drag-and-drop implementation.

## Testing
- git diff --check
- Not run: package lint/typecheck because this checkout still has no
node_modules and Yarn is not available on PATH

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22496?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
This commit is contained in:
Gautam pandit
2026-07-20 21:30:31 +05:30
committed by GitHub
parent 8a35f78d70
commit 3e14dcbb05
@@ -1,5 +1,6 @@
import { styled } from '@linaria/react';
import { useContext, useState } from 'react';
import { t } from '@lingui/core/macro';
import { useContext } from 'react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
@@ -21,6 +22,7 @@ import { canCreateRecordsForObjectMetadataItem } from '@/object-record/utils/can
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useDisableDragSelectOnPointerDown } from '@/ui/utilities/drag-select/hooks/useDisableDragSelectOnPointerDown';
import { useToggleDropdown } from '@/ui/layout/dropdown/hooks/useToggleDropdown';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
@@ -38,8 +40,31 @@ const StyledHeader = styled.div`
`;
const StyledHeaderActions = styled.div`
align-items: center;
display: flex;
margin-left: auto;
flex-shrink: 0;
// padding + negative margin cancel out in layout and exist only so
// overflow:hidden clips 4px outside each button, leaving room for
// LightIconButton's 3px focus ring
margin: calc(-1 * ${themeCssVariables.spacing[1]});
max-width: 0;
min-width: 0;
opacity: 0;
overflow: hidden;
padding: ${themeCssVariables.spacing[1]};
pointer-events: none;
transition:
max-width ease-in-out
calc(${themeCssVariables.animation.duration.fast} * 1s),
opacity ease-in-out calc(${themeCssVariables.animation.duration.fast} * 1s);
&[data-dropdown-open='true'],
${StyledHeader}:hover &,
${StyledHeader}:focus-within & {
max-width: ${themeCssVariables.spacing[14]};
opacity: 1;
pointer-events: auto;
}
`;
const StyledHeaderContainer = styled.div`
@@ -55,11 +80,6 @@ const StyledLeftContainer = styled.div`
overflow: hidden;
`;
const StyledRightContainer = styled.div`
align-items: center;
display: flex;
`;
const StyledColumn = styled.div`
background-color: ${themeCssVariables.background.primary};
display: flex;
@@ -104,8 +124,6 @@ export const RecordBoardColumnHeader = () => {
onPointerUp: handlePointerUp,
} = useDisableDragSelectOnPointerDown();
const [isHeaderHovered, setIsHeaderHovered] = useState(false);
const { objectMetadataItem, selectFieldMetadataItem } =
useContext(RecordBoardContext);
@@ -140,6 +158,11 @@ export const RecordBoardColumnHeader = () => {
const dropdownId = `record-board-column-dropdown-${columnDefinition.id}`;
const isDropdownOpen = useAtomComponentStateValue(
isDropdownOpenComponentState,
dropdownId,
);
const handleCreateNewRecordClick = async () => {
await createNewIndexRecord({
position: 'first',
@@ -152,8 +175,6 @@ export const RecordBoardColumnHeader = () => {
<StyledColumn data-has-left-border={columnIndex > 0 ? 'true' : undefined}>
<DragDropColumnSortableHandle fill>
<StyledHeader
onMouseEnter={() => setIsHeaderHovered(true)}
onMouseLeave={() => setIsHeaderHovered(false)}
onPointerCancel={handlePointerCancel}
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
@@ -187,28 +208,28 @@ export const RecordBoardColumnHeader = () => {
aggregateLabel={recordIndexAggregateDisplayLabel}
/>
</StyledLeftContainer>
<StyledRightContainer>
{isHeaderHovered && (
<StyledHeaderActions>
<LightIconButton
accent="tertiary"
Icon={IconDotsVertical}
onClick={() => {
toggleDropdown({
dropdownComponentInstanceIdFromProps: dropdownId,
});
}}
/>
{canCreateRecords && !hasAnySoftDeleteFilterOnView && (
<LightIconButton
accent="tertiary"
Icon={IconPlus}
onClick={handleCreateNewRecordClick}
/>
)}
</StyledHeaderActions>
<StyledHeaderActions
data-dropdown-open={isDropdownOpen ? 'true' : undefined}
>
<LightIconButton
accent="tertiary"
aria-label={t`More options`}
Icon={IconDotsVertical}
onClick={() => {
toggleDropdown({
dropdownComponentInstanceIdFromProps: dropdownId,
});
}}
/>
{canCreateRecords && !hasAnySoftDeleteFilterOnView && (
<LightIconButton
accent="tertiary"
aria-label={t`Add new`}
Icon={IconPlus}
onClick={handleCreateNewRecordClick}
/>
)}
</StyledRightContainer>
</StyledHeaderActions>
</StyledHeaderContainer>
</StyledHeader>
</DragDropColumnSortableHandle>