Replace hotkey scopes by focus stack (Part 1 - Dropdowns and Side Panel) (#12673)
This PR is the first part of a refactoring aiming to deprecate the hotkey scopes api in favor of the new focus stack api which is more robust. The refactored components in this PR are the dropdowns and the side panel/command menu. - Replaced `useScopedHotkeys` by `useHotkeysOnFocusedElement` for all dropdown components, selectable lists and the command menu - Introduced `focusId` for all dropdowns and created a common hotkey scope `DropdownHotkeyScope` for backward compatibility - Replaced `setHotkeyScopeAndMemorizePreviousScope` occurrences with `usePushFocusItemToFocusStack` and `goBackToPreviousHotkeyScope` with `removeFocusItemFromFocusStack` Note: Test that the shorcuts and arrow key navigation still work properly when interacting with dropdowns and the command menu. Bugs that I have spotted during the QA but which are already present on main: - Icon picker select with arrow keys doesn’t work inside dropdowns - Some dropdowns are not selectable with arrow keys (no selectable list) - Dropdowns in dropdowns don’t reset the hotkey scope correctly when closing - The table click outside is not triggered after closing a table cell and clicking outside of the table
This commit is contained in:
@@ -4,11 +4,10 @@ import { DROPDOWN_RESIZE_MIN_HEIGHT } from '@/ui/layout/dropdown/constants/Dropd
|
||||
import { DROPDOWN_RESIZE_MIN_WIDTH } from '@/ui/layout/dropdown/constants/DropdownResizeMinWidth';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext';
|
||||
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||
import { dropdownHotkeyComponentState } from '@/ui/layout/dropdown/states/dropdownHotkeyComponentState';
|
||||
import { dropdownMaxHeightComponentState } from '@/ui/layout/dropdown/states/internal/dropdownMaxHeightComponentState';
|
||||
import { dropdownMaxWidthComponentState } from '@/ui/layout/dropdown/states/internal/dropdownMaxWidthComponentState';
|
||||
import { DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
@@ -47,9 +46,8 @@ export type DropdownProps = {
|
||||
dropdownComponents: ReactNode;
|
||||
hotkey?: {
|
||||
key: Keys;
|
||||
scope: string;
|
||||
};
|
||||
dropdownHotkeyScope: HotkeyScope;
|
||||
globalHotkeysConfig?: Partial<GlobalHotkeysConfig>;
|
||||
dropdownId: string;
|
||||
dropdownPlacement?: Placement;
|
||||
dropdownOffset?: DropdownOffset;
|
||||
@@ -66,7 +64,7 @@ export const Dropdown = ({
|
||||
dropdownComponents,
|
||||
hotkey,
|
||||
dropdownId,
|
||||
dropdownHotkeyScope,
|
||||
globalHotkeysConfig,
|
||||
dropdownPlacement = 'bottom-end',
|
||||
dropdownStrategy = 'absolute',
|
||||
dropdownOffset,
|
||||
@@ -145,21 +143,14 @@ export const Dropdown = ({
|
||||
});
|
||||
|
||||
const handleClickableComponentClick = useRecoilCallback(
|
||||
({ set }) =>
|
||||
async (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
() => async (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
// TODO: refactor this when we have finished dropdown refactor with state and V1 + V2
|
||||
set(
|
||||
dropdownHotkeyComponentState({ scopeId: dropdownId }),
|
||||
dropdownHotkeyScope,
|
||||
);
|
||||
|
||||
toggleDropdown(dropdownHotkeyScope);
|
||||
onClickOutside?.();
|
||||
},
|
||||
[dropdownId, dropdownHotkeyScope, onClickOutside, toggleDropdown],
|
||||
toggleDropdown(globalHotkeysConfig);
|
||||
onClickOutside?.();
|
||||
},
|
||||
[globalHotkeysConfig, onClickOutside, toggleDropdown],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -190,10 +181,9 @@ export const Dropdown = ({
|
||||
dropdownId={dropdownId}
|
||||
dropdownPlacement={placement}
|
||||
floatingUiRefs={refs}
|
||||
hotkeyScope={dropdownHotkeyScope}
|
||||
hotkey={hotkey}
|
||||
onClickOutside={onClickOutside}
|
||||
onHotkeyTriggered={toggleDropdown}
|
||||
onHotkeyTriggered={onOpen}
|
||||
excludedClickOutsideIds={excludedClickOutsideIds}
|
||||
isDropdownInModal={isDropdownInModal}
|
||||
/>
|
||||
|
||||
+3
-7
@@ -1,7 +1,6 @@
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownMenuHotkeyScope';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
@@ -72,12 +71,9 @@ export const DropdownMenuInnerSelect = ({
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
dropdownHotkeyScope={{
|
||||
scope: DropdownMenuHotkeyScope.InnerSelect,
|
||||
customScopes: {
|
||||
commandMenu: false,
|
||||
commandMenuOpen: false,
|
||||
},
|
||||
globalHotkeysConfig={{
|
||||
enableGlobalHotkeysWithModifiers: false,
|
||||
enableGlobalHotkeysConflictingWithKeyboard: false,
|
||||
}}
|
||||
dropdownId={dropdownId}
|
||||
dropdownOffset={{
|
||||
|
||||
-3
@@ -39,13 +39,11 @@ const meta: Meta<typeof Dropdown> = {
|
||||
decorators: [ComponentDecorator, (Story) => <Story />],
|
||||
args: {
|
||||
clickableComponent: <Button title="Open Dropdown" />,
|
||||
dropdownHotkeyScope: { scope: 'testDropdownMenu' },
|
||||
dropdownOffset: { x: 0, y: 8 },
|
||||
dropdownId: 'test-dropdown-id',
|
||||
},
|
||||
argTypes: {
|
||||
clickableComponent: { control: false },
|
||||
dropdownHotkeyScope: { control: false },
|
||||
dropdownOffset: { control: false },
|
||||
dropdownComponents: { control: false },
|
||||
},
|
||||
@@ -352,7 +350,6 @@ const ModalWithDropdown = () => {
|
||||
title="Open Dropdown in Modal"
|
||||
/>
|
||||
}
|
||||
dropdownHotkeyScope={{ scope: 'modal-dropdown' }}
|
||||
dropdownOffset={{ x: 0, y: 8 }}
|
||||
dropdownId="modal-dropdown-test"
|
||||
isDropdownInModal={true}
|
||||
|
||||
-2
@@ -1,6 +1,5 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { SelectHotkeyScope } from '@/ui/input/types/SelectHotkeyScope';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
@@ -63,7 +62,6 @@ export const ContextDropdownAndAvatar: Story = {
|
||||
EndComponent: (
|
||||
<Dropdown
|
||||
dropdownId={'story-dropdown-id-context-menu'}
|
||||
dropdownHotkeyScope={{ scope: SelectHotkeyScope.Select }}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
|
||||
+19
-18
@@ -1,13 +1,12 @@
|
||||
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
|
||||
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { useInternalHotkeyScopeManagement } from '@/ui/layout/dropdown/hooks/useInternalHotkeyScopeManagement';
|
||||
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
|
||||
import { dropdownMaxHeightComponentState } from '@/ui/layout/dropdown/states/internal/dropdownMaxHeightComponentState';
|
||||
import { dropdownMaxWidthComponentState } from '@/ui/layout/dropdown/states/internal/dropdownMaxWidthComponentState';
|
||||
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
||||
import { HotkeyEffect } from '@/ui/utilities/hotkey/components/HotkeyEffect';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
@@ -45,11 +44,9 @@ export type DropdownInternalContainerProps = {
|
||||
dropdownPlacement: Placement;
|
||||
floatingUiRefs: UseFloatingReturn['refs'];
|
||||
onClickOutside?: () => void;
|
||||
hotkeyScope: HotkeyScope;
|
||||
floatingStyles: UseFloatingReturn['floatingStyles'];
|
||||
hotkey?: {
|
||||
key: Keys;
|
||||
scope: string;
|
||||
};
|
||||
onHotkeyTriggered?: () => void;
|
||||
dropdownComponents: React.ReactNode;
|
||||
@@ -63,7 +60,6 @@ export const DropdownInternalContainer = ({
|
||||
dropdownPlacement,
|
||||
floatingUiRefs,
|
||||
onClickOutside,
|
||||
hotkeyScope,
|
||||
floatingStyles,
|
||||
hotkey,
|
||||
onHotkeyTriggered,
|
||||
@@ -108,23 +104,24 @@ export const DropdownInternalContainer = ({
|
||||
excludedClickOutsideIds,
|
||||
});
|
||||
|
||||
useInternalHotkeyScopeManagement({
|
||||
dropdownScopeId: dropdownId,
|
||||
dropdownHotkeyScopeFromParent: hotkeyScope,
|
||||
});
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Escape],
|
||||
() => {
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: [Key.Escape],
|
||||
callback: () => {
|
||||
if (activeDropdownFocusId !== dropdownId) return;
|
||||
|
||||
if (isDropdownOpen) {
|
||||
closeDropdown();
|
||||
}
|
||||
},
|
||||
hotkeyScope?.scope,
|
||||
[closeDropdown, isDropdownOpen],
|
||||
);
|
||||
focusId: dropdownId,
|
||||
scope: DropdownHotkeyScope.Dropdown,
|
||||
dependencies: [
|
||||
closeDropdown,
|
||||
isDropdownOpen,
|
||||
activeDropdownFocusId,
|
||||
dropdownId,
|
||||
],
|
||||
});
|
||||
|
||||
const dropdownMenuStyles = {
|
||||
...floatingStyles,
|
||||
@@ -137,7 +134,11 @@ export const DropdownInternalContainer = ({
|
||||
return (
|
||||
<>
|
||||
{hotkey && onHotkeyTriggered && (
|
||||
<HotkeyEffect hotkey={hotkey} onHotkeyTriggered={onHotkeyTriggered} />
|
||||
<HotkeyEffect
|
||||
hotkey={hotkey}
|
||||
onHotkeyTriggered={onHotkeyTriggered}
|
||||
focusId={dropdownId}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FloatingPortal>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum DropdownHotkeyScope {
|
||||
Dropdown = 'dropdown',
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
export enum DropdownMenuHotkeyScope {
|
||||
InnerSelect = 'dropdown-menu-inner-select',
|
||||
}
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
import { expect } from '@storybook/test';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
|
||||
import { useDropdownStates } from '@/ui/layout/dropdown/hooks/internal/useDropdownStates';
|
||||
import { useInternalHotkeyScopeManagement } from '@/ui/layout/dropdown/hooks/useInternalHotkeyScopeManagement';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
|
||||
const dropdownScopeId = 'test-dropdown-id-scope';
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return <RecoilRoot>{children}</RecoilRoot>;
|
||||
};
|
||||
|
||||
describe('useInternalHotkeyScopeManagement', () => {
|
||||
it('should update dropdownHotkeyScope', async () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({
|
||||
dropdownHotkeyScopeFromParent,
|
||||
}: {
|
||||
dropdownHotkeyScopeFromParent?: HotkeyScope;
|
||||
}) => {
|
||||
useInternalHotkeyScopeManagement({
|
||||
dropdownScopeId,
|
||||
dropdownHotkeyScopeFromParent,
|
||||
});
|
||||
const { dropdownHotkeyScopeState } = useDropdownStates({
|
||||
dropdownScopeId,
|
||||
});
|
||||
const dropdownHotkeyScope = useRecoilValue(dropdownHotkeyScopeState);
|
||||
return { dropdownHotkeyScope };
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
initialProps: {},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current.dropdownHotkeyScope).toBeNull();
|
||||
|
||||
const scopeFromParent = { scope: 'customScope' };
|
||||
|
||||
rerender({ dropdownHotkeyScopeFromParent: scopeFromParent });
|
||||
|
||||
expect(result.current.dropdownHotkeyScope).toEqual(scopeFromParent);
|
||||
});
|
||||
});
|
||||
-5
@@ -1,5 +1,4 @@
|
||||
import { DropdownScopeInternalContext } from '@/ui/layout/dropdown/scopes/scope-internal-context/DropdownScopeInternalContext';
|
||||
import { dropdownHotkeyComponentState } from '@/ui/layout/dropdown/states/dropdownHotkeyComponentState';
|
||||
import { dropdownPlacementComponentState } from '@/ui/layout/dropdown/states/dropdownPlacementComponentState';
|
||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||
@@ -23,10 +22,6 @@ export const useDropdownStates = ({
|
||||
dropdownPlacementComponentState,
|
||||
scopeId,
|
||||
),
|
||||
dropdownHotkeyScopeState: extractComponentState(
|
||||
dropdownHotkeyComponentState,
|
||||
scopeId,
|
||||
),
|
||||
isDropdownOpenState: extractComponentState(
|
||||
isDropdownOpenComponentState,
|
||||
scopeId,
|
||||
|
||||
+12
-6
@@ -1,15 +1,15 @@
|
||||
import { useCloseDropdownFromOutside } from '@/ui/layout/dropdown/hooks/useCloseDropdownFromOutside';
|
||||
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
|
||||
import { previousDropdownFocusIdState } from '@/ui/layout/dropdown/states/previousDropdownFocusIdState';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useRemoveFocusItemFromFocusStack } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStack';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useCloseAnyOpenDropdown = () => {
|
||||
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
|
||||
|
||||
const { closeDropdownFromOutside } = useCloseDropdownFromOutside();
|
||||
|
||||
const { removeFocusItemFromFocusStack } = useRemoveFocusItemFromFocusStack();
|
||||
|
||||
const closeAnyOpenDropdown = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
() => {
|
||||
@@ -33,18 +33,24 @@ export const useCloseAnyOpenDropdown = () => {
|
||||
|
||||
if (isDefined(activeDropdownFocusId)) {
|
||||
closeDropdownFromOutside(activeDropdownFocusId);
|
||||
removeFocusItemFromFocusStack({
|
||||
focusId: activeDropdownFocusId,
|
||||
memoizeKey: 'global',
|
||||
});
|
||||
}
|
||||
|
||||
if (thereIsOneNestedDropdownOpen) {
|
||||
closeDropdownFromOutside(previousDropdownFocusId);
|
||||
removeFocusItemFromFocusStack({
|
||||
focusId: previousDropdownFocusId,
|
||||
memoizeKey: 'global',
|
||||
});
|
||||
}
|
||||
|
||||
set(previousDropdownFocusIdState, null);
|
||||
set(activeDropdownFocusIdState, null);
|
||||
|
||||
goBackToPreviousHotkeyScope();
|
||||
},
|
||||
[closeDropdownFromOutside, goBackToPreviousHotkeyScope],
|
||||
[closeDropdownFromOutside, removeFocusItemFromFocusStack],
|
||||
);
|
||||
|
||||
return { closeAnyOpenDropdown };
|
||||
|
||||
@@ -3,18 +3,19 @@ import { useRecoilCallback, useRecoilState } from 'recoil';
|
||||
import { useDropdownStates } from '@/ui/layout/dropdown/hooks/internal/useDropdownStates';
|
||||
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
|
||||
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
|
||||
import { dropdownHotkeyComponentState } from '@/ui/layout/dropdown/states/dropdownHotkeyComponentState';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
import { useRemoveFocusItemFromFocusStack } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStack';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useDropdown = (dropdownId?: string) => {
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const { removeFocusItemFromFocusStack } = useRemoveFocusItemFromFocusStack();
|
||||
|
||||
const { scopeId, isDropdownOpenState, dropdownPlacementState } =
|
||||
useDropdownStates({
|
||||
dropdownScopeId: dropdownId,
|
||||
});
|
||||
useDropdownStates({ dropdownScopeId: dropdownId });
|
||||
|
||||
const { setActiveDropdownFocusIdAndMemorizePrevious } =
|
||||
useSetActiveDropdownFocusIdAndMemorizePrevious();
|
||||
@@ -22,71 +23,67 @@ export const useDropdown = (dropdownId?: string) => {
|
||||
const { goBackToPreviousDropdownFocusId } =
|
||||
useGoBackToPreviousDropdownFocusId();
|
||||
|
||||
const {
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
goBackToPreviousHotkeyScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
const [isDropdownOpen, setIsDropdownOpen] =
|
||||
useRecoilState(isDropdownOpenState);
|
||||
|
||||
const [dropdownPlacement, setDropdownPlacement] = useRecoilState(
|
||||
dropdownPlacementState,
|
||||
);
|
||||
|
||||
const [isDropdownOpen, setIsDropdownOpen] =
|
||||
useRecoilState(isDropdownOpenState);
|
||||
|
||||
const closeDropdown = useCallback(() => {
|
||||
if (isDropdownOpen) {
|
||||
goBackToPreviousHotkeyScope();
|
||||
setIsDropdownOpen(false);
|
||||
goBackToPreviousDropdownFocusId();
|
||||
removeFocusItemFromFocusStack({
|
||||
focusId: dropdownId ?? scopeId,
|
||||
memoizeKey: 'global',
|
||||
});
|
||||
}
|
||||
}, [
|
||||
isDropdownOpen,
|
||||
goBackToPreviousHotkeyScope,
|
||||
setIsDropdownOpen,
|
||||
goBackToPreviousDropdownFocusId,
|
||||
removeFocusItemFromFocusStack,
|
||||
dropdownId,
|
||||
scopeId,
|
||||
]);
|
||||
|
||||
const openDropdown = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(dropdownHotkeyScopeFromProps?: HotkeyScope) => {
|
||||
if (!isDropdownOpen) {
|
||||
setIsDropdownOpen(true);
|
||||
setActiveDropdownFocusIdAndMemorizePrevious(dropdownId ?? scopeId);
|
||||
() => (globalHotkeysConfig?: Partial<GlobalHotkeysConfig>) => {
|
||||
if (!isDropdownOpen) {
|
||||
setIsDropdownOpen(true);
|
||||
setActiveDropdownFocusIdAndMemorizePrevious(dropdownId ?? scopeId);
|
||||
|
||||
const dropdownHotkeyScope = getSnapshotValue(
|
||||
snapshot,
|
||||
dropdownHotkeyComponentState({
|
||||
scopeId: dropdownId ?? scopeId,
|
||||
}),
|
||||
);
|
||||
|
||||
const dropdownHotkeyScopeForOpening =
|
||||
dropdownHotkeyScopeFromProps ?? dropdownHotkeyScope;
|
||||
|
||||
if (isDefined(dropdownHotkeyScopeForOpening)) {
|
||||
setHotkeyScopeAndMemorizePreviousScope({
|
||||
scope: dropdownHotkeyScopeForOpening.scope,
|
||||
customScopes: dropdownHotkeyScopeForOpening.customScopes,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
pushFocusItemToFocusStack({
|
||||
focusId: dropdownId ?? scopeId,
|
||||
component: {
|
||||
type: FocusComponentType.DROPDOWN,
|
||||
instanceId: dropdownId ?? scopeId,
|
||||
},
|
||||
globalHotkeysConfig,
|
||||
// TODO: Remove this once we've fully migrated away from hotkey scopes
|
||||
hotkeyScope: { scope: 'dropdown' } as HotkeyScope,
|
||||
memoizeKey: 'global',
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
isDropdownOpen,
|
||||
setIsDropdownOpen,
|
||||
setActiveDropdownFocusIdAndMemorizePrevious,
|
||||
pushFocusItemToFocusStack,
|
||||
dropdownId,
|
||||
scopeId,
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
],
|
||||
);
|
||||
|
||||
const toggleDropdown = (dropdownHotkeyScopeFromProps?: HotkeyScope) => {
|
||||
const toggleDropdown = (
|
||||
globalHotkeysConfig?: Partial<GlobalHotkeysConfig>,
|
||||
) => {
|
||||
if (isDropdownOpen) {
|
||||
closeDropdown();
|
||||
} else {
|
||||
openDropdown(dropdownHotkeyScopeFromProps);
|
||||
openDropdown(globalHotkeysConfig);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
|
||||
import { dropdownHotkeyComponentState } from '@/ui/layout/dropdown/states/dropdownHotkeyComponentState';
|
||||
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
|
||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
import { useRemoveFocusItemFromFocusStack } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStack';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useDropdownV2 = () => {
|
||||
const {
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
goBackToPreviousHotkeyScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
|
||||
const { goBackToPreviousDropdownFocusId } =
|
||||
useGoBackToPreviousDropdownFocusId();
|
||||
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
|
||||
const { removeFocusItemFromFocusStack } = useRemoveFocusItemFromFocusStack();
|
||||
|
||||
const { setActiveDropdownFocusIdAndMemorizePrevious } =
|
||||
useSetActiveDropdownFocusIdAndMemorizePrevious();
|
||||
|
||||
const closeDropdown = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(specificComponentId: string) => {
|
||||
@@ -26,7 +30,10 @@ export const useDropdownV2 = () => {
|
||||
.getValue();
|
||||
|
||||
if (isDropdownOpen) {
|
||||
goBackToPreviousHotkeyScope();
|
||||
removeFocusItemFromFocusStack({
|
||||
focusId: scopeId,
|
||||
memoizeKey: 'global',
|
||||
});
|
||||
goBackToPreviousDropdownFocusId();
|
||||
set(
|
||||
isDropdownOpenComponentState({
|
||||
@@ -36,18 +43,17 @@ export const useDropdownV2 = () => {
|
||||
);
|
||||
}
|
||||
},
|
||||
[goBackToPreviousHotkeyScope, goBackToPreviousDropdownFocusId],
|
||||
[removeFocusItemFromFocusStack, goBackToPreviousDropdownFocusId],
|
||||
);
|
||||
|
||||
const openDropdown = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(specificComponentId: string, customHotkeyScope?: HotkeyScope) => {
|
||||
({ set }) =>
|
||||
(
|
||||
specificComponentId: string,
|
||||
globalHotkeysConfig?: Partial<GlobalHotkeysConfig>,
|
||||
) => {
|
||||
const scopeId = specificComponentId;
|
||||
|
||||
const dropdownHotkeyScope = snapshot
|
||||
.getLoadable(dropdownHotkeyComponentState({ scopeId }))
|
||||
.getValue();
|
||||
|
||||
set(
|
||||
isDropdownOpenComponentState({
|
||||
scopeId,
|
||||
@@ -55,24 +61,29 @@ export const useDropdownV2 = () => {
|
||||
true,
|
||||
);
|
||||
|
||||
if (isDefined(customHotkeyScope)) {
|
||||
setHotkeyScopeAndMemorizePreviousScope({
|
||||
scope: customHotkeyScope.scope,
|
||||
customScopes: customHotkeyScope.customScopes,
|
||||
});
|
||||
} else if (isDefined(dropdownHotkeyScope)) {
|
||||
setHotkeyScopeAndMemorizePreviousScope({
|
||||
scope: dropdownHotkeyScope.scope,
|
||||
customScopes: dropdownHotkeyScope.customScopes,
|
||||
});
|
||||
}
|
||||
setActiveDropdownFocusIdAndMemorizePrevious(specificComponentId);
|
||||
|
||||
pushFocusItemToFocusStack({
|
||||
focusId: scopeId,
|
||||
component: {
|
||||
type: FocusComponentType.DROPDOWN,
|
||||
instanceId: scopeId,
|
||||
},
|
||||
globalHotkeysConfig,
|
||||
// TODO: Remove this once we've fully migrated away from hotkey scopes
|
||||
hotkeyScope: { scope: 'dropdown' } as HotkeyScope,
|
||||
memoizeKey: 'global',
|
||||
});
|
||||
},
|
||||
[setHotkeyScopeAndMemorizePreviousScope],
|
||||
[pushFocusItemToFocusStack, setActiveDropdownFocusIdAndMemorizePrevious],
|
||||
);
|
||||
|
||||
const toggleDropdown = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(specificComponentId: string, customHotkeyScope?: HotkeyScope) => {
|
||||
(
|
||||
specificComponentId: string,
|
||||
globalHotkeysConfig?: Partial<GlobalHotkeysConfig>,
|
||||
) => {
|
||||
const scopeId = specificComponentId;
|
||||
const isDropdownOpen = snapshot
|
||||
.getLoadable(isDropdownOpenComponentState({ scopeId }))
|
||||
@@ -81,7 +92,7 @@ export const useDropdownV2 = () => {
|
||||
if (isDropdownOpen) {
|
||||
closeDropdown(specificComponentId);
|
||||
} else {
|
||||
openDropdown(specificComponentId, customHotkeyScope);
|
||||
openDropdown(specificComponentId, globalHotkeysConfig);
|
||||
}
|
||||
},
|
||||
[closeDropdown, openDropdown],
|
||||
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useDropdownStates } from '@/ui/layout/dropdown/hooks/internal/useDropdownStates';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const useInternalHotkeyScopeManagement = ({
|
||||
dropdownScopeId,
|
||||
dropdownHotkeyScopeFromParent,
|
||||
}: {
|
||||
dropdownScopeId: string;
|
||||
dropdownHotkeyScopeFromParent?: HotkeyScope;
|
||||
}) => {
|
||||
const { dropdownHotkeyScopeState } = useDropdownStates({ dropdownScopeId });
|
||||
|
||||
const [dropdownHotkeyScope, setDropdownHotkeyScope] = useRecoilState(
|
||||
dropdownHotkeyScopeState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDeeplyEqual(dropdownHotkeyScopeFromParent, dropdownHotkeyScope)) {
|
||||
setDropdownHotkeyScope(dropdownHotkeyScopeFromParent);
|
||||
}
|
||||
}, [
|
||||
dropdownHotkeyScope,
|
||||
dropdownHotkeyScopeFromParent,
|
||||
setDropdownHotkeyScope,
|
||||
]);
|
||||
};
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const dropdownHotkeyComponentState = createComponentState<
|
||||
HotkeyScope | null | undefined
|
||||
>({
|
||||
key: 'dropdownHotkeyComponentState',
|
||||
defaultValue: null,
|
||||
});
|
||||
@@ -1,13 +1,13 @@
|
||||
import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope';
|
||||
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
import { useRemoveFocusIdFromFocusStack } from '@/ui/utilities/focus/hooks/useRemoveFocusIdFromFocusStack';
|
||||
import { useRemoveFocusItemFromFocusStack } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStack';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const useModal = () => {
|
||||
const pushFocusItem = usePushFocusItemToFocusStack();
|
||||
const removeFocusId = useRemoveFocusIdFromFocusStack();
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const { removeFocusItemFromFocusStack } = useRemoveFocusItemFromFocusStack();
|
||||
|
||||
const closeModal = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
@@ -22,7 +22,7 @@ export const useModal = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
removeFocusId({
|
||||
removeFocusItemFromFocusStack({
|
||||
focusId: modalId,
|
||||
memoizeKey: modalId,
|
||||
});
|
||||
@@ -32,7 +32,7 @@ export const useModal = () => {
|
||||
false,
|
||||
);
|
||||
},
|
||||
[removeFocusId],
|
||||
[removeFocusItemFromFocusStack],
|
||||
);
|
||||
|
||||
const openModal = useRecoilCallback(
|
||||
@@ -53,7 +53,7 @@ export const useModal = () => {
|
||||
true,
|
||||
);
|
||||
|
||||
pushFocusItem({
|
||||
pushFocusItemToFocusStack({
|
||||
focusId: modalId,
|
||||
component: {
|
||||
type: FocusComponentType.MODAL,
|
||||
@@ -76,7 +76,7 @@ export const useModal = () => {
|
||||
memoizeKey: modalId,
|
||||
});
|
||||
},
|
||||
[pushFocusItem],
|
||||
[pushFocusItemToFocusStack],
|
||||
);
|
||||
|
||||
const toggleModal = useRecoilCallback(
|
||||
|
||||
+11
-4
@@ -13,19 +13,26 @@ type SelectableListProps = {
|
||||
selectableItemIdArray?: string[];
|
||||
selectableItemIdMatrix?: string[][];
|
||||
onSelect?: (selected: string) => void;
|
||||
hotkeyScope: string;
|
||||
selectableListInstanceId: string;
|
||||
focusId: string;
|
||||
hotkeyScope: string;
|
||||
};
|
||||
|
||||
export const SelectableList = ({
|
||||
children,
|
||||
hotkeyScope,
|
||||
selectableItemIdArray,
|
||||
selectableItemIdMatrix,
|
||||
selectableListInstanceId,
|
||||
onSelect,
|
||||
focusId,
|
||||
hotkeyScope,
|
||||
}: SelectableListProps) => {
|
||||
useSelectableListHotKeys(selectableListInstanceId, hotkeyScope, onSelect);
|
||||
useSelectableListHotKeys(
|
||||
selectableListInstanceId,
|
||||
hotkeyScope,
|
||||
focusId,
|
||||
onSelect,
|
||||
);
|
||||
|
||||
const setSelectableItemIds = useSetRecoilComponentStateV2(
|
||||
selectableItemIdsComponentState,
|
||||
@@ -54,7 +61,7 @@ export const SelectableList = ({
|
||||
instanceId: selectableListInstanceId,
|
||||
}}
|
||||
>
|
||||
<SelectableListContextProvider value={{ hotkeyScope }}>
|
||||
<SelectableListContextProvider value={{ focusId, hotkeyScope }}>
|
||||
{children}
|
||||
</SelectableListContextProvider>
|
||||
</SelectableListComponentInstanceContext.Provider>
|
||||
|
||||
+4
-2
@@ -8,12 +8,14 @@ export const SelectableListItemHotkeyEffect = ({
|
||||
itemId: string;
|
||||
onEnter: () => void;
|
||||
}) => {
|
||||
const { hotkeyScope } = useSelectableListContextOrThrow();
|
||||
const { focusId, hotkeyScope } = useSelectableListContextOrThrow();
|
||||
|
||||
useSelectableListListenToEnterHotkeyOnItem({
|
||||
hotkeyScope,
|
||||
focusId,
|
||||
itemId,
|
||||
onEnter,
|
||||
hotkeyScope,
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
+31
-10
@@ -5,14 +5,16 @@ import { Key } from 'ts-key-enum';
|
||||
import { selectableItemIdsComponentState } from '@/ui/layout/selectable-list/states/selectableItemIdsComponentState';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
|
||||
type Direction = 'up' | 'down' | 'left' | 'right';
|
||||
|
||||
export const useSelectableListHotKeys = (
|
||||
instanceId: string,
|
||||
// TODO: Remove this after migration to focus stack
|
||||
hotkeyScope: string,
|
||||
focusId: string,
|
||||
onSelect?: (itemId: string) => void,
|
||||
) => {
|
||||
const findPosition = (
|
||||
@@ -134,16 +136,35 @@ export const useSelectableListHotKeys = (
|
||||
[instanceId, onSelect],
|
||||
);
|
||||
|
||||
useScopedHotkeys(Key.ArrowUp, () => handleSelect('up'), hotkeyScope, []);
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: Key.ArrowUp,
|
||||
callback: () => handleSelect('up'),
|
||||
focusId,
|
||||
scope: hotkeyScope,
|
||||
dependencies: [handleSelect],
|
||||
});
|
||||
|
||||
useScopedHotkeys(Key.ArrowDown, () => handleSelect('down'), hotkeyScope, []);
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: Key.ArrowDown,
|
||||
callback: () => handleSelect('down'),
|
||||
focusId,
|
||||
scope: hotkeyScope,
|
||||
dependencies: [handleSelect],
|
||||
});
|
||||
|
||||
useScopedHotkeys(Key.ArrowLeft, () => handleSelect('left'), hotkeyScope, []);
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: Key.ArrowLeft,
|
||||
callback: () => handleSelect('left'),
|
||||
focusId,
|
||||
scope: hotkeyScope,
|
||||
dependencies: [handleSelect],
|
||||
});
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.ArrowRight,
|
||||
() => handleSelect('right'),
|
||||
hotkeyScope,
|
||||
[],
|
||||
);
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: Key.ArrowRight,
|
||||
callback: () => handleSelect('right'),
|
||||
focusId,
|
||||
scope: hotkeyScope,
|
||||
dependencies: [handleSelect],
|
||||
});
|
||||
};
|
||||
|
||||
+14
-9
@@ -1,6 +1,6 @@
|
||||
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
@@ -8,20 +8,24 @@ import { useRecoilCallback } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
export const useSelectableListListenToEnterHotkeyOnItem = ({
|
||||
hotkeyScope,
|
||||
focusId,
|
||||
itemId,
|
||||
onEnter,
|
||||
hotkeyScope,
|
||||
}: {
|
||||
hotkeyScope: string;
|
||||
focusId: string;
|
||||
itemId: string;
|
||||
onEnter: () => void;
|
||||
// TODO: Remove this after migration to focus stack
|
||||
hotkeyScope: string;
|
||||
}) => {
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
SelectableListComponentInstanceContext,
|
||||
);
|
||||
useScopedHotkeys(
|
||||
Key.Enter,
|
||||
useRecoilCallback(
|
||||
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: Key.Enter,
|
||||
callback: useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
() => {
|
||||
const selectedItemId = getSnapshotValue(
|
||||
@@ -37,7 +41,8 @@ export const useSelectableListListenToEnterHotkeyOnItem = ({
|
||||
},
|
||||
[instanceId, itemId, onEnter],
|
||||
),
|
||||
hotkeyScope,
|
||||
[itemId, onEnter],
|
||||
);
|
||||
focusId,
|
||||
scope: hotkeyScope,
|
||||
dependencies: [itemId, onEnter],
|
||||
});
|
||||
};
|
||||
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
import { createRequiredContext } from '~/utils/createRequiredContext';
|
||||
|
||||
export type SelectableListContextValue = {
|
||||
focusId: string;
|
||||
hotkeyScope: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -67,7 +67,6 @@ export const TabListDropdown = ({
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
dropdownHotkeyScope={{ scope: dropdownId }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user