feat: add Table and TableSection components (#1849)
* refactor: rename ui/table to ui/data-table * feat: add Table and TableSection components Closes #1806
This commit is contained in:
@ -0,0 +1,74 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { FieldDisplay } from '@/ui/field/components/FieldDisplay';
|
||||
import { FieldInput } from '@/ui/field/components/FieldInput';
|
||||
import { FieldContext } from '@/ui/field/contexts/FieldContext';
|
||||
import { FieldInputEvent } from '@/ui/field/types/FieldInputEvent';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
|
||||
import { useMoveSoftFocus } from '../../hooks/useMoveSoftFocus';
|
||||
import { useTableCell } from '../hooks/useTableCell';
|
||||
|
||||
import { TableCellContainer } from './TableCellContainer';
|
||||
|
||||
export const TableCell = ({
|
||||
customHotkeyScope,
|
||||
}: {
|
||||
customHotkeyScope: HotkeyScope;
|
||||
}) => {
|
||||
const { fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
const { closeTableCell } = useTableCell();
|
||||
|
||||
const { moveLeft, moveRight, moveDown } = useMoveSoftFocus();
|
||||
|
||||
const handleEnter: FieldInputEvent = (persistField) => {
|
||||
persistField();
|
||||
closeTableCell();
|
||||
moveDown();
|
||||
};
|
||||
|
||||
const handleSubmit: FieldInputEvent = (persistField) => {
|
||||
persistField();
|
||||
closeTableCell();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
closeTableCell();
|
||||
};
|
||||
|
||||
const handleEscape = () => {
|
||||
closeTableCell();
|
||||
};
|
||||
|
||||
const handleTab: FieldInputEvent = (persistField) => {
|
||||
persistField();
|
||||
closeTableCell();
|
||||
moveRight();
|
||||
};
|
||||
|
||||
const handleShiftTab: FieldInputEvent = (persistField) => {
|
||||
persistField();
|
||||
closeTableCell();
|
||||
moveLeft();
|
||||
};
|
||||
|
||||
return (
|
||||
<TableCellContainer
|
||||
editHotkeyScope={customHotkeyScope}
|
||||
editModeContent={
|
||||
<FieldInput
|
||||
onCancel={handleCancel}
|
||||
onClickOutside={handleCancel}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onSubmit={handleSubmit}
|
||||
onTab={handleTab}
|
||||
/>
|
||||
}
|
||||
nonEditModeContent={<FieldDisplay />}
|
||||
buttonIcon={fieldDefinition.buttonIcon}
|
||||
></TableCellContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,26 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { FloatingIconButton } from '@/ui/button/components/FloatingIconButton';
|
||||
import { IconComponent } from '@/ui/icon/types/IconComponent';
|
||||
|
||||
const StyledEditButtonContainer = styled(motion.div)`
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
`;
|
||||
|
||||
type TableCellButtonProps = {
|
||||
onClick?: () => void;
|
||||
Icon: IconComponent;
|
||||
};
|
||||
|
||||
export const TableCellButton = ({ onClick, Icon }: TableCellButtonProps) => (
|
||||
<StyledEditButtonContainer
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
whileHover={{ scale: 1.04 }}
|
||||
>
|
||||
<FloatingIconButton size="small" onClick={onClick} Icon={Icon} />
|
||||
</StyledEditButtonContainer>
|
||||
);
|
||||
@ -0,0 +1,134 @@
|
||||
import { ReactElement, useContext, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useIsFieldEmpty } from '@/ui/field/hooks/useIsFieldEmpty';
|
||||
import { useIsFieldInputOnly } from '@/ui/field/hooks/useIsFieldInputOnly';
|
||||
import { IconComponent } from '@/ui/icon/types/IconComponent';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
|
||||
import { CellHotkeyScopeContext } from '../../contexts/CellHotkeyScopeContext';
|
||||
import { ColumnIndexContext } from '../../contexts/ColumnIndexContext';
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
import { useCurrentTableCellEditMode } from '../hooks/useCurrentTableCellEditMode';
|
||||
import { useIsSoftFocusOnCurrentTableCell } from '../hooks/useIsSoftFocusOnCurrentTableCell';
|
||||
import { useSetSoftFocusOnCurrentTableCell } from '../hooks/useSetSoftFocusOnCurrentTableCell';
|
||||
import { useTableCell } from '../hooks/useTableCell';
|
||||
|
||||
import { TableCellButton } from './TableCellButton';
|
||||
import { TableCellDisplayMode } from './TableCellDisplayMode';
|
||||
import { TableCellEditMode } from './TableCellEditMode';
|
||||
import { TableCellSoftFocusMode } from './TableCellSoftFocusMode';
|
||||
|
||||
const StyledCellBaseContainer = styled.div`
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 32px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
export type EditableCellProps = {
|
||||
editModeContent: ReactElement;
|
||||
nonEditModeContent: ReactElement;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
editModeVerticalPosition?: 'over' | 'below';
|
||||
editHotkeyScope?: HotkeyScope;
|
||||
transparent?: boolean;
|
||||
maxContentWidth?: number;
|
||||
buttonIcon?: IconComponent;
|
||||
onSubmit?: () => void;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
const DEFAULT_CELL_SCOPE: HotkeyScope = {
|
||||
scope: TableHotkeyScope.CellEditMode,
|
||||
};
|
||||
|
||||
export const TableCellContainer = ({
|
||||
editModeHorizontalAlign = 'left',
|
||||
editModeVerticalPosition = 'over',
|
||||
editModeContent,
|
||||
nonEditModeContent,
|
||||
editHotkeyScope,
|
||||
transparent = false,
|
||||
maxContentWidth,
|
||||
buttonIcon,
|
||||
}: EditableCellProps) => {
|
||||
const { isCurrentTableCellInEditMode } = useCurrentTableCellEditMode();
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const setSoftFocusOnCurrentTableCell = useSetSoftFocusOnCurrentTableCell();
|
||||
|
||||
const { openTableCell } = useTableCell();
|
||||
|
||||
const handleButtonClick = () => {
|
||||
setSoftFocusOnCurrentTableCell();
|
||||
openTableCell();
|
||||
};
|
||||
|
||||
const handleContainerMouseEnter = () => {
|
||||
setIsHovered(true);
|
||||
};
|
||||
|
||||
const handleContainerMouseLeave = () => {
|
||||
setIsHovered(false);
|
||||
};
|
||||
|
||||
const editModeContentOnly = useIsFieldInputOnly();
|
||||
|
||||
const isFirstColumnCell = useContext(ColumnIndexContext) === 0;
|
||||
|
||||
const isEmpty = useIsFieldEmpty();
|
||||
|
||||
const showButton =
|
||||
buttonIcon &&
|
||||
isHovered &&
|
||||
!isCurrentTableCellInEditMode &&
|
||||
!editModeContentOnly &&
|
||||
(!isFirstColumnCell || !isEmpty);
|
||||
|
||||
const hasSoftFocus = useIsSoftFocusOnCurrentTableCell();
|
||||
|
||||
return (
|
||||
<CellHotkeyScopeContext.Provider
|
||||
value={editHotkeyScope ?? DEFAULT_CELL_SCOPE}
|
||||
>
|
||||
<StyledCellBaseContainer
|
||||
onMouseEnter={handleContainerMouseEnter}
|
||||
onMouseLeave={handleContainerMouseLeave}
|
||||
>
|
||||
{isCurrentTableCellInEditMode ? (
|
||||
<TableCellEditMode
|
||||
maxContentWidth={maxContentWidth}
|
||||
transparent={transparent}
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeVerticalPosition={editModeVerticalPosition}
|
||||
>
|
||||
{editModeContent}
|
||||
</TableCellEditMode>
|
||||
) : hasSoftFocus ? (
|
||||
<>
|
||||
{showButton && (
|
||||
<TableCellButton onClick={handleButtonClick} Icon={buttonIcon} />
|
||||
)}
|
||||
<TableCellSoftFocusMode>
|
||||
{editModeContentOnly ? editModeContent : nonEditModeContent}
|
||||
</TableCellSoftFocusMode>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{showButton && (
|
||||
<TableCellButton onClick={handleButtonClick} Icon={buttonIcon} />
|
||||
)}
|
||||
<TableCellDisplayMode isHovered={isHovered}>
|
||||
{editModeContentOnly ? editModeContent : nonEditModeContent}
|
||||
</TableCellDisplayMode>
|
||||
</>
|
||||
)}
|
||||
</StyledCellBaseContainer>
|
||||
</CellHotkeyScopeContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,57 @@
|
||||
import { Ref } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export type EditableCellDisplayContainerProps = {
|
||||
softFocus?: boolean;
|
||||
onClick?: () => void;
|
||||
scrollRef?: Ref<HTMLDivElement>;
|
||||
isHovered?: boolean;
|
||||
};
|
||||
|
||||
const StyledEditableCellDisplayModeOuterContainer = styled.div<
|
||||
Pick<EditableCellDisplayContainerProps, 'softFocus' | 'isHovered'>
|
||||
>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||
width: 100%;
|
||||
${(props) =>
|
||||
props.softFocus || props.isHovered
|
||||
? `background: ${props.theme.background.transparent.secondary};
|
||||
border-radius: ${props.theme.border.radius.sm};
|
||||
outline: 1px solid ${props.theme.font.color.extraLight};`
|
||||
: ''}
|
||||
`;
|
||||
|
||||
const StyledEditableCellDisplayModeInnerContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const TableCellDisplayContainer = ({
|
||||
children,
|
||||
softFocus,
|
||||
onClick,
|
||||
scrollRef,
|
||||
isHovered,
|
||||
}: React.PropsWithChildren<EditableCellDisplayContainerProps>) => (
|
||||
<StyledEditableCellDisplayModeOuterContainer
|
||||
data-testid={
|
||||
softFocus ? 'editable-cell-soft-focus-mode' : 'editable-cell-display-mode'
|
||||
}
|
||||
onClick={onClick}
|
||||
isHovered={isHovered}
|
||||
softFocus={softFocus}
|
||||
ref={scrollRef}
|
||||
>
|
||||
<StyledEditableCellDisplayModeInnerContainer>
|
||||
{children}
|
||||
</StyledEditableCellDisplayModeInnerContainer>
|
||||
</StyledEditableCellDisplayModeOuterContainer>
|
||||
);
|
||||
@ -0,0 +1,31 @@
|
||||
import { useIsFieldInputOnly } from '@/ui/field/hooks/useIsFieldInputOnly';
|
||||
|
||||
import { useSetSoftFocusOnCurrentTableCell } from '../hooks/useSetSoftFocusOnCurrentTableCell';
|
||||
import { useTableCell } from '../hooks/useTableCell';
|
||||
|
||||
import { TableCellDisplayContainer } from './TableCellDisplayContainer';
|
||||
|
||||
export const TableCellDisplayMode = ({
|
||||
children,
|
||||
isHovered,
|
||||
}: React.PropsWithChildren<unknown> & { isHovered?: boolean }) => {
|
||||
const setSoftFocusOnCurrentCell = useSetSoftFocusOnCurrentTableCell();
|
||||
|
||||
const isFieldInputOnly = useIsFieldInputOnly();
|
||||
|
||||
const { openTableCell } = useTableCell();
|
||||
|
||||
const handleClick = () => {
|
||||
setSoftFocusOnCurrentCell();
|
||||
|
||||
if (!isFieldInputOnly) {
|
||||
openTableCell();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TableCellDisplayContainer isHovered={isHovered} onClick={handleClick}>
|
||||
{children}
|
||||
</TableCellDisplayContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,26 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { FloatingIconButton } from '@/ui/button/components/FloatingIconButton';
|
||||
import { IconComponent } from '@/ui/icon/types/IconComponent';
|
||||
|
||||
const StyledEditButtonContainer = styled(motion.div)`
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
`;
|
||||
|
||||
type TableCellButtonProps = {
|
||||
onClick?: () => void;
|
||||
Icon: IconComponent;
|
||||
};
|
||||
|
||||
export const TableCellButton = ({ onClick, Icon }: TableCellButtonProps) => (
|
||||
<StyledEditButtonContainer
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
whileHover={{ scale: 1.04 }}
|
||||
>
|
||||
<FloatingIconButton size="small" onClick={onClick} Icon={Icon} />
|
||||
</StyledEditButtonContainer>
|
||||
);
|
||||
@ -0,0 +1,53 @@
|
||||
import { ReactElement } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { overlayBackground } from '@/ui/theme/constants/effects';
|
||||
|
||||
const StyledEditableCellEditModeContainer = styled.div<EditableCellEditModeProps>`
|
||||
align-items: center;
|
||||
border: ${({ transparent, theme }) =>
|
||||
transparent ? 'none' : `1px solid ${theme.border.color.light}`};
|
||||
border-radius: ${({ transparent, theme }) =>
|
||||
transparent ? 'none' : theme.border.radius.sm};
|
||||
display: flex;
|
||||
left: ${(props) =>
|
||||
props.editModeHorizontalAlign === 'right' ? 'auto' : '0'};
|
||||
margin: -1px;
|
||||
max-width: ${({ maxContentWidth }) =>
|
||||
maxContentWidth ? `${maxContentWidth}px` : 'none'};
|
||||
min-height: 100%;
|
||||
min-width: ${({ maxContentWidth }) => (maxContentWidth ? `none` : '100%')};
|
||||
position: absolute;
|
||||
right: ${(props) =>
|
||||
props.editModeHorizontalAlign === 'right' ? '0' : 'auto'};
|
||||
top: ${(props) => (props.editModeVerticalPosition === 'over' ? '0' : '100%')};
|
||||
${({ transparent }) => (transparent ? '' : overlayBackground)};
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
export type EditableCellEditModeProps = {
|
||||
children: ReactElement;
|
||||
transparent?: boolean;
|
||||
maxContentWidth?: number;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
editModeVerticalPosition?: 'over' | 'below';
|
||||
initialValue?: string;
|
||||
};
|
||||
|
||||
export const TableCellEditMode = ({
|
||||
editModeHorizontalAlign,
|
||||
editModeVerticalPosition,
|
||||
children,
|
||||
transparent = false,
|
||||
maxContentWidth,
|
||||
}: EditableCellEditModeProps) => (
|
||||
<StyledEditableCellEditModeContainer
|
||||
maxContentWidth={maxContentWidth}
|
||||
transparent={transparent}
|
||||
data-testid="editable-cell-edit-mode-container"
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeVerticalPosition={editModeVerticalPosition}
|
||||
>
|
||||
{children}
|
||||
</StyledEditableCellEditModeContainer>
|
||||
);
|
||||
@ -0,0 +1,74 @@
|
||||
import { PropsWithChildren, useEffect, useRef } from 'react';
|
||||
|
||||
import { useIsFieldInputOnly } from '@/ui/field/hooks/useIsFieldInputOnly';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
|
||||
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
import { useTableCell } from '../hooks/useTableCell';
|
||||
|
||||
import { TableCellDisplayContainer } from './TableCellDisplayContainer';
|
||||
|
||||
type OwnProps = PropsWithChildren<unknown>;
|
||||
|
||||
export const TableCellSoftFocusMode = ({ children }: OwnProps) => {
|
||||
const { openTableCell } = useTableCell();
|
||||
|
||||
const isFieldInputOnly = useIsFieldInputOnly();
|
||||
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
scrollRef.current?.scrollIntoView({ block: 'nearest' });
|
||||
}, []);
|
||||
|
||||
useScopedHotkeys(
|
||||
'enter',
|
||||
() => {
|
||||
openTableCell();
|
||||
},
|
||||
TableHotkeyScope.TableSoftFocus,
|
||||
[openTableCell],
|
||||
{
|
||||
enabled: !isFieldInputOnly,
|
||||
},
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
'*',
|
||||
(keyboardEvent) => {
|
||||
const isWritingText =
|
||||
!isNonTextWritingKey(keyboardEvent.key) &&
|
||||
!keyboardEvent.ctrlKey &&
|
||||
!keyboardEvent.metaKey;
|
||||
|
||||
if (!isWritingText) {
|
||||
return;
|
||||
}
|
||||
|
||||
openTableCell();
|
||||
},
|
||||
TableHotkeyScope.TableSoftFocus,
|
||||
[openTableCell],
|
||||
{
|
||||
preventDefault: false,
|
||||
enabled: !isFieldInputOnly,
|
||||
},
|
||||
);
|
||||
|
||||
const handleClick = () => {
|
||||
if (!isFieldInputOnly) {
|
||||
openTableCell();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TableCellDisplayContainer
|
||||
onClick={handleClick}
|
||||
softFocus
|
||||
scrollRef={scrollRef}
|
||||
>
|
||||
{children}
|
||||
</TableCellDisplayContainer>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user