Migrate to a monorepo structure (#2909)
This commit is contained in:
@ -0,0 +1,27 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useSelectableListScopedStates } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListScopedStates';
|
||||
|
||||
type SelectableItemProps = {
|
||||
itemId: string;
|
||||
children: React.ReactElement;
|
||||
};
|
||||
|
||||
export const SelectableItem = ({ itemId, children }: SelectableItemProps) => {
|
||||
const { isSelectedItemIdSelector } = useSelectableListScopedStates({
|
||||
itemId: itemId,
|
||||
});
|
||||
|
||||
const isSelectedItemId = useRecoilValue(isSelectedItemIdSelector);
|
||||
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelectedItemId) {
|
||||
scrollRef.current?.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
}, [isSelectedItemId]);
|
||||
|
||||
return <div ref={scrollRef}>{children}</div>;
|
||||
};
|
||||
@ -0,0 +1,49 @@
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useSelectableListHotKeys } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListHotKeys';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { SelectableListScope } from '@/ui/layout/selectable-list/scopes/SelectableListScope';
|
||||
|
||||
type SelectableListProps = {
|
||||
children: ReactNode;
|
||||
selectableListId: string;
|
||||
selectableItemIds: string[][];
|
||||
onSelect?: (selected: string) => void;
|
||||
hotkeyScope: string;
|
||||
onEnter?: (itemId: string) => void;
|
||||
};
|
||||
|
||||
const StyledSelectableItemsContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const SelectableList = ({
|
||||
children,
|
||||
selectableListId,
|
||||
hotkeyScope,
|
||||
selectableItemIds,
|
||||
onEnter,
|
||||
}: SelectableListProps) => {
|
||||
useSelectableListHotKeys(selectableListId, hotkeyScope);
|
||||
|
||||
const { setSelectableItemIds, setSelectableListOnEnter } = useSelectableList({
|
||||
selectableListId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setSelectableListOnEnter(() => onEnter);
|
||||
}, [onEnter, setSelectableListOnEnter]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectableItemIds(selectableItemIds);
|
||||
}, [selectableItemIds, setSelectableItemIds]);
|
||||
|
||||
return (
|
||||
<SelectableListScope selectableListScopeId={selectableListId}>
|
||||
<StyledSelectableItemsContainer>
|
||||
{children}
|
||||
</StyledSelectableItemsContainer>
|
||||
</SelectableListScope>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,160 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { getSelectableListScopedStates } from '@/ui/layout/selectable-list/utils/internal/getSelectableListScopedStates';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
|
||||
type Direction = 'up' | 'down' | 'left' | 'right';
|
||||
|
||||
export const useSelectableListHotKeys = (
|
||||
scopeId: string,
|
||||
hotkeyScope: string,
|
||||
) => {
|
||||
const findPosition = (
|
||||
selectableItemIds: string[][],
|
||||
selectedItemId?: string | null,
|
||||
) => {
|
||||
if (!selectedItemId) {
|
||||
// If nothing is selected, return the default position
|
||||
return { row: 0, col: 0 };
|
||||
}
|
||||
|
||||
for (let row = 0; row < selectableItemIds.length; row++) {
|
||||
const col = selectableItemIds[row].indexOf(selectedItemId);
|
||||
if (col !== -1) {
|
||||
return { row, col };
|
||||
}
|
||||
}
|
||||
return { row: 0, col: 0 };
|
||||
};
|
||||
|
||||
const handleSelect = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(direction: Direction) => {
|
||||
const { selectedItemIdState, selectableItemIdsState } =
|
||||
getSelectableListScopedStates({
|
||||
selectableListScopeId: scopeId,
|
||||
});
|
||||
const selectedItemId = getSnapshotValue(snapshot, selectedItemIdState);
|
||||
const selectableItemIds = getSnapshotValue(
|
||||
snapshot,
|
||||
selectableItemIdsState,
|
||||
);
|
||||
|
||||
const { row: currentRow, col: currentCol } = findPosition(
|
||||
selectableItemIds,
|
||||
selectedItemId,
|
||||
);
|
||||
|
||||
const computeNextId = (direction: Direction) => {
|
||||
if (selectableItemIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isSingleRow = selectableItemIds.length === 1;
|
||||
|
||||
let nextRow: number;
|
||||
let nextCol: number;
|
||||
|
||||
switch (direction) {
|
||||
case 'up':
|
||||
nextRow = isSingleRow ? currentRow : Math.max(0, currentRow - 1);
|
||||
nextCol = isSingleRow ? Math.max(0, currentCol - 1) : currentCol;
|
||||
break;
|
||||
case 'down':
|
||||
nextRow = isSingleRow
|
||||
? currentRow
|
||||
: Math.min(selectableItemIds.length - 1, currentRow + 1);
|
||||
nextCol = isSingleRow
|
||||
? Math.min(
|
||||
selectableItemIds[currentRow].length - 1,
|
||||
currentCol + 1,
|
||||
)
|
||||
: currentCol;
|
||||
break;
|
||||
case 'left':
|
||||
nextRow = currentRow;
|
||||
nextCol = Math.max(0, currentCol - 1);
|
||||
break;
|
||||
case 'right':
|
||||
nextRow = currentRow;
|
||||
nextCol = Math.min(
|
||||
selectableItemIds[currentRow].length - 1,
|
||||
currentCol + 1,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
nextRow = currentRow;
|
||||
nextCol = currentCol;
|
||||
}
|
||||
|
||||
return selectableItemIds[nextRow][nextCol];
|
||||
};
|
||||
|
||||
const nextId = computeNextId(direction);
|
||||
|
||||
if (nextId) {
|
||||
const { isSelectedItemIdSelector } = getSelectableListScopedStates({
|
||||
selectableListScopeId: scopeId,
|
||||
itemId: nextId,
|
||||
});
|
||||
set(isSelectedItemIdSelector, true);
|
||||
set(selectedItemIdState, nextId);
|
||||
}
|
||||
|
||||
if (selectedItemId) {
|
||||
const { isSelectedItemIdSelector } = getSelectableListScopedStates({
|
||||
selectableListScopeId: scopeId,
|
||||
itemId: selectedItemId,
|
||||
});
|
||||
set(isSelectedItemIdSelector, false);
|
||||
}
|
||||
},
|
||||
[scopeId],
|
||||
);
|
||||
|
||||
useScopedHotkeys(Key.ArrowUp, () => handleSelect('up'), hotkeyScope, []);
|
||||
|
||||
useScopedHotkeys(Key.ArrowDown, () => handleSelect('down'), hotkeyScope, []);
|
||||
|
||||
useScopedHotkeys(Key.ArrowLeft, () => handleSelect('left'), hotkeyScope, []);
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.ArrowRight,
|
||||
() => handleSelect('right'),
|
||||
hotkeyScope,
|
||||
[],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Enter,
|
||||
useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
() => {
|
||||
const { selectedItemIdState, selectableListOnEnterState } =
|
||||
getSelectableListScopedStates({
|
||||
selectableListScopeId: scopeId,
|
||||
});
|
||||
const selectedItemId = getSnapshotValue(
|
||||
snapshot,
|
||||
selectedItemIdState,
|
||||
);
|
||||
|
||||
const onEnter = getSnapshotValue(
|
||||
snapshot,
|
||||
selectableListOnEnterState,
|
||||
);
|
||||
|
||||
if (selectedItemId) {
|
||||
onEnter?.(selectedItemId);
|
||||
}
|
||||
},
|
||||
[scopeId],
|
||||
),
|
||||
hotkeyScope,
|
||||
[],
|
||||
);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -0,0 +1,36 @@
|
||||
import { SelectableListScopeInternalContext } from '@/ui/layout/selectable-list/scopes/scope-internal-context/SelectableListScopeInternalContext';
|
||||
import { getSelectableListScopedStates } from '@/ui/layout/selectable-list/utils/internal/getSelectableListScopedStates';
|
||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||
|
||||
type UseSelectableListScopedStatesProps = {
|
||||
selectableListScopeId?: string;
|
||||
itemId?: string;
|
||||
};
|
||||
|
||||
export const useSelectableListScopedStates = (
|
||||
args?: UseSelectableListScopedStatesProps,
|
||||
) => {
|
||||
const { selectableListScopeId, itemId } = args ?? {};
|
||||
const scopeId = useAvailableScopeIdOrThrow(
|
||||
SelectableListScopeInternalContext,
|
||||
selectableListScopeId,
|
||||
);
|
||||
|
||||
const {
|
||||
selectedItemIdState,
|
||||
selectableItemIdsState,
|
||||
isSelectedItemIdSelector,
|
||||
selectableListOnEnterState,
|
||||
} = getSelectableListScopedStates({
|
||||
selectableListScopeId: scopeId,
|
||||
itemId: itemId,
|
||||
});
|
||||
|
||||
return {
|
||||
scopeId,
|
||||
isSelectedItemIdSelector,
|
||||
selectableItemIdsState,
|
||||
selectedItemIdState,
|
||||
selectableListOnEnterState,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,40 @@
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useSelectableListScopedStates } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListScopedStates';
|
||||
import { SelectableListScopeInternalContext } from '@/ui/layout/selectable-list/scopes/scope-internal-context/SelectableListScopeInternalContext';
|
||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||
|
||||
type UseSelectableListProps = {
|
||||
selectableListId?: string;
|
||||
itemId?: string;
|
||||
};
|
||||
|
||||
export const useSelectableList = (props?: UseSelectableListProps) => {
|
||||
const scopeId = useAvailableScopeIdOrThrow(
|
||||
SelectableListScopeInternalContext,
|
||||
props?.selectableListId,
|
||||
);
|
||||
|
||||
const {
|
||||
selectableItemIdsState,
|
||||
isSelectedItemIdSelector,
|
||||
selectableListOnEnterState,
|
||||
} = useSelectableListScopedStates({
|
||||
selectableListScopeId: scopeId,
|
||||
itemId: props?.itemId,
|
||||
});
|
||||
|
||||
const setSelectableItemIds = useSetRecoilState(selectableItemIdsState);
|
||||
const setSelectableListOnEnter = useSetRecoilState(
|
||||
selectableListOnEnterState,
|
||||
);
|
||||
const isSelectedItemId = useRecoilValue(isSelectedItemIdSelector);
|
||||
|
||||
return {
|
||||
setSelectableItemIds,
|
||||
isSelectedItemId,
|
||||
setSelectableListOnEnter,
|
||||
selectableListId: scopeId,
|
||||
isSelectedItemIdSelector,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,21 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { SelectableListScopeInternalContext } from './scope-internal-context/SelectableListScopeInternalContext';
|
||||
|
||||
type SelectableListScopeProps = {
|
||||
children: ReactNode;
|
||||
selectableListScopeId: string;
|
||||
};
|
||||
|
||||
export const SelectableListScope = ({
|
||||
children,
|
||||
selectableListScopeId,
|
||||
}: SelectableListScopeProps) => {
|
||||
return (
|
||||
<SelectableListScopeInternalContext.Provider
|
||||
value={{ scopeId: selectableListScopeId }}
|
||||
>
|
||||
{children}
|
||||
</SelectableListScopeInternalContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,7 @@
|
||||
import { ScopedStateKey } from '@/ui/utilities/recoil-scope/scopes-internal/types/ScopedStateKey';
|
||||
import { createScopeInternalContext } from '@/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext';
|
||||
|
||||
type SelectableListScopeInternalContextProps = ScopedStateKey;
|
||||
|
||||
export const SelectableListScopeInternalContext =
|
||||
createScopeInternalContext<SelectableListScopeInternalContextProps>();
|
||||
@ -0,0 +1,9 @@
|
||||
import { createScopedFamilyState } from '@/ui/utilities/recoil-scope/utils/createScopedFamilyState';
|
||||
|
||||
export const isSelectedItemIdMapScopedFamilyState = createScopedFamilyState<
|
||||
boolean,
|
||||
string
|
||||
>({
|
||||
key: 'isSelectedItemIdMapScopedFamilyState',
|
||||
defaultValue: false,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const selectableItemIdsScopedState = createScopedState<string[][]>({
|
||||
key: 'selectableItemIdsScopedState',
|
||||
defaultValue: [[]],
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const selectableListOnEnterScopedState = createScopedState<
|
||||
((itemId: string) => void) | undefined
|
||||
>({
|
||||
key: 'selectableListOnEnterScopedState',
|
||||
defaultValue: undefined,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
|
||||
|
||||
export const selectedItemIdScopedState = createScopedState<string | null>({
|
||||
key: 'selectedItemIdScopedState',
|
||||
defaultValue: null,
|
||||
});
|
||||
@ -0,0 +1,26 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { isSelectedItemIdMapScopedFamilyState } from '@/ui/layout/selectable-list/states/isSelectedItemIdMapScopedFamilyState';
|
||||
|
||||
export const isSelectedItemIdScopedFamilySelector = selectorFamily({
|
||||
key: 'isSelectedItemIdScopedFamilySelector',
|
||||
get:
|
||||
({ scopeId, itemId }: { scopeId: string; itemId: string }) =>
|
||||
({ get }) =>
|
||||
get(
|
||||
isSelectedItemIdMapScopedFamilyState({
|
||||
scopeId: scopeId,
|
||||
familyKey: itemId,
|
||||
}),
|
||||
),
|
||||
set:
|
||||
({ scopeId, itemId }: { scopeId: string; itemId: string }) =>
|
||||
({ set }, newValue) =>
|
||||
set(
|
||||
isSelectedItemIdMapScopedFamilyState({
|
||||
scopeId: scopeId,
|
||||
familyKey: itemId,
|
||||
}),
|
||||
newValue,
|
||||
),
|
||||
});
|
||||
@ -0,0 +1,42 @@
|
||||
import { selectableItemIdsScopedState } from '@/ui/layout/selectable-list/states/selectableItemIdsScopedState';
|
||||
import { selectableListOnEnterScopedState } from '@/ui/layout/selectable-list/states/selectableListOnEnterScopedState';
|
||||
import { selectedItemIdScopedState } from '@/ui/layout/selectable-list/states/selectedItemIdScopedState';
|
||||
import { isSelectedItemIdScopedFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdScopedFamilySelector';
|
||||
import { getScopedState } from '@/ui/utilities/recoil-scope/utils/getScopedState';
|
||||
|
||||
const UNDEFINED_SELECTABLE_ITEM_ID = 'UNDEFINED_SELECTABLE_ITEM_ID';
|
||||
|
||||
export const getSelectableListScopedStates = ({
|
||||
selectableListScopeId,
|
||||
itemId,
|
||||
}: {
|
||||
selectableListScopeId: string;
|
||||
itemId?: string;
|
||||
}) => {
|
||||
const isSelectedItemIdSelector = isSelectedItemIdScopedFamilySelector({
|
||||
scopeId: selectableListScopeId,
|
||||
itemId: itemId ?? UNDEFINED_SELECTABLE_ITEM_ID,
|
||||
});
|
||||
|
||||
const selectedItemIdState = getScopedState(
|
||||
selectedItemIdScopedState,
|
||||
selectableListScopeId,
|
||||
);
|
||||
|
||||
const selectableItemIdsState = getScopedState(
|
||||
selectableItemIdsScopedState,
|
||||
selectableListScopeId,
|
||||
);
|
||||
|
||||
const selectableListOnEnterState = getScopedState(
|
||||
selectableListOnEnterScopedState,
|
||||
selectableListScopeId,
|
||||
);
|
||||
|
||||
return {
|
||||
isSelectedItemIdSelector,
|
||||
selectableItemIdsState,
|
||||
selectedItemIdState,
|
||||
selectableListOnEnterState,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user