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

View File

@ -1,6 +1,4 @@
import { useContext } from 'react'; import { useContext } from 'react';
import styled from '@emotion/styled';
import { useSetRecoilState } from 'recoil';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; 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 { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableCell } from '@/object-record/record-table/record-table-cell/components/RecordTableCell'; 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 { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; 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'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
const StyledContainer = styled.td<{ isSelected: boolean }>`
background: ${({ isSelected, theme }) =>
isSelected ? theme.accent.quaternary : theme.background.primary};
`;
export const RecordTableCellContainer = () => { 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 { objectMetadataItem } = useContext(RecordTableContext);
const { columnDefinition } = useContext(RecordTableCellContext); const { columnDefinition } = useContext(RecordTableCellContext);
const { recordId, pathToShowPage, isSelected } = useContext( const { recordId, pathToShowPage } = useContext(RecordTableRowContext);
RecordTableRowContext,
);
const updateRecord = useContext(RecordUpdateContext); const updateRecord = useContext(RecordUpdateContext);
@ -55,29 +28,24 @@ export const RecordTableCellContainer = () => {
: TableHotkeyScope.CellEditMode; : TableHotkeyScope.CellEditMode;
return ( return (
<StyledContainer <FieldContext.Provider
isSelected={isSelected} value={{
onContextMenu={(event) => handleContextMenu(event)} 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 <RecordTableCell customHotkeyScope={{ scope: customHotkeyScope }} />
value={{ </FieldContext.Provider>
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>
); );
}; };

View File

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

View File

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