Fix issue 2126: DataTable '+' button dropdown positioning glitch (#2150)
* Fix issue 2126: DataTable '+' button dropdown positioning glitch * Simplify code * Fix lint --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -4,6 +4,8 @@ import { useRecoilCallback, useRecoilState } from 'recoil';
|
|||||||
|
|
||||||
import { IconPlus } from '@/ui/display/icon';
|
import { IconPlus } from '@/ui/display/icon';
|
||||||
import { IconButton } from '@/ui/input/button/components/IconButton';
|
import { IconButton } from '@/ui/input/button/components/IconButton';
|
||||||
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
|
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||||
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
|
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
|
||||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||||
|
|
||||||
@ -16,7 +18,7 @@ import { visibleTableColumnsScopedSelector } from '../states/selectors/visibleTa
|
|||||||
import { tableColumnsScopedState } from '../states/tableColumnsScopedState';
|
import { tableColumnsScopedState } from '../states/tableColumnsScopedState';
|
||||||
|
|
||||||
import { ColumnHeadWithDropdown } from './ColumnHeadWithDropdown';
|
import { ColumnHeadWithDropdown } from './ColumnHeadWithDropdown';
|
||||||
import { DataTableHeaderPlusButton } from './DataTableHeaderPlusButton';
|
import { DataTableHeaderPlusButtonContent } from './DataTableHeaderPlusButtonContent';
|
||||||
import { SelectAllCheckbox } from './SelectAllCheckbox';
|
import { SelectAllCheckbox } from './SelectAllCheckbox';
|
||||||
|
|
||||||
const COLUMN_MIN_WIDTH = 104;
|
const COLUMN_MIN_WIDTH = 104;
|
||||||
@ -58,18 +60,6 @@ const StyledResizeHandler = styled.div`
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledAddIconButtonWrapper = styled.div`
|
|
||||||
display: inline-flex;
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledDataTableColumnMenu = styled(DataTableHeaderPlusButton)`
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 100%;
|
|
||||||
z-index: ${({ theme }) => theme.lastLayerZIndex};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledTableHead = styled.thead`
|
const StyledTableHead = styled.thead`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
`;
|
`;
|
||||||
@ -79,6 +69,12 @@ const StyledColumnHeadContainer = styled.div`
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const HIDDEN_TABLE_COLUMN_DROPDOWN_SCOPE_ID =
|
||||||
|
'hidden-table-columns-dropdown-scope-id';
|
||||||
|
|
||||||
|
const HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID =
|
||||||
|
'hidden-table-columns-dropdown-hotkey-scope-id';
|
||||||
|
|
||||||
export const DataTableHeader = () => {
|
export const DataTableHeader = () => {
|
||||||
const [resizeFieldOffset, setResizeFieldOffset] = useRecoilState(
|
const [resizeFieldOffset, setResizeFieldOffset] = useRecoilState(
|
||||||
resizeFieldOffsetState,
|
resizeFieldOffsetState,
|
||||||
@ -104,7 +100,6 @@ export const DataTableHeader = () => {
|
|||||||
number | null
|
number | null
|
||||||
>(null);
|
>(null);
|
||||||
const [resizedFieldKey, setResizedFieldKey] = useState<string | null>(null);
|
const [resizedFieldKey, setResizedFieldKey] = useState<string | null>(null);
|
||||||
const [isColumnMenuOpen, setIsColumnMenuOpen] = useState(false);
|
|
||||||
|
|
||||||
const { handleColumnsChange } = useTableColumns();
|
const { handleColumnsChange } = useTableColumns();
|
||||||
|
|
||||||
@ -157,10 +152,6 @@ export const DataTableHeader = () => {
|
|||||||
onMouseUp: handleResizeHandlerEnd,
|
onMouseUp: handleResizeHandlerEnd,
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleColumnMenu = useCallback(() => {
|
|
||||||
setIsColumnMenuOpen((previousValue) => !previousValue);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const primaryColumn = visibleTableColumns[0];
|
const primaryColumn = visibleTableColumns[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -202,24 +193,29 @@ export const DataTableHeader = () => {
|
|||||||
/>
|
/>
|
||||||
</StyledColumnHeaderCell>
|
</StyledColumnHeaderCell>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<th>
|
<th>
|
||||||
{hiddenTableColumns.length > 0 && (
|
{hiddenTableColumns.length > 0 && (
|
||||||
<StyledAddIconButtonWrapper>
|
<StyledColumnHeadContainer>
|
||||||
<IconButton
|
<DropdownScope
|
||||||
size="medium"
|
dropdownScopeId={HIDDEN_TABLE_COLUMN_DROPDOWN_SCOPE_ID}
|
||||||
variant="tertiary"
|
>
|
||||||
Icon={IconPlus}
|
<Dropdown
|
||||||
onClick={toggleColumnMenu}
|
clickableComponent={
|
||||||
position="middle"
|
<IconButton
|
||||||
/>
|
size="medium"
|
||||||
{isColumnMenuOpen && (
|
variant="tertiary"
|
||||||
<StyledDataTableColumnMenu
|
Icon={IconPlus}
|
||||||
onAddColumn={toggleColumnMenu}
|
position="middle"
|
||||||
onClickOutside={toggleColumnMenu}
|
/>
|
||||||
|
}
|
||||||
|
dropdownComponents={<DataTableHeaderPlusButtonContent />}
|
||||||
|
dropdownPlacement="bottom-start"
|
||||||
|
dropdownHotkeyScope={{
|
||||||
|
scope: HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
</DropdownScope>
|
||||||
</StyledAddIconButtonWrapper>
|
</StyledColumnHeadContainer>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -1,71 +0,0 @@
|
|||||||
import { ComponentProps, useCallback, useRef } from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
|
|
||||||
import { IconPlus } from '@/ui/display/icon';
|
|
||||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
|
||||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
|
||||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
|
||||||
|
|
||||||
import { useTableColumns } from '../hooks/useTableColumns';
|
|
||||||
import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext';
|
|
||||||
import { hiddenTableColumnsScopedSelector } from '../states/selectors/hiddenTableColumnsScopedSelector';
|
|
||||||
import { ColumnDefinition } from '../types/ColumnDefinition';
|
|
||||||
|
|
||||||
const StyledHeaderPlusButton = styled(DropdownMenu)`
|
|
||||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
|
||||||
`;
|
|
||||||
|
|
||||||
type DataTableHeaderPlusButtonProps = {
|
|
||||||
onAddColumn?: () => void;
|
|
||||||
onClickOutside?: () => void;
|
|
||||||
} & ComponentProps<'div'>;
|
|
||||||
|
|
||||||
export const DataTableHeaderPlusButton = ({
|
|
||||||
onAddColumn,
|
|
||||||
onClickOutside = () => undefined,
|
|
||||||
}: DataTableHeaderPlusButtonProps) => {
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const hiddenTableColumns = useRecoilScopedValue(
|
|
||||||
hiddenTableColumnsScopedSelector,
|
|
||||||
TableRecoilScopeContext,
|
|
||||||
);
|
|
||||||
|
|
||||||
useListenClickOutside({
|
|
||||||
refs: [ref],
|
|
||||||
callback: onClickOutside,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { handleColumnVisibilityChange } = useTableColumns();
|
|
||||||
|
|
||||||
const handleAddColumn = useCallback(
|
|
||||||
(column: ColumnDefinition<FieldMetadata>) => {
|
|
||||||
onAddColumn?.();
|
|
||||||
handleColumnVisibilityChange(column);
|
|
||||||
},
|
|
||||||
[handleColumnVisibilityChange, onAddColumn],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledHeaderPlusButton ref={ref}>
|
|
||||||
<DropdownMenuItemsContainer>
|
|
||||||
{hiddenTableColumns.map((column) => (
|
|
||||||
<MenuItem
|
|
||||||
key={column.key}
|
|
||||||
iconButtons={[
|
|
||||||
{
|
|
||||||
Icon: IconPlus,
|
|
||||||
onClick: () => handleAddColumn(column),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
LeftIcon={column.Icon}
|
|
||||||
text={column.name}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</DropdownMenuItemsContainer>
|
|
||||||
</StyledHeaderPlusButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
|
||||||
|
import { IconPlus } from '@/ui/display/icon';
|
||||||
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
|
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||||
|
|
||||||
|
import { useTableColumns } from '../hooks/useTableColumns';
|
||||||
|
import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||||
|
import { hiddenTableColumnsScopedSelector } from '../states/selectors/hiddenTableColumnsScopedSelector';
|
||||||
|
import { ColumnDefinition } from '../types/ColumnDefinition';
|
||||||
|
|
||||||
|
export const DataTableHeaderPlusButtonContent = () => {
|
||||||
|
const { closeDropdown } = useDropdown();
|
||||||
|
|
||||||
|
const hiddenTableColumns = useRecoilScopedValue(
|
||||||
|
hiddenTableColumnsScopedSelector,
|
||||||
|
TableRecoilScopeContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { handleColumnVisibilityChange } = useTableColumns();
|
||||||
|
|
||||||
|
const handleAddColumn = useCallback(
|
||||||
|
(column: ColumnDefinition<FieldMetadata>) => {
|
||||||
|
closeDropdown();
|
||||||
|
handleColumnVisibilityChange(column);
|
||||||
|
},
|
||||||
|
[handleColumnVisibilityChange, closeDropdown],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItemsContainer>
|
||||||
|
{hiddenTableColumns.map((column) => (
|
||||||
|
<MenuItem
|
||||||
|
key={column.key}
|
||||||
|
iconButtons={[
|
||||||
|
{
|
||||||
|
Icon: IconPlus,
|
||||||
|
onClick: () => handleAddColumn(column),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
LeftIcon={column.Icon}
|
||||||
|
text={column.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -51,7 +51,7 @@ export const MenuItem = ({
|
|||||||
</StyledMenuItemLeftContent>
|
</StyledMenuItemLeftContent>
|
||||||
<div className="hoverable-buttons">
|
<div className="hoverable-buttons">
|
||||||
{showIconButtons && (
|
{showIconButtons && (
|
||||||
<FloatingIconButtonGroup iconButtons={iconButtons} />
|
<FloatingIconButtonGroup iconButtons={iconButtons} size="small" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</StyledHoverableMenuItemBase>
|
</StyledHoverableMenuItemBase>
|
||||||
|
|||||||
Reference in New Issue
Block a user