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:
@ -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} />
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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};
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user