Uniformize folder structure (#693)
* Uniformize folder structure * Fix icons * Fix icons * Fix tests * Fix tests
This commit is contained in:
@ -0,0 +1,9 @@
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
export function CellSkeleton() {
|
||||
return (
|
||||
<div style={{ width: '100%', alignItems: 'center' }}>
|
||||
<Skeleton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
import { ReactElement } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
|
||||
|
||||
import { useCurrentCellEditMode } from '../hooks/useCurrentCellEditMode';
|
||||
import { useIsSoftFocusOnCurrentCell } from '../hooks/useIsSoftFocusOnCurrentCell';
|
||||
|
||||
import { EditableCellDisplayMode } from './EditableCellDisplayMode';
|
||||
import { EditableCellEditMode } from './EditableCellEditMode';
|
||||
import { EditableCellSoftFocusMode } from './EditableCellSoftFocusMode';
|
||||
|
||||
export const CellBaseContainer = styled.div`
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 32px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
editModeContent: ReactElement;
|
||||
nonEditModeContent: ReactElement;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
editModeVerticalPosition?: 'over' | 'below';
|
||||
editHotkeyScope?: HotkeyScope;
|
||||
onSubmit?: () => void;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
export function EditableCell({
|
||||
editModeHorizontalAlign = 'left',
|
||||
editModeVerticalPosition = 'over',
|
||||
editModeContent,
|
||||
nonEditModeContent,
|
||||
editHotkeyScope,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: OwnProps) {
|
||||
const { isCurrentCellInEditMode } = useCurrentCellEditMode();
|
||||
|
||||
const hasSoftFocus = useIsSoftFocusOnCurrentCell();
|
||||
|
||||
return (
|
||||
<CellBaseContainer>
|
||||
{isCurrentCellInEditMode ? (
|
||||
<EditableCellEditMode
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeVerticalPosition={editModeVerticalPosition}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
>
|
||||
{editModeContent}
|
||||
</EditableCellEditMode>
|
||||
) : hasSoftFocus ? (
|
||||
<EditableCellSoftFocusMode editHotkeyScope={editHotkeyScope}>
|
||||
{nonEditModeContent}
|
||||
</EditableCellSoftFocusMode>
|
||||
) : (
|
||||
<EditableCellDisplayMode>{nonEditModeContent}</EditableCellDisplayMode>
|
||||
)}
|
||||
</CellBaseContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useSetSoftFocusOnCurrentCell } from '../hooks/useSetSoftFocusOnCurrentCell';
|
||||
|
||||
type Props = {
|
||||
softFocus?: boolean;
|
||||
};
|
||||
|
||||
export const EditableCellNormalModeOuterContainer = styled.div<Props>`
|
||||
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
|
||||
? `background: ${props.theme.background.transparent.secondary};
|
||||
border-radius: ${props.theme.border.radius.md};
|
||||
box-shadow: inset 0 0 0 1px ${props.theme.font.color.extraLight};`
|
||||
: ''}
|
||||
`;
|
||||
|
||||
export const EditableCellNormalModeInnerContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export function EditableCellDisplayMode({
|
||||
children,
|
||||
}: React.PropsWithChildren<unknown>) {
|
||||
const setSoftFocusOnCurrentCell = useSetSoftFocusOnCurrentCell();
|
||||
|
||||
function handleOnClick() {
|
||||
setSoftFocusOnCurrentCell();
|
||||
}
|
||||
|
||||
return (
|
||||
<EditableCellNormalModeOuterContainer onClick={handleOnClick}>
|
||||
<EditableCellNormalModeInnerContainer>
|
||||
{children}
|
||||
</EditableCellNormalModeInnerContainer>
|
||||
</EditableCellNormalModeOuterContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
import { ReactElement, useRef } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { overlayBackground } from '@/ui/themes/effects';
|
||||
|
||||
import { useRegisterCloseCellHandlers } from '../hooks/useRegisterCloseCellHandlers';
|
||||
|
||||
export const EditableCellEditModeContainer = styled.div<OwnProps>`
|
||||
align-items: center;
|
||||
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
display: flex;
|
||||
left: ${(props) =>
|
||||
props.editModeHorizontalAlign === 'right' ? 'auto' : '0'};
|
||||
margin-left: -1px;
|
||||
margin-top: -1px;
|
||||
|
||||
min-height: 100%;
|
||||
position: absolute;
|
||||
right: ${(props) =>
|
||||
props.editModeHorizontalAlign === 'right' ? '0' : 'auto'};
|
||||
|
||||
top: ${(props) => (props.editModeVerticalPosition === 'over' ? '0' : '100%')};
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
${overlayBackground}
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
children: ReactElement;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
editModeVerticalPosition?: 'over' | 'below';
|
||||
onOutsideClick?: () => void;
|
||||
onCancel?: () => void;
|
||||
onSubmit?: () => void;
|
||||
};
|
||||
|
||||
export function EditableCellEditMode({
|
||||
editModeHorizontalAlign,
|
||||
editModeVerticalPosition,
|
||||
children,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}: OwnProps) {
|
||||
const wrapperRef = useRef(null);
|
||||
|
||||
useRegisterCloseCellHandlers(wrapperRef, onSubmit, onCancel);
|
||||
|
||||
return (
|
||||
<EditableCellEditModeContainer
|
||||
data-testid="editable-cell-edit-mode-container"
|
||||
ref={wrapperRef}
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeVerticalPosition={editModeVerticalPosition}
|
||||
>
|
||||
{children}
|
||||
</EditableCellEditModeContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
|
||||
import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
|
||||
import { isNonTextWritingKey } from '@/ui/hotkey/utils/isNonTextWritingKey';
|
||||
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
import { useEditableCell } from '../hooks/useEditableCell';
|
||||
|
||||
import {
|
||||
EditableCellNormalModeInnerContainer,
|
||||
EditableCellNormalModeOuterContainer,
|
||||
} from './EditableCellDisplayMode';
|
||||
|
||||
export function EditableCellSoftFocusMode({
|
||||
children,
|
||||
editHotkeyScope,
|
||||
}: React.PropsWithChildren<{ editHotkeyScope?: HotkeyScope }>) {
|
||||
const { openEditableCell } = useEditableCell();
|
||||
|
||||
function openEditMode() {
|
||||
openEditableCell(
|
||||
editHotkeyScope ?? {
|
||||
scope: TableHotkeyScope.CellEditMode,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
useScopedHotkeys(
|
||||
'enter',
|
||||
() => {
|
||||
openEditMode();
|
||||
},
|
||||
TableHotkeyScope.TableSoftFocus,
|
||||
[openEditMode],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
'*',
|
||||
(keyboardEvent) => {
|
||||
const isWritingText =
|
||||
!isNonTextWritingKey(keyboardEvent.key) &&
|
||||
!keyboardEvent.ctrlKey &&
|
||||
!keyboardEvent.metaKey;
|
||||
|
||||
if (!isWritingText) {
|
||||
return;
|
||||
}
|
||||
|
||||
openEditMode();
|
||||
},
|
||||
TableHotkeyScope.TableSoftFocus,
|
||||
[openEditMode],
|
||||
{
|
||||
preventDefault: false,
|
||||
},
|
||||
);
|
||||
|
||||
function handleClick() {
|
||||
openEditMode();
|
||||
}
|
||||
|
||||
return (
|
||||
<EditableCellNormalModeOuterContainer
|
||||
onClick={handleClick}
|
||||
softFocus={true}
|
||||
>
|
||||
<EditableCellNormalModeInnerContainer>
|
||||
{children}
|
||||
</EditableCellNormalModeInnerContainer>
|
||||
</EditableCellNormalModeOuterContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const HoverableMenuItem = styled.div`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
transition: background 0.1s ease;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.background.transparent.light};
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,46 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { userEvent, within } from '@storybook/testing-library';
|
||||
|
||||
import {
|
||||
CellPositionDecorator,
|
||||
ComponentDecorator,
|
||||
} from '~/testing/decorators';
|
||||
|
||||
import { EditableCellText } from '../../types/EditableCellText';
|
||||
|
||||
const meta: Meta<typeof EditableCellText> = {
|
||||
title: 'UI/EditableCell/EditableCellText',
|
||||
component: EditableCellText,
|
||||
decorators: [ComponentDecorator, CellPositionDecorator],
|
||||
args: {
|
||||
value: 'Content',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof EditableCellText>;
|
||||
|
||||
export const DisplayMode: Story = {
|
||||
render: EditableCellText,
|
||||
};
|
||||
|
||||
export const SoftFocusMode: Story = {
|
||||
...DisplayMode,
|
||||
play: async ({ canvasElement, step }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await step('Click once', () =>
|
||||
userEvent.click(canvas.getByText('Content')),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const EditMode: Story = {
|
||||
...DisplayMode,
|
||||
play: async ({ canvasElement, step }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const click = async () => userEvent.click(canvas.getByText('Content'));
|
||||
|
||||
await step('Click once', click);
|
||||
await step('Click twice', click);
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useMoveEditModeToCellPosition } from '../../hooks/useMoveEditModeToCellPosition';
|
||||
import { isCellInEditModeFamilyState } from '../../states/isCellInEditModeFamilyState';
|
||||
|
||||
import { useCurrentCellPosition } from './useCurrentCellPosition';
|
||||
|
||||
export function useCurrentCellEditMode() {
|
||||
const moveEditModeToCellPosition = useMoveEditModeToCellPosition();
|
||||
|
||||
const currentCellPosition = useCurrentCellPosition();
|
||||
|
||||
const [isCurrentCellInEditMode] = useRecoilState(
|
||||
isCellInEditModeFamilyState(currentCellPosition),
|
||||
);
|
||||
|
||||
const setCurrentCellInEditMode = useCallback(() => {
|
||||
moveEditModeToCellPosition(currentCellPosition);
|
||||
}, [currentCellPosition, moveEditModeToCellPosition]);
|
||||
|
||||
return { isCurrentCellInEditMode, setCurrentCellInEditMode };
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { CellContext } from '../../states/CellContext';
|
||||
import { currentColumnNumberScopedState } from '../../states/currentColumnNumberScopedState';
|
||||
import { currentRowNumberScopedState } from '../../states/currentRowNumberScopedState';
|
||||
import { RowContext } from '../../states/RowContext';
|
||||
import { CellPosition } from '../../types/CellPosition';
|
||||
|
||||
export function useCurrentCellPosition() {
|
||||
const [currentRowNumber] = useRecoilScopedState(
|
||||
currentRowNumberScopedState,
|
||||
RowContext,
|
||||
);
|
||||
|
||||
const [currentColumnNumber] = useRecoilScopedState(
|
||||
currentColumnNumberScopedState,
|
||||
CellContext,
|
||||
);
|
||||
|
||||
const currentCellPosition: CellPosition = useMemo(
|
||||
() => ({
|
||||
column: currentColumnNumber,
|
||||
row: currentRowNumber,
|
||||
}),
|
||||
[currentColumnNumber, currentRowNumber],
|
||||
);
|
||||
|
||||
return currentCellPosition;
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
|
||||
import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
|
||||
|
||||
import { useCloseCurrentCellInEditMode } from '../../hooks/useClearCellInEditMode';
|
||||
import { isSomeInputInEditModeState } from '../../states/isSomeInputInEditModeState';
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
|
||||
import { useCurrentCellEditMode } from './useCurrentCellEditMode';
|
||||
|
||||
export function useEditableCell() {
|
||||
const { setCurrentCellInEditMode } = useCurrentCellEditMode();
|
||||
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
const closeCurrentCellInEditMode = useCloseCurrentCellInEditMode();
|
||||
|
||||
function closeEditableCell() {
|
||||
closeCurrentCellInEditMode();
|
||||
setHotkeyScope(TableHotkeyScope.TableSoftFocus);
|
||||
}
|
||||
|
||||
const openEditableCell = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(HotkeyScope: HotkeyScope) => {
|
||||
const isSomeInputInEditMode = snapshot
|
||||
.getLoadable(isSomeInputInEditModeState)
|
||||
.valueOrThrow();
|
||||
|
||||
if (!isSomeInputInEditMode) {
|
||||
set(isSomeInputInEditModeState, true);
|
||||
|
||||
setCurrentCellInEditMode();
|
||||
|
||||
setHotkeyScope(HotkeyScope.scope);
|
||||
}
|
||||
},
|
||||
[setCurrentCellInEditMode, setHotkeyScope],
|
||||
);
|
||||
|
||||
return {
|
||||
closeEditableCell,
|
||||
openEditableCell,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { isSoftFocusOnCellFamilyState } from '../../states/isSoftFocusOnCellFamilyState';
|
||||
|
||||
import { useCurrentCellPosition } from './useCurrentCellPosition';
|
||||
|
||||
export function useIsSoftFocusOnCurrentCell() {
|
||||
const currentCellPosition = useCurrentCellPosition();
|
||||
|
||||
const isSoftFocusOnCell = useRecoilValue(
|
||||
isSoftFocusOnCellFamilyState(currentCellPosition),
|
||||
);
|
||||
|
||||
return isSoftFocusOnCell;
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
|
||||
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
|
||||
|
||||
import { useMoveSoftFocus } from '../../hooks/useMoveSoftFocus';
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
|
||||
import { useCurrentCellEditMode } from './useCurrentCellEditMode';
|
||||
import { useEditableCell } from './useEditableCell';
|
||||
|
||||
export function useRegisterCloseCellHandlers(
|
||||
wrapperRef: React.RefObject<HTMLDivElement>,
|
||||
onSubmit?: () => void,
|
||||
onCancel?: () => void,
|
||||
) {
|
||||
const { closeEditableCell } = useEditableCell();
|
||||
const { isCurrentCellInEditMode } = useCurrentCellEditMode();
|
||||
useListenClickOutsideArrayOfRef([wrapperRef], () => {
|
||||
if (isCurrentCellInEditMode) {
|
||||
onSubmit?.();
|
||||
closeEditableCell();
|
||||
}
|
||||
});
|
||||
const { moveRight, moveLeft, moveDown } = useMoveSoftFocus();
|
||||
|
||||
useScopedHotkeys(
|
||||
'enter',
|
||||
() => {
|
||||
onSubmit?.();
|
||||
closeEditableCell();
|
||||
moveDown();
|
||||
},
|
||||
TableHotkeyScope.CellEditMode,
|
||||
[closeEditableCell, onSubmit, moveDown],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
'esc',
|
||||
() => {
|
||||
closeEditableCell();
|
||||
onCancel?.();
|
||||
},
|
||||
TableHotkeyScope.CellEditMode,
|
||||
[closeEditableCell, onCancel],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
'tab',
|
||||
() => {
|
||||
onSubmit?.();
|
||||
closeEditableCell();
|
||||
moveRight();
|
||||
},
|
||||
TableHotkeyScope.CellEditMode,
|
||||
[closeEditableCell, onSubmit, moveRight],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
'shift+tab',
|
||||
() => {
|
||||
onSubmit?.();
|
||||
closeEditableCell();
|
||||
moveLeft();
|
||||
},
|
||||
TableHotkeyScope.CellEditMode,
|
||||
[closeEditableCell, onSubmit, moveRight],
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
|
||||
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { useSetSoftFocusPosition } from '../../hooks/useSetSoftFocusPosition';
|
||||
import { CellContext } from '../../states/CellContext';
|
||||
import { currentColumnNumberScopedState } from '../../states/currentColumnNumberScopedState';
|
||||
import { currentRowNumberScopedState } from '../../states/currentRowNumberScopedState';
|
||||
import { isSoftFocusActiveState } from '../../states/isSoftFocusActiveState';
|
||||
import { RowContext } from '../../states/RowContext';
|
||||
import { CellPosition } from '../../types/CellPosition';
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
|
||||
export function useSetSoftFocusOnCurrentCell() {
|
||||
const setSoftFocusPosition = useSetSoftFocusPosition();
|
||||
|
||||
const [currentRowNumber] = useRecoilScopedState(
|
||||
currentRowNumberScopedState,
|
||||
RowContext,
|
||||
);
|
||||
|
||||
const [currentColumnNumber] = useRecoilScopedState(
|
||||
currentColumnNumberScopedState,
|
||||
CellContext,
|
||||
);
|
||||
|
||||
const currentTablePosition: CellPosition = useMemo(
|
||||
() => ({
|
||||
column: currentColumnNumber,
|
||||
row: currentRowNumber,
|
||||
}),
|
||||
[currentColumnNumber, currentRowNumber],
|
||||
);
|
||||
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
return useRecoilCallback(
|
||||
({ set }) =>
|
||||
() => {
|
||||
setSoftFocusPosition(currentTablePosition);
|
||||
|
||||
set(isSoftFocusActiveState, true);
|
||||
|
||||
setHotkeyScope(TableHotkeyScope.TableSoftFocus);
|
||||
},
|
||||
[setHotkeyScope, currentTablePosition, setSoftFocusPosition],
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const isCreateModeScopedState = atomFamily<boolean, string>({
|
||||
key: 'isCreateModeScopedState',
|
||||
default: false,
|
||||
});
|
||||
@ -0,0 +1,29 @@
|
||||
import { InplaceInputDateDisplayMode } from '@/ui/display/component/InplaceInputDateDisplayMode';
|
||||
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
import { EditableCell } from '../components/EditableCell';
|
||||
|
||||
import { EditableCellDateEditMode } from './EditableCellDateEditMode';
|
||||
|
||||
export type EditableDateProps = {
|
||||
value: Date;
|
||||
onChange: (date: Date) => void;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
};
|
||||
|
||||
export function EditableCellDate({
|
||||
value,
|
||||
onChange,
|
||||
editModeHorizontalAlign,
|
||||
}: EditableDateProps) {
|
||||
return (
|
||||
<EditableCell
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={
|
||||
<EditableCellDateEditMode onChange={onChange} value={value} />
|
||||
}
|
||||
nonEditModeContent={<InplaceInputDateDisplayMode value={value} />}
|
||||
editHotkeyScope={{ scope: TableHotkeyScope.CellDateEditMode }}
|
||||
></EditableCell>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
|
||||
import { InplaceInputDate } from '@/ui/inplace-input/components/InplaceInputDate';
|
||||
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
import { useEditableCell } from '../hooks/useEditableCell';
|
||||
|
||||
const EditableCellDateEditModeContainer = styled.div`
|
||||
margin-top: -1px;
|
||||
width: inherit;
|
||||
`;
|
||||
|
||||
export type EditableDateProps = {
|
||||
value: Date;
|
||||
onChange: (date: Date) => void;
|
||||
};
|
||||
|
||||
export function EditableCellDateEditMode({
|
||||
value,
|
||||
onChange,
|
||||
}: EditableDateProps) {
|
||||
const { closeEditableCell } = useEditableCell();
|
||||
|
||||
function handleDateChange(newDate: Date) {
|
||||
onChange(newDate);
|
||||
closeEditableCell();
|
||||
}
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
() => {
|
||||
closeEditableCell();
|
||||
},
|
||||
TableHotkeyScope.CellDateEditMode,
|
||||
[closeEditableCell],
|
||||
);
|
||||
|
||||
return (
|
||||
<EditableCellDateEditModeContainer>
|
||||
<InplaceInputDate onChange={handleDateChange} value={value} />
|
||||
</EditableCellDateEditModeContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
import { ReactElement, useEffect, useState } from 'react';
|
||||
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
import { CellSkeleton } from '../components/CellSkeleton';
|
||||
import { EditableCell } from '../components/EditableCell';
|
||||
|
||||
import { EditableCellDoubleTextEditMode } from './EditableCellDoubleTextEditMode';
|
||||
|
||||
type OwnProps = {
|
||||
firstValue: string;
|
||||
secondValue: string;
|
||||
firstValuePlaceholder: string;
|
||||
secondValuePlaceholder: string;
|
||||
nonEditModeContent: ReactElement;
|
||||
onChange: (firstValue: string, secondValue: string) => void;
|
||||
onSubmit?: () => void;
|
||||
onCancel?: () => void;
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
export function EditableCellDoubleText({
|
||||
firstValue,
|
||||
secondValue,
|
||||
firstValuePlaceholder,
|
||||
secondValuePlaceholder,
|
||||
onChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
nonEditModeContent,
|
||||
loading,
|
||||
}: OwnProps) {
|
||||
const [firstInternalValue, setFirstInternalValue] = useState(firstValue);
|
||||
const [secondInternalValue, setSecondInternalValue] = useState(secondValue);
|
||||
|
||||
useEffect(() => {
|
||||
setFirstInternalValue(firstValue);
|
||||
setSecondInternalValue(secondValue);
|
||||
}, [firstValue, secondValue]);
|
||||
|
||||
function handleOnChange(firstValue: string, secondValue: string): void {
|
||||
setFirstInternalValue(firstValue);
|
||||
setSecondInternalValue(secondValue);
|
||||
onChange(firstValue, secondValue);
|
||||
}
|
||||
|
||||
return (
|
||||
<EditableCell
|
||||
editHotkeyScope={{ scope: TableHotkeyScope.CellDoubleTextInput }}
|
||||
editModeContent={
|
||||
<EditableCellDoubleTextEditMode
|
||||
firstValue={firstInternalValue}
|
||||
secondValue={secondInternalValue}
|
||||
firstValuePlaceholder={firstValuePlaceholder}
|
||||
secondValuePlaceholder={secondValuePlaceholder}
|
||||
onChange={handleOnChange}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
}
|
||||
nonEditModeContent={loading ? <CellSkeleton /> : nonEditModeContent}
|
||||
></EditableCell>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,129 @@
|
||||
import { ChangeEvent, useRef, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
|
||||
import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
|
||||
|
||||
import { useMoveSoftFocus } from '../../hooks/useMoveSoftFocus';
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
import { useEditableCell } from '../hooks/useEditableCell';
|
||||
|
||||
type OwnProps = {
|
||||
firstValue: string;
|
||||
secondValue: string;
|
||||
firstValuePlaceholder: string;
|
||||
secondValuePlaceholder: string;
|
||||
onChange: (firstValue: string, secondValue: string) => void;
|
||||
onSubmit?: () => void;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
& > input:last-child {
|
||||
border-left: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
}
|
||||
`;
|
||||
|
||||
export function EditableCellDoubleTextEditMode({
|
||||
firstValue,
|
||||
secondValue,
|
||||
firstValuePlaceholder,
|
||||
secondValuePlaceholder,
|
||||
onChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: OwnProps) {
|
||||
const [focusPosition, setFocusPosition] = useState<'left' | 'right'>('left');
|
||||
|
||||
const firstValueInputRef = useRef<HTMLInputElement>(null);
|
||||
const secondValueInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { closeEditableCell } = useEditableCell();
|
||||
const { moveRight, moveLeft, moveDown } = useMoveSoftFocus();
|
||||
|
||||
function closeCell() {
|
||||
setFocusPosition('left');
|
||||
closeEditableCell();
|
||||
}
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Enter,
|
||||
() => {
|
||||
closeCell();
|
||||
moveDown();
|
||||
onSubmit?.();
|
||||
},
|
||||
TableHotkeyScope.CellDoubleTextInput,
|
||||
[closeCell],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
() => {
|
||||
onCancel?.();
|
||||
closeCell();
|
||||
},
|
||||
TableHotkeyScope.CellDoubleTextInput,
|
||||
[closeCell],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
'tab',
|
||||
() => {
|
||||
if (focusPosition === 'left') {
|
||||
setFocusPosition('right');
|
||||
secondValueInputRef.current?.focus();
|
||||
} else {
|
||||
onSubmit?.();
|
||||
closeCell();
|
||||
moveRight();
|
||||
}
|
||||
},
|
||||
TableHotkeyScope.CellDoubleTextInput,
|
||||
[closeCell, moveRight, focusPosition],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
'shift+tab',
|
||||
() => {
|
||||
if (focusPosition === 'right') {
|
||||
setFocusPosition('left');
|
||||
firstValueInputRef.current?.focus();
|
||||
} else {
|
||||
onSubmit?.();
|
||||
closeCell();
|
||||
moveLeft();
|
||||
}
|
||||
},
|
||||
TableHotkeyScope.CellDoubleTextInput,
|
||||
[closeCell, moveRight, focusPosition],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<InplaceInputTextEditMode
|
||||
autoFocus
|
||||
placeholder={firstValuePlaceholder}
|
||||
ref={firstValueInputRef}
|
||||
value={firstValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(event.target.value, secondValue);
|
||||
}}
|
||||
/>
|
||||
<InplaceInputTextEditMode
|
||||
placeholder={secondValuePlaceholder}
|
||||
ref={secondValueInputRef}
|
||||
value={secondValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(firstValue, event.target.value);
|
||||
}}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { InplaceInputPhoneDisplayMode } from '@/ui/display/component/InplaceInputPhoneDisplayMode';
|
||||
import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
|
||||
|
||||
import { EditableCell } from '../components/EditableCell';
|
||||
|
||||
type OwnProps = {
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
onChange: (updated: string) => void;
|
||||
onSubmit?: () => void;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
export function EditableCellPhone({
|
||||
value,
|
||||
placeholder,
|
||||
onChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: OwnProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<EditableCell
|
||||
editModeContent={
|
||||
<InplaceInputTextEditMode
|
||||
autoFocus
|
||||
placeholder={placeholder || ''}
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(event.target.value);
|
||||
onChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
nonEditModeContent={<InplaceInputPhoneDisplayMode value={inputValue} />}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const EditableCellRelationCreateButton = styled.button`
|
||||
align-items: center;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-family: 'Inter';
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
height: 31px;
|
||||
padding-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
padding-top: ${({ theme }) => theme.spacing(1)};
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
`;
|
||||
@ -0,0 +1,61 @@
|
||||
import { ChangeEvent, useEffect, useState } from 'react';
|
||||
|
||||
import { InplaceInputTextDisplayMode } from '@/ui/display/component/InplaceInputTextDisplayMode';
|
||||
import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
|
||||
|
||||
import { CellSkeleton } from '../components/CellSkeleton';
|
||||
import { EditableCell } from '../components/EditableCell';
|
||||
|
||||
type OwnProps = {
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
onChange: (newValue: string) => void;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
loading?: boolean;
|
||||
onSubmit?: () => void;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
export function EditableCellText({
|
||||
value,
|
||||
placeholder,
|
||||
onChange,
|
||||
editModeHorizontalAlign,
|
||||
loading,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}: OwnProps) {
|
||||
const [internalValue, setInternalValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalValue(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<EditableCell
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={
|
||||
<InplaceInputTextEditMode
|
||||
placeholder={placeholder || ''}
|
||||
autoFocus
|
||||
value={internalValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInternalValue(event.target.value);
|
||||
onChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
nonEditModeContent={
|
||||
loading ? (
|
||||
<CellSkeleton />
|
||||
) : (
|
||||
<InplaceInputTextDisplayMode>
|
||||
{internalValue}
|
||||
</InplaceInputTextDisplayMode>
|
||||
)
|
||||
}
|
||||
></EditableCell>
|
||||
);
|
||||
}
|
||||
112
front/src/modules/ui/table/editable-cell/types/EditableChip.tsx
Normal file
112
front/src/modules/ui/table/editable-cell/types/EditableChip.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
import {
|
||||
ChangeEvent,
|
||||
ComponentType,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { textInputStyle } from '@/ui/themes/effects';
|
||||
|
||||
import { EditableCell } from '../components/EditableCell';
|
||||
|
||||
export type EditableChipProps = {
|
||||
id: string;
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
picture: string;
|
||||
changeHandler: (updated: string) => void;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
ChipComponent: ComponentType<{
|
||||
id: string;
|
||||
name: string;
|
||||
picture: string;
|
||||
isOverlapped?: boolean;
|
||||
}>;
|
||||
commentThreadCount?: number;
|
||||
onCommentClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
rightEndContents?: ReactNode[];
|
||||
onSubmit?: () => void;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
// TODO: refactor
|
||||
const StyledInplaceInput = styled.input`
|
||||
width: 100%;
|
||||
|
||||
${textInputStyle}
|
||||
`;
|
||||
|
||||
const NoEditModeContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const RightContainer = styled.div`
|
||||
margin-left: ${(props) => props.theme.spacing(1)};
|
||||
`;
|
||||
|
||||
// TODO: move right end content in EditableCell
|
||||
export function EditableCellChip({
|
||||
id,
|
||||
value,
|
||||
placeholder,
|
||||
changeHandler,
|
||||
picture,
|
||||
editModeHorizontalAlign,
|
||||
ChipComponent,
|
||||
rightEndContents,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: EditableChipProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(value);
|
||||
}, [value]);
|
||||
|
||||
const handleRightEndContentClick = (
|
||||
event: React.MouseEvent<HTMLDivElement>,
|
||||
) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<EditableCell
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={
|
||||
<StyledInplaceInput
|
||||
placeholder={placeholder || ''}
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(event.target.value);
|
||||
changeHandler(event.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
nonEditModeContent={
|
||||
<NoEditModeContainer>
|
||||
<ChipComponent id={id} name={inputValue} picture={picture} />
|
||||
<RightContainer>
|
||||
{rightEndContents &&
|
||||
rightEndContents.length > 0 &&
|
||||
rightEndContents.map((content, index) => (
|
||||
<div key={index} onClick={handleRightEndContentClick}>
|
||||
{content}
|
||||
</div>
|
||||
))}
|
||||
</RightContainer>
|
||||
</NoEditModeContainer>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user