Feat/open input not focus (#811)
* Fixed click outside * Finished * Fixed tests
This commit is contained in:
@ -1,14 +1,14 @@
|
||||
import { ReactNode, useRef } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
|
||||
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
|
||||
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
|
||||
import { IconChevronDown } from '@/ui/icon/index';
|
||||
|
||||
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
|
||||
|
||||
import { DropdownMenuContainer } from './DropdownMenuContainer';
|
||||
|
||||
type OwnProps = {
|
||||
label: string;
|
||||
isActive: boolean;
|
||||
@ -50,12 +50,6 @@ const StyledDropdownButton = styled.div<StyledDropdownButtonProps>`
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledDropdownMenuContainer = styled.ul`
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 14px;
|
||||
`;
|
||||
|
||||
const StyledDropdownTopOption = styled.li`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
cursor: pointer;
|
||||
@ -104,12 +98,6 @@ function DropdownButton({
|
||||
onIsUnfoldedChange?.(false);
|
||||
};
|
||||
|
||||
const dropdownRef = useRef(null);
|
||||
useListenClickOutsideArrayOfRef({
|
||||
refs: [dropdownRef],
|
||||
callback: onOutsideClick,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledDropdownButtonContainer>
|
||||
<StyledDropdownButton
|
||||
@ -121,9 +109,9 @@ function DropdownButton({
|
||||
{label}
|
||||
</StyledDropdownButton>
|
||||
{isUnfolded && (
|
||||
<StyledDropdownMenuContainer>
|
||||
<DropdownMenu ref={dropdownRef}>{children}</DropdownMenu>
|
||||
</StyledDropdownMenuContainer>
|
||||
<DropdownMenuContainer onClose={onOutsideClick}>
|
||||
{children}
|
||||
</DropdownMenuContainer>
|
||||
)}
|
||||
</StyledDropdownButtonContainer>
|
||||
);
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
import { useRef } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { DropdownMenu } from '../../dropdown/components/DropdownMenu';
|
||||
import { useListenClickOutsideArrayOfRef } from '../../hooks/useListenClickOutsideArrayOfRef';
|
||||
|
||||
export const StyledDropdownMenuContainer = styled.ul`
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 14px;
|
||||
`;
|
||||
|
||||
export function DropdownMenuContainer({
|
||||
children,
|
||||
onClose,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
useListenClickOutsideArrayOfRef({
|
||||
refs: [dropdownRef],
|
||||
callback: () => {
|
||||
onClose?.();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledDropdownMenuContainer>
|
||||
<DropdownMenu ref={dropdownRef}>{children}</DropdownMenu>
|
||||
</StyledDropdownMenuContainer>
|
||||
);
|
||||
}
|
||||
@ -28,12 +28,12 @@ test('useListenClickOutsideArrayOfRef hook works in dom mode', async () => {
|
||||
const inside2 = getByText('Inside 2');
|
||||
const outside = getByText('Outside');
|
||||
|
||||
fireEvent.mouseUp(inside);
|
||||
fireEvent.click(inside);
|
||||
expect(onOutsideClick).toHaveBeenCalledTimes(0);
|
||||
|
||||
fireEvent.mouseUp(inside2);
|
||||
fireEvent.click(inside2);
|
||||
expect(onOutsideClick).toHaveBeenCalledTimes(0);
|
||||
|
||||
fireEvent.mouseUp(outside);
|
||||
fireEvent.click(outside);
|
||||
expect(onOutsideClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export enum ClickOutsideMode {
|
||||
absolute = 'absolute',
|
||||
dom = 'dom',
|
||||
@ -13,7 +11,7 @@ export function useListenClickOutsideArrayOfRef<T extends Element>({
|
||||
mode = ClickOutsideMode.dom,
|
||||
}: {
|
||||
refs: Array<React.RefObject<T>>;
|
||||
callback: (event?: MouseEvent | TouchEvent) => void;
|
||||
callback: (event: MouseEvent | TouchEvent) => void;
|
||||
mode?: ClickOutsideMode;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
@ -59,16 +57,18 @@ export function useListenClickOutsideArrayOfRef<T extends Element>({
|
||||
}
|
||||
}
|
||||
|
||||
const hasAtLeastOneRefDefined = refs.some((ref) => isDefined(ref.current));
|
||||
|
||||
if (hasAtLeastOneRefDefined) {
|
||||
document.addEventListener('mouseup', handleClickOutside);
|
||||
document.addEventListener('touchend', handleClickOutside);
|
||||
}
|
||||
document.addEventListener('click', handleClickOutside, { capture: true });
|
||||
document.addEventListener('touchend', handleClickOutside, {
|
||||
capture: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mouseup', handleClickOutside);
|
||||
document.removeEventListener('touchend', handleClickOutside);
|
||||
document.removeEventListener('click', handleClickOutside, {
|
||||
capture: true,
|
||||
});
|
||||
document.removeEventListener('touchend', handleClickOutside, {
|
||||
capture: true,
|
||||
});
|
||||
};
|
||||
}, [refs, callback, mode]);
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
|
||||
|
||||
import { useCurrentCellEditMode } from '../hooks/useCurrentCellEditMode';
|
||||
import { useIsSoftFocusOnCurrentCell } from '../hooks/useIsSoftFocusOnCurrentCell';
|
||||
import { useRegisterEditableCell } from '../hooks/useRegisterEditableCell';
|
||||
|
||||
import { EditableCellDisplayMode } from './EditableCellDisplayMode';
|
||||
import { EditableCellEditMode } from './EditableCellEditMode';
|
||||
@ -48,6 +49,8 @@ export function EditableCell({
|
||||
|
||||
const hasSoftFocus = useIsSoftFocusOnCurrentCell();
|
||||
|
||||
useRegisterEditableCell(editHotkeyScope);
|
||||
|
||||
return (
|
||||
<CellBaseContainer>
|
||||
{isCurrentCellInEditMode ? (
|
||||
@ -62,7 +65,7 @@ export function EditableCell({
|
||||
{editModeContent}
|
||||
</EditableCellEditMode>
|
||||
) : hasSoftFocus ? (
|
||||
<EditableCellSoftFocusMode editHotkeyScope={editHotkeyScope}>
|
||||
<EditableCellSoftFocusMode>
|
||||
{nonEditModeContent}
|
||||
</EditableCellSoftFocusMode>
|
||||
) : (
|
||||
|
||||
@ -40,6 +40,11 @@ export function EditableCellDisplayContainer({
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
return (
|
||||
<EditableCellDisplayModeOuterContainer
|
||||
data-testid={
|
||||
softFocus
|
||||
? 'editable-cell-soft-focus-mode'
|
||||
: 'editable-cell-display-mode'
|
||||
}
|
||||
onClick={onClick}
|
||||
softFocus={softFocus}
|
||||
>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useEditableCell } from '../hooks/useEditableCell';
|
||||
import { useSetSoftFocusOnCurrentCell } from '../hooks/useSetSoftFocusOnCurrentCell';
|
||||
|
||||
import { EditableCellDisplayContainer } from './EditableCellContainer';
|
||||
@ -7,8 +8,11 @@ export function EditableCellDisplayMode({
|
||||
}: React.PropsWithChildren<unknown>) {
|
||||
const setSoftFocusOnCurrentCell = useSetSoftFocusOnCurrentCell();
|
||||
|
||||
const { openEditableCell } = useEditableCell();
|
||||
|
||||
function handleClick() {
|
||||
setSoftFocusOnCurrentCell();
|
||||
openEditableCell();
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { PropsWithChildren } 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';
|
||||
@ -9,22 +8,13 @@ import { useEditableCell } from '../hooks/useEditableCell';
|
||||
|
||||
import { EditableCellDisplayContainer } from './EditableCellContainer';
|
||||
|
||||
type OwnProps = PropsWithChildren<{
|
||||
editHotkeyScope?: HotkeyScope;
|
||||
}>;
|
||||
type OwnProps = PropsWithChildren<unknown>;
|
||||
|
||||
export function EditableCellSoftFocusMode({
|
||||
children,
|
||||
editHotkeyScope,
|
||||
}: OwnProps) {
|
||||
export function EditableCellSoftFocusMode({ children }: OwnProps) {
|
||||
const { openEditableCell } = useEditableCell();
|
||||
|
||||
function openEditMode() {
|
||||
openEditableCell(
|
||||
editHotkeyScope ?? {
|
||||
scope: TableHotkeyScope.CellEditMode,
|
||||
},
|
||||
);
|
||||
openEditableCell();
|
||||
}
|
||||
|
||||
useScopedHotkeys(
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { expect } from '@storybook/jest';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { userEvent, within } from '@storybook/testing-library';
|
||||
|
||||
@ -28,9 +29,18 @@ export const SoftFocusMode: Story = {
|
||||
...DisplayMode,
|
||||
play: async ({ canvasElement, step }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await step('Click once', () =>
|
||||
userEvent.click(canvas.getByText('Content')),
|
||||
);
|
||||
|
||||
await step('Escape', () => {
|
||||
userEvent.keyboard('{esc}');
|
||||
});
|
||||
|
||||
await step('Has soft focus mode', () => {
|
||||
expect(canvas.getByTestId('editable-cell-soft-focus-mode')).toBeDefined();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@ -38,9 +48,15 @@ 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);
|
||||
|
||||
await step('Has edit mode', () => {
|
||||
expect(
|
||||
canvas.getByTestId('editable-cell-edit-mode-container'),
|
||||
).toBeDefined();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@ -3,12 +3,20 @@ import { useRecoilCallback } from 'recoil';
|
||||
import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
|
||||
import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
|
||||
|
||||
import { useContextScopeId } from '../../../recoil-scope/hooks/useContextScopeId';
|
||||
import { getSnapshotScopedState } from '../../../recoil-scope/utils/getSnapshotScopedState';
|
||||
import { useCloseCurrentCellInEditMode } from '../../hooks/useClearCellInEditMode';
|
||||
import { CellContext } from '../../states/CellContext';
|
||||
import { isSomeInputInEditModeState } from '../../states/isSomeInputInEditModeState';
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
import { customCellHotkeyScopeScopedState } from '../states/customCellHotkeyScopeScopedState';
|
||||
|
||||
import { useCurrentCellEditMode } from './useCurrentCellEditMode';
|
||||
|
||||
const DEFAULT_CELL_SCOPE: HotkeyScope = {
|
||||
scope: TableHotkeyScope.CellEditMode,
|
||||
};
|
||||
|
||||
export function useEditableCell() {
|
||||
const { setCurrentCellInEditMode } = useCurrentCellEditMode();
|
||||
|
||||
@ -16,6 +24,8 @@ export function useEditableCell() {
|
||||
|
||||
const closeCurrentCellInEditMode = useCloseCurrentCellInEditMode();
|
||||
|
||||
const cellContextId = useContextScopeId(CellContext);
|
||||
|
||||
function closeEditableCell() {
|
||||
closeCurrentCellInEditMode();
|
||||
setHotkeyScope(TableHotkeyScope.TableSoftFocus);
|
||||
@ -23,20 +33,36 @@ export function useEditableCell() {
|
||||
|
||||
const openEditableCell = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(HotkeyScope: HotkeyScope) => {
|
||||
() => {
|
||||
const isSomeInputInEditMode = snapshot
|
||||
.getLoadable(isSomeInputInEditModeState)
|
||||
.valueOrThrow();
|
||||
|
||||
const customCellHotkeyScope = getSnapshotScopedState({
|
||||
snapshot,
|
||||
state: customCellHotkeyScopeScopedState,
|
||||
contextScopeId: cellContextId,
|
||||
});
|
||||
|
||||
if (!isSomeInputInEditMode) {
|
||||
set(isSomeInputInEditModeState, true);
|
||||
|
||||
setCurrentCellInEditMode();
|
||||
|
||||
setHotkeyScope(HotkeyScope.scope);
|
||||
if (customCellHotkeyScope) {
|
||||
setHotkeyScope(
|
||||
customCellHotkeyScope.scope,
|
||||
customCellHotkeyScope.customScopes,
|
||||
);
|
||||
} else {
|
||||
setHotkeyScope(
|
||||
DEFAULT_CELL_SCOPE.scope,
|
||||
DEFAULT_CELL_SCOPE.customScopes,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[setCurrentCellInEditMode, setHotkeyScope],
|
||||
[setCurrentCellInEditMode, setHotkeyScope, cellContextId],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@ -16,9 +16,12 @@ export function useRegisterCloseCellHandlers(
|
||||
const { isCurrentCellInEditMode } = useCurrentCellEditMode();
|
||||
useListenClickOutsideArrayOfRef({
|
||||
refs: [wrapperRef],
|
||||
callback: () => {
|
||||
callback: (event) => {
|
||||
if (isCurrentCellInEditMode) {
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
onSubmit?.();
|
||||
|
||||
closeEditableCell();
|
||||
}
|
||||
},
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
|
||||
|
||||
import { useRecoilScopedState } from '../../../recoil-scope/hooks/useRecoilScopedState';
|
||||
import { CellContext } from '../../states/CellContext';
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
import { customCellHotkeyScopeScopedState } from '../states/customCellHotkeyScopeScopedState';
|
||||
|
||||
const DEFAULT_CELL_SCOPE: HotkeyScope = {
|
||||
scope: TableHotkeyScope.CellEditMode,
|
||||
};
|
||||
|
||||
export function useRegisterEditableCell(cellHotkeyScope?: HotkeyScope) {
|
||||
const [, setCustomCellHotkeyScope] = useRecoilScopedState(
|
||||
customCellHotkeyScopeScopedState,
|
||||
CellContext,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCustomCellHotkeyScope(cellHotkeyScope ?? DEFAULT_CELL_SCOPE);
|
||||
}, [cellHotkeyScope, setCustomCellHotkeyScope]);
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { HotkeyScope } from '../../../hotkey/types/HotkeyScope';
|
||||
|
||||
export const customCellHotkeyScopeScopedState = atomFamily<
|
||||
HotkeyScope | null,
|
||||
string
|
||||
>({
|
||||
key: 'customCellHotkeyScopeScopedState',
|
||||
default: null,
|
||||
});
|
||||
Reference in New Issue
Block a user