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,10 +28,6 @@ export const RecordTableCellContainer = () => {
: TableHotkeyScope.CellEditMode; : TableHotkeyScope.CellEditMode;
return ( return (
<StyledContainer
isSelected={isSelected}
onContextMenu={(event) => handleContextMenu(event)}
>
<FieldContext.Provider <FieldContext.Provider
value={{ value={{
recoilScopeId: recordId + columnDefinition.label, recoilScopeId: recordId + columnDefinition.label,
@ -78,6 +47,5 @@ export const RecordTableCellContainer = () => {
> >
<RecordTableCell customHotkeyScope={{ scope: customHotkeyScope }} /> <RecordTableCell customHotkeyScope={{ scope: customHotkeyScope }} />
</FieldContext.Provider> </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,13 +90,43 @@ export const RecordTableCellContainer = ({
openTableCell(); openTableCell();
}; };
const handleContainerMouseEnter = () => { 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) { if (!isHovered && !isSomeCellInEditMode) {
setIsHovered(true); setIsHovered(true);
moveSoftFocusToCurrentCellOnHover(); moveSoftFocusToCurrentCellOnHover();
setIsSoftFocusUsingMouseState(true); setIsSoftFocusUsingMouseState(true);
} }
}; },
[
isHovered,
isSomeCellInEditModeState,
moveSoftFocusToCurrentCellOnHover,
setIsSoftFocusUsingMouseState,
],
);
const handleContainerMouseLeave = () => { const handleContainerMouseLeave = () => {
setIsHovered(false); setIsHovered(false);
@ -109,6 +149,11 @@ export const RecordTableCellContainer = ({
(!isFirstColumn || !isEmpty); (!isFirstColumn || !isEmpty);
return ( return (
<StyledTd
isSelected={isSelected}
onContextMenu={(event) => handleContextMenu(event)}
isInEditMode={isCurrentTableCellInEditMode}
>
<CellHotkeyScopeContext.Provider <CellHotkeyScopeContext.Provider
value={editHotkeyScope ?? DEFAULT_CELL_SCOPE} value={editHotkeyScope ?? DEFAULT_CELL_SCOPE}
> >
@ -150,5 +195,6 @@ export const RecordTableCellContainer = ({
)} )}
</StyledCellBaseContainer> </StyledCellBaseContainer>
</CellHotkeyScopeContext.Provider> </CellHotkeyScopeContext.Provider>
</StyledTd>
); );
}; };