Feat/open input not focus (#811)

* Fixed click outside

* Finished

* Fixed tests
This commit is contained in:
Lucas Bordeau
2023-07-22 07:09:02 +02:00
committed by GitHub
parent 0f3f6fa948
commit 62720944fa
15 changed files with 192 additions and 88 deletions

View File

@ -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>
);

View File

@ -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>
);
}

View File

@ -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);
});

View File

@ -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]);
}

View File

@ -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>
) : (

View File

@ -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}
>

View File

@ -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 (

View File

@ -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(

View File

@ -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();
});
},
};

View File

@ -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 {

View File

@ -16,9 +16,12 @@ export function useRegisterCloseCellHandlers(
const { isCurrentCellInEditMode } = useCurrentCellEditMode();
useListenClickOutsideArrayOfRef({
refs: [wrapperRef],
callback: () => {
callback: (event) => {
if (isCurrentCellInEditMode) {
event.stopImmediatePropagation();
onSubmit?.();
closeEditableCell();
}
},

View File

@ -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]);
}

View File

@ -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,
});