4162-Sticky-Header (#4627)

* initial commit

* functionality added

* Suggested changes fixed

* Fix broken shadow

* Unrelated fix (input stuck under container)

* Performance improvement

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
Kanav Arora
2024-03-25 23:35:56 +05:30
committed by GitHub
parent 8baa59b6f4
commit 9dda6a8fa1
4 changed files with 134 additions and 103 deletions

View File

@ -12,8 +12,12 @@ import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTabl
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/MobileViewport';
import { RGBA } from '@/ui/theme/constants/Rgba';
import { scrollLeftState } from '@/ui/utilities/scroll/states/scrollLeftState';
import { scrollTopState } from '@/ui/utilities/scroll/states/scrollTopState';
const StyledTable = styled.table<{ freezeFirstColumns?: boolean }>`
const StyledTable = styled.table<{
freezeFirstColumns?: boolean;
freezeHeaders?: boolean;
}>`
border-radius: ${({ theme }) => theme.border.radius.sm};
border-spacing: 0;
margin-right: ${({ theme }) => theme.table.horizontalCellMargin};
@ -65,12 +69,24 @@ const StyledTable = styled.table<{ freezeFirstColumns?: boolean }>`
border-right: none;
}
thead th:nth-of-type(1),
tbody td:nth-of-type(1) {
left: 0;
}
// Label identifier column
thead th:nth-of-type(1),
thead th:nth-of-type(2) {
left: 0;
top: 0;
z-index: 6;
}
thead th:nth-child(n + 3) {
top: 0;
z-index: 5;
position: sticky;
}
thead th:nth-of-type(2),
tbody td:nth-of-type(2) {
left: calc(${({ theme }) => theme.table.checkboxColumnWidth} - 2px);
@ -88,9 +104,9 @@ const StyledTable = styled.table<{ freezeFirstColumns?: boolean }>`
content: '';
height: calc(100% + 1px);
position: absolute;
top: 0;
width: 4px;
right: -4px;
top: 0;
${({ freezeFirstColumns, theme }) =>
freezeFirstColumns &&
@ -123,6 +139,7 @@ export const RecordTable = ({
}: RecordTableProps) => {
const { scopeId } = useRecordTableStates(recordTableId);
const scrollLeft = useRecoilValue(scrollLeftState);
const scrollTop = useRecoilValue(scrollTopState);
const { objectMetadataItem } = useObjectMetadataItemOnly({
objectNameSingular,
@ -141,6 +158,7 @@ export const RecordTable = ({
>
<StyledTable
freezeFirstColumns={scrollLeft > 0}
freezeHeaders={scrollTop > 0}
className="entity-table-cell"
>
<RecordTableHeader createRecord={createRecord} />

View File

@ -1,6 +1,4 @@
import { useContext } from 'react';
import styled from '@emotion/styled';
import { useSetRecoilState } from 'recoil';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
@ -10,39 +8,14 @@ import { RecordTableCellContext } from '@/object-record/record-table/contexts/Re
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableCell } from '@/object-record/record-table/record-table-cell/components/RecordTableCell';
import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState';
import { contextMenuPositionState } from '@/ui/navigation/context-menu/states/contextMenuPositionState';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
const StyledContainer = styled.td<{ isSelected: boolean }>`
background: ${({ isSelected, theme }) =>
isSelected ? theme.accent.quaternary : theme.background.primary};
`;
export const RecordTableCellContainer = () => {
const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState);
const { setCurrentRowSelected } = useSetCurrentRowSelected();
const handleContextMenu = (event: React.MouseEvent) => {
event.preventDefault();
setCurrentRowSelected(true);
setContextMenuPosition({
x: event.clientX,
y: event.clientY,
});
setContextMenuOpenState(true);
};
const { objectMetadataItem } = useContext(RecordTableContext);
const { columnDefinition } = useContext(RecordTableCellContext);
const { recordId, pathToShowPage, isSelected } = useContext(
RecordTableRowContext,
);
const { recordId, pathToShowPage } = useContext(RecordTableRowContext);
const updateRecord = useContext(RecordUpdateContext);
@ -55,29 +28,24 @@ export const RecordTableCellContainer = () => {
: TableHotkeyScope.CellEditMode;
return (
<StyledContainer
isSelected={isSelected}
onContextMenu={(event) => handleContextMenu(event)}
<FieldContext.Provider
value={{
recoilScopeId: recordId + columnDefinition.label,
entityId: recordId,
fieldDefinition: columnDefinition,
useUpdateRecord: () => [updateRecord, {}],
hotkeyScope: customHotkeyScope,
basePathToShowPage: pathToShowPage,
isLabelIdentifier: isLabelIdentifierField({
fieldMetadataItem: {
id: columnDefinition.fieldMetadataId,
name: columnDefinition.metadata.fieldName,
},
objectMetadataItem,
}),
}}
>
<FieldContext.Provider
value={{
recoilScopeId: recordId + columnDefinition.label,
entityId: recordId,
fieldDefinition: columnDefinition,
useUpdateRecord: () => [updateRecord, {}],
hotkeyScope: customHotkeyScope,
basePathToShowPage: pathToShowPage,
isLabelIdentifier: isLabelIdentifierField({
fieldMetadataItem: {
id: columnDefinition.fieldMetadataId,
name: columnDefinition.metadata.fieldName,
},
objectMetadataItem,
}),
}}
>
<RecordTableCell customHotkeyScope={{ scope: customHotkeyScope }} />
</FieldContext.Provider>
</StyledContainer>
<RecordTableCell customHotkeyScope={{ scope: customHotkeyScope }} />
</FieldContext.Provider>
);
};

View File

@ -28,8 +28,7 @@ const StyledPlusIconHeaderCell = styled.th<{ isTableWiderThanScreen: boolean }>`
min-width: 32px;
${({ isTableWiderThanScreen, theme }) =>
isTableWiderThanScreen &&
`position: relative;
right: 0;
`
width: 32px;
border-right: none !important;
background-color: ${theme.background.primary};

View File

@ -1,16 +1,21 @@
import { ReactElement, useContext, useState } from 'react';
import styled from '@emotion/styled';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon';
import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEmpty';
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useGetIsSomeCellInEditModeState } from '@/object-record/record-table/hooks/internal/useGetIsSomeCellInEditMode';
import { useOpenRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCell';
import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected';
import { isSoftFocusUsingMouseState } from '@/object-record/record-table/states/isSoftFocusUsingMouseState';
import { IconArrowUpRight } from '@/ui/display/icon';
import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState';
import { contextMenuPositionState } from '@/ui/navigation/context-menu/states/contextMenuPositionState';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { CellHotkeyScopeContext } from '../../contexts/CellHotkeyScopeContext';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
@ -24,6 +29,12 @@ import { RecordTableCellDisplayMode } from './RecordTableCellDisplayMode';
import { RecordTableCellEditMode } from './RecordTableCellEditMode';
import { RecordTableCellSoftFocusMode } from './RecordTableCellSoftFocusMode';
const StyledTd = styled.td<{ isSelected: boolean; isInEditMode: boolean }>`
background: ${({ isSelected, theme }) =>
isSelected ? theme.accent.quaternary : theme.background.primary};
z-index: ${({ isInEditMode }) => (isInEditMode ? '4 !important' : '3')};
`;
const StyledCellBaseContainer = styled.div`
align-items: center;
box-sizing: border-box;
@ -61,7 +72,6 @@ export const RecordTableCellContainer = ({
const { isCurrentTableCellInEditMode } = useCurrentTableCellEditMode();
const isSomeCellInEditModeState = useGetIsSomeCellInEditModeState();
const isSomeCellInEditMode = useRecoilValue(isSomeCellInEditModeState());
const setIsSoftFocusUsingMouseState = useSetRecoilState(
isSoftFocusUsingMouseState,
@ -80,14 +90,44 @@ export const RecordTableCellContainer = ({
openTableCell();
};
const handleContainerMouseEnter = () => {
if (!isHovered && !isSomeCellInEditMode) {
setIsHovered(true);
moveSoftFocusToCurrentCellOnHover();
setIsSoftFocusUsingMouseState(true);
}
const { isSelected } = useContext(RecordTableRowContext);
const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState);
const { setCurrentRowSelected } = useSetCurrentRowSelected();
const handleContextMenu = (event: React.MouseEvent) => {
event.preventDefault();
setCurrentRowSelected(true);
setContextMenuPosition({
x: event.clientX,
y: event.clientY,
});
setContextMenuOpenState(true);
};
const handleContainerMouseEnter = useRecoilCallback(
({ snapshot }) =>
() => {
const isSomeCellInEditMode = getSnapshotValue(
snapshot,
isSomeCellInEditModeState(),
);
if (!isHovered && !isSomeCellInEditMode) {
setIsHovered(true);
moveSoftFocusToCurrentCellOnHover();
setIsSoftFocusUsingMouseState(true);
}
},
[
isHovered,
isSomeCellInEditModeState,
moveSoftFocusToCurrentCellOnHover,
setIsSoftFocusUsingMouseState,
],
);
const handleContainerMouseLeave = () => {
setIsHovered(false);
};
@ -109,46 +149,52 @@ export const RecordTableCellContainer = ({
(!isFirstColumn || !isEmpty);
return (
<CellHotkeyScopeContext.Provider
value={editHotkeyScope ?? DEFAULT_CELL_SCOPE}
<StyledTd
isSelected={isSelected}
onContextMenu={(event) => handleContextMenu(event)}
isInEditMode={isCurrentTableCellInEditMode}
>
<StyledCellBaseContainer
onMouseEnter={handleContainerMouseEnter}
onMouseLeave={handleContainerMouseLeave}
<CellHotkeyScopeContext.Provider
value={editHotkeyScope ?? DEFAULT_CELL_SCOPE}
>
{isCurrentTableCellInEditMode ? (
<RecordTableCellEditMode
editModeHorizontalAlign={editModeHorizontalAlign}
editModeVerticalPosition={editModeVerticalPosition}
>
{editModeContent}
</RecordTableCellEditMode>
) : hasSoftFocus ? (
<>
{showButton && (
<RecordTableCellButton
onClick={handleButtonClick}
Icon={buttonIcon}
/>
)}
<RecordTableCellSoftFocusMode>
{editModeContentOnly ? editModeContent : nonEditModeContent}
</RecordTableCellSoftFocusMode>
</>
) : (
<>
{showButton && (
<RecordTableCellButton
onClick={handleButtonClick}
Icon={buttonIcon}
/>
)}
<RecordTableCellDisplayMode>
{editModeContentOnly ? editModeContent : nonEditModeContent}
</RecordTableCellDisplayMode>
</>
)}
</StyledCellBaseContainer>
</CellHotkeyScopeContext.Provider>
<StyledCellBaseContainer
onMouseEnter={handleContainerMouseEnter}
onMouseLeave={handleContainerMouseLeave}
>
{isCurrentTableCellInEditMode ? (
<RecordTableCellEditMode
editModeHorizontalAlign={editModeHorizontalAlign}
editModeVerticalPosition={editModeVerticalPosition}
>
{editModeContent}
</RecordTableCellEditMode>
) : hasSoftFocus ? (
<>
{showButton && (
<RecordTableCellButton
onClick={handleButtonClick}
Icon={buttonIcon}
/>
)}
<RecordTableCellSoftFocusMode>
{editModeContentOnly ? editModeContent : nonEditModeContent}
</RecordTableCellSoftFocusMode>
</>
) : (
<>
{showButton && (
<RecordTableCellButton
onClick={handleButtonClick}
Icon={buttonIcon}
/>
)}
<RecordTableCellDisplayMode>
{editModeContentOnly ? editModeContent : nonEditModeContent}
</RecordTableCellDisplayMode>
</>
)}
</StyledCellBaseContainer>
</CellHotkeyScopeContext.Provider>
</StyledTd>
);
};