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:
@ -2,15 +2,17 @@ import { useRef, useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
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 { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@ -21,7 +23,7 @@ import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmp
|
||||
type MultiSelectInputProps = {
|
||||
selectableListComponentInstanceId: string;
|
||||
values: FieldMultiSelectValue;
|
||||
hotkeyScope: string;
|
||||
focusId: string;
|
||||
onCancel?: () => void;
|
||||
options: SelectOption[];
|
||||
onOptionSelected: (value: FieldMultiSelectValue) => void;
|
||||
@ -32,7 +34,7 @@ export const MultiSelectInput = ({
|
||||
selectableListComponentInstanceId,
|
||||
values,
|
||||
options,
|
||||
hotkeyScope,
|
||||
focusId,
|
||||
onCancel,
|
||||
onOptionSelected,
|
||||
dropdownWidth,
|
||||
@ -69,15 +71,16 @@ export const MultiSelectInput = ({
|
||||
}
|
||||
};
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
() => {
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: Key.Escape,
|
||||
callback: () => {
|
||||
onCancel?.();
|
||||
resetSelectedItem();
|
||||
},
|
||||
hotkeyScope,
|
||||
[onCancel, resetSelectedItem],
|
||||
);
|
||||
focusId,
|
||||
scope: DEFAULT_CELL_SCOPE.scope,
|
||||
dependencies: [onCancel, resetSelectedItem],
|
||||
});
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [containerRef],
|
||||
@ -102,7 +105,8 @@ export const MultiSelectInput = ({
|
||||
<SelectableList
|
||||
selectableListInstanceId={selectableListComponentInstanceId}
|
||||
selectableItemIdArray={optionIds}
|
||||
hotkeyScope={hotkeyScope}
|
||||
focusId={focusId}
|
||||
hotkeyScope={DEFAULT_CELL_SCOPE.scope}
|
||||
>
|
||||
<DropdownContent
|
||||
ref={containerRef}
|
||||
@ -122,17 +126,25 @@ export const MultiSelectInput = ({
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{filteredOptionsInDropDown.map((option) => {
|
||||
return (
|
||||
<MenuItemMultiSelectTag
|
||||
<SelectableListItem
|
||||
key={option.value}
|
||||
selected={values?.includes(option.value) || false}
|
||||
text={option.label}
|
||||
color={option.color ?? 'transparent'}
|
||||
Icon={option.Icon ?? undefined}
|
||||
onClick={() =>
|
||||
onOptionSelected(formatNewSelectedOptions(option.value))
|
||||
}
|
||||
isKeySelected={selectedItemId === option.value}
|
||||
/>
|
||||
itemId={option.value}
|
||||
onEnter={() => {
|
||||
onOptionSelected(formatNewSelectedOptions(option.value));
|
||||
}}
|
||||
>
|
||||
<MenuItemMultiSelectTag
|
||||
key={option.value}
|
||||
selected={values?.includes(option.value) || false}
|
||||
text={option.label}
|
||||
color={option.color ?? 'transparent'}
|
||||
Icon={option.Icon ?? undefined}
|
||||
onClick={() =>
|
||||
onOptionSelected(formatNewSelectedOptions(option.value))
|
||||
}
|
||||
isKeySelected={selectedItemId === option.value}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuItemsContainer>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
|
||||
import { SelectInput as SelectBaseInput } from '@/ui/input/components/SelectInput';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectOption } from 'twenty-ui/input';
|
||||
@ -5,7 +6,7 @@ import { SelectOption } from 'twenty-ui/input';
|
||||
type SelectInputProps = {
|
||||
selectableListComponentInstanceId: string;
|
||||
selectableItemIdArray: string[];
|
||||
hotkeyScope: string;
|
||||
focusId: string;
|
||||
onEnter: (itemId: string) => void;
|
||||
onOptionSelected: (selectedOption: SelectOption) => void;
|
||||
options: SelectOption[];
|
||||
@ -19,7 +20,7 @@ type SelectInputProps = {
|
||||
export const SelectInput = ({
|
||||
selectableListComponentInstanceId,
|
||||
selectableItemIdArray,
|
||||
hotkeyScope,
|
||||
focusId,
|
||||
onOptionSelected,
|
||||
options,
|
||||
onCancel,
|
||||
@ -32,7 +33,8 @@ export const SelectInput = ({
|
||||
<SelectableList
|
||||
selectableListInstanceId={selectableListComponentInstanceId}
|
||||
selectableItemIdArray={selectableItemIdArray}
|
||||
hotkeyScope={hotkeyScope}
|
||||
focusId={focusId}
|
||||
hotkeyScope={DEFAULT_CELL_SCOPE.scope}
|
||||
>
|
||||
<SelectBaseInput
|
||||
onOptionSelected={onOptionSelected}
|
||||
@ -42,7 +44,7 @@ export const SelectInput = ({
|
||||
onFilterChange={onFilterChange}
|
||||
onClear={onClear}
|
||||
clearLabel={clearLabel}
|
||||
hotkeyScope={hotkeyScope}
|
||||
focusId={focusId}
|
||||
/>
|
||||
</SelectableList>
|
||||
);
|
||||
|
||||
@ -12,6 +12,7 @@ import { arrayToChunks } from '~/utils/array/arrayToChunks';
|
||||
|
||||
import { ICON_PICKER_DROPDOWN_CONTENT_WIDTH } from '@/ui/input/components/constants/IconPickerDropdownContentWidth';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
|
||||
import { useSelectableListListenToEnterHotkeyOnItem } from '@/ui/layout/selectable-list/hooks/useSelectableListListenToEnterHotkeyOnItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
@ -71,9 +72,10 @@ const IconPickerIcon = ({
|
||||
);
|
||||
|
||||
useSelectableListListenToEnterHotkeyOnItem({
|
||||
hotkeyScope: IconPickerHotkeyScope.IconPicker,
|
||||
focusId: iconKey,
|
||||
itemId: iconKey,
|
||||
onEnter: onClick,
|
||||
hotkeyScope: DropdownHotkeyScope.Dropdown,
|
||||
});
|
||||
|
||||
return (
|
||||
@ -184,7 +186,6 @@ export const IconPicker = ({
|
||||
<div className={className}>
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownHotkeyScope={{ scope: IconPickerHotkeyScope.IconPicker }}
|
||||
clickableComponent={
|
||||
<IconButton
|
||||
ariaLabel={`Click to select icon ${
|
||||
@ -203,7 +204,8 @@ export const IconPicker = ({
|
||||
<SelectableList
|
||||
selectableListInstanceId="icon-list"
|
||||
selectableItemIdMatrix={iconKeys2d}
|
||||
hotkeyScope={IconPickerHotkeyScope.IconPicker}
|
||||
focusId={dropdownId}
|
||||
hotkeyScope={DropdownHotkeyScope.Dropdown}
|
||||
>
|
||||
<DropdownMenuSearchInput
|
||||
placeholder={t`Search icon`}
|
||||
|
||||
@ -9,6 +9,7 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
|
||||
import { SelectControl } from '@/ui/input/components/SelectControl';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
@ -19,7 +20,6 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconComponent } from 'twenty-ui/display';
|
||||
import { SelectOption } from 'twenty-ui/input';
|
||||
import { MenuItem, MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { SelectHotkeyScope } from '../types/SelectHotkeyScope';
|
||||
|
||||
export type SelectSizeVariant = 'small' | 'default';
|
||||
|
||||
@ -166,9 +166,10 @@ export const Select = <Value extends SelectValue>({
|
||||
{!!filteredOptions.length && (
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<SelectableList
|
||||
hotkeyScope={SelectHotkeyScope.Select}
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={selectableItemIdArray}
|
||||
hotkeyScope={DropdownHotkeyScope.Dropdown}
|
||||
>
|
||||
{filteredOptions.map((option) => (
|
||||
<SelectableListItem
|
||||
@ -211,7 +212,6 @@ export const Select = <Value extends SelectValue>({
|
||||
)}
|
||||
</DropdownContent>
|
||||
}
|
||||
dropdownHotkeyScope={{ scope: SelectHotkeyScope.Select }}
|
||||
/>
|
||||
)}
|
||||
</StyledContainer>
|
||||
|
||||
@ -2,10 +2,13 @@ import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { TagColor } from 'twenty-ui/components';
|
||||
import { SelectOption } from 'twenty-ui/input';
|
||||
@ -19,7 +22,7 @@ interface SelectInputProps {
|
||||
onFilterChange?: (filteredOptions: SelectOption[]) => void;
|
||||
onClear?: () => void;
|
||||
clearLabel?: string;
|
||||
hotkeyScope: string;
|
||||
focusId: string;
|
||||
}
|
||||
|
||||
export const SelectInput = ({
|
||||
@ -30,10 +33,19 @@ export const SelectInput = ({
|
||||
onCancel,
|
||||
defaultOption,
|
||||
onFilterChange,
|
||||
hotkeyScope,
|
||||
}: SelectInputProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Get the SelectableList instance id from context
|
||||
const selectableListInstanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
SelectableListComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValueV2(
|
||||
selectedItemIdComponentState,
|
||||
selectableListInstanceId,
|
||||
);
|
||||
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
const [selectedOption, setSelectedOption] = useState<
|
||||
SelectOption | undefined
|
||||
@ -61,6 +73,11 @@ export const SelectInput = ({
|
||||
onOptionSelected(option);
|
||||
};
|
||||
|
||||
const handleClearOption = () => {
|
||||
setSelectedOption(undefined);
|
||||
onClear?.();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onFilterChange?.(optionsInDropDown);
|
||||
}, [onFilterChange, optionsInDropDown]);
|
||||
@ -81,20 +98,6 @@ export const SelectInput = ({
|
||||
listenerId: 'select-input',
|
||||
});
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Enter,
|
||||
() => {
|
||||
const selectedOption = optionsInDropDown.find((option) =>
|
||||
option.label.toLowerCase().includes(searchFilter.toLowerCase()),
|
||||
);
|
||||
if (isDefined(selectedOption)) {
|
||||
handleOptionChange(selectedOption);
|
||||
}
|
||||
},
|
||||
hotkeyScope,
|
||||
[searchFilter, optionsInDropDown],
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownContent ref={containerRef} selectDisabled>
|
||||
<DropdownMenuSearchInput
|
||||
@ -105,27 +108,37 @@ export const SelectInput = ({
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{onClear && clearLabel && (
|
||||
<MenuItemSelectTag
|
||||
key={`No ${clearLabel}`}
|
||||
text={`No ${clearLabel}`}
|
||||
color="transparent"
|
||||
variant={'outline'}
|
||||
onClick={() => {
|
||||
setSelectedOption(undefined);
|
||||
onClear();
|
||||
}}
|
||||
/>
|
||||
<SelectableListItem
|
||||
itemId={`No ${clearLabel}`}
|
||||
onEnter={handleClearOption}
|
||||
>
|
||||
<MenuItemSelectTag
|
||||
key={`No ${clearLabel}`}
|
||||
text={`No ${clearLabel}`}
|
||||
color="transparent"
|
||||
variant={'outline'}
|
||||
onClick={handleClearOption}
|
||||
isKeySelected={selectedItemId === `No ${clearLabel}`}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
)}
|
||||
{optionsInDropDown.map((option) => {
|
||||
return (
|
||||
<MenuItemSelectTag
|
||||
<SelectableListItem
|
||||
key={option.value}
|
||||
focused={selectedOption?.value === option.value}
|
||||
text={option.label}
|
||||
color={(option.color as TagColor) ?? 'transparent'}
|
||||
onClick={() => handleOptionChange(option)}
|
||||
LeftIcon={option.Icon}
|
||||
/>
|
||||
itemId={option.value}
|
||||
onEnter={() => handleOptionChange(option)}
|
||||
>
|
||||
<MenuItemSelectTag
|
||||
key={option.value}
|
||||
selected={selectedOption?.value === option.value}
|
||||
text={option.label}
|
||||
color={(option.color as TagColor) ?? 'transparent'}
|
||||
onClick={() => handleOptionChange(option)}
|
||||
LeftIcon={option.Icon}
|
||||
isKeySelected={selectedItemId === option.value}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuItemsContainer>
|
||||
|
||||
@ -70,7 +70,6 @@ export const CurrencyPickerDropdownButton = ({
|
||||
return (
|
||||
<Dropdown
|
||||
dropdownId="currency-picker-dropdown-id"
|
||||
dropdownHotkeyScope={{ scope: CurrencyPickerHotkeyScope.CurrencyPicker }}
|
||||
clickableComponent={
|
||||
<StyledDropdownButtonContainer>
|
||||
<StyledIconContainer>
|
||||
|
||||
@ -6,8 +6,6 @@ import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { CountryPickerHotkeyScope } from '../types/CountryPickerHotkeyScope';
|
||||
|
||||
import { PhoneCountryPickerDropdownSelect } from './PhoneCountryPickerDropdownSelect';
|
||||
|
||||
import 'react-phone-number-input/style.css';
|
||||
@ -98,7 +96,6 @@ export const PhoneCountryPickerDropdownButton = ({
|
||||
return (
|
||||
<Dropdown
|
||||
dropdownId="country-picker-dropdown-id"
|
||||
dropdownHotkeyScope={{ scope: CountryPickerHotkeyScope.CountryPicker }}
|
||||
clickableComponent={
|
||||
<StyledDropdownButtonContainer isUnfolded={isDropdownOpen}>
|
||||
<StyledIconContainer>
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
export enum SelectHotkeyScope {
|
||||
Select = 'select',
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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={{
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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',
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
export enum DropdownMenuHotkeyScope {
|
||||
InnerSelect = 'dropdown-menu-inner-select',
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
};
|
||||
@ -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(
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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],
|
||||
});
|
||||
};
|
||||
|
||||
@ -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,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 }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { SelectHotkeyScope } from '@/ui/input/types/SelectHotkeyScope';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { Placement } from '@floating-ui/react';
|
||||
@ -93,7 +92,6 @@ export const MenuItemWithOptionDropdown = ({
|
||||
dropdownPlacement={dropdownPlacement}
|
||||
dropdownComponents={dropdownContent}
|
||||
dropdownId={dropdownId}
|
||||
dropdownHotkeyScope={{ scope: SelectHotkeyScope.Select }}
|
||||
/>
|
||||
</div>
|
||||
{hasSubMenu && (
|
||||
|
||||
@ -5,7 +5,6 @@ import { MultiWorkspaceDropdownThemesComponents } from '@/ui/navigation/navigati
|
||||
import { MultiWorkspaceDropdownWorkspacesListComponents } from '@/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownWorkspacesListComponents';
|
||||
import { MULTI_WORKSPACE_DROPDOWN_ID } from '@/ui/navigation/navigation-drawer/constants/MultiWorkspaceDropdownId';
|
||||
import { multiWorkspaceDropdownState } from '@/ui/navigation/navigation-drawer/states/multiWorkspaceDropdownState';
|
||||
import { NavigationDrawerHotKeyScope } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerHotKeyScope';
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
@ -28,9 +27,6 @@ export const MultiWorkspaceDropdownButton = () => {
|
||||
return (
|
||||
<Dropdown
|
||||
dropdownId={MULTI_WORKSPACE_DROPDOWN_ID}
|
||||
dropdownHotkeyScope={{
|
||||
scope: NavigationDrawerHotKeyScope.MultiWorkspaceDropdownButton,
|
||||
}}
|
||||
dropdownOffset={{ y: -35, x: -5 }}
|
||||
clickableComponent={<MultiWorkspaceDropdownClickableComponent />}
|
||||
dropdownComponents={<DropdownComponents />}
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
|
||||
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { countAvailableWorkspaces } from '@/auth/utils/availableWorkspacesUtils';
|
||||
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
|
||||
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
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';
|
||||
@ -37,13 +38,11 @@ import {
|
||||
UndecoratedLink,
|
||||
} from 'twenty-ui/navigation';
|
||||
import {
|
||||
useSignUpInNewWorkspaceMutation,
|
||||
AvailableWorkspace,
|
||||
useSignUpInNewWorkspaceMutation,
|
||||
} from '~/generated/graphql';
|
||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
import { countAvailableWorkspaces } from '@/auth/utils/availableWorkspacesUtils';
|
||||
|
||||
const StyledDescription = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
@ -118,7 +117,6 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
|
||||
/>
|
||||
}
|
||||
dropdownId={'multi-workspace-dropdown-context-menu'}
|
||||
dropdownHotkeyScope={{ scope: SelectHotkeyScope.Select }}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
import { DEBUG_HOTKEY_SCOPE } from '@/ui/utilities/hotkey/constants/DebugHotkeyScope';
|
||||
|
||||
export const DEBUG_FOCUS_STACK = DEBUG_HOTKEY_SCOPE;
|
||||
@ -9,12 +9,12 @@ import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
const renderHooks = () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const pushFocusItem = usePushFocusItemToFocusStack();
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const focusStack = useRecoilValue(focusStackState);
|
||||
const currentFocusId = useRecoilValue(currentFocusIdSelector);
|
||||
|
||||
return {
|
||||
pushFocusItem,
|
||||
pushFocusItemToFocusStack,
|
||||
focusStack,
|
||||
currentFocusId,
|
||||
};
|
||||
@ -46,7 +46,7 @@ describe('usePushFocusItemToFocusStack', () => {
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
result.current.pushFocusItem({
|
||||
result.current.pushFocusItemToFocusStack({
|
||||
focusId: focusItem.focusId,
|
||||
component: {
|
||||
type: focusItem.componentInstance.componentType,
|
||||
@ -73,7 +73,7 @@ describe('usePushFocusItemToFocusStack', () => {
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
result.current.pushFocusItem({
|
||||
result.current.pushFocusItemToFocusStack({
|
||||
focusId: anotherFocusItem.focusId,
|
||||
component: {
|
||||
type: anotherFocusItem.componentInstance.componentType,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector';
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
@ -10,14 +10,15 @@ import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
const renderHooks = () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const pushFocusItem = usePushFocusItemToFocusStack();
|
||||
const removeFocusId = useRemoveFocusIdFromFocusStack();
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const { removeFocusItemFromFocusStack } =
|
||||
useRemoveFocusItemFromFocusStack();
|
||||
const focusStack = useRecoilValue(focusStackState);
|
||||
const currentFocusId = useRecoilValue(currentFocusIdSelector);
|
||||
|
||||
return {
|
||||
pushFocusItem,
|
||||
removeFocusId,
|
||||
pushFocusItemToFocusStack,
|
||||
removeFocusItemFromFocusStack,
|
||||
focusStack,
|
||||
currentFocusId,
|
||||
};
|
||||
@ -30,8 +31,8 @@ const renderHooks = () => {
|
||||
return { result };
|
||||
};
|
||||
|
||||
describe('useRemoveFocusIdFromFocusStack', () => {
|
||||
it('should remove focus id from the stack', async () => {
|
||||
describe('useRemoveFocusItemFromFocusStack', () => {
|
||||
it('should remove focus item from the stack', async () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
const firstFocusItem = {
|
||||
@ -59,7 +60,7 @@ describe('useRemoveFocusIdFromFocusStack', () => {
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
result.current.pushFocusItem({
|
||||
result.current.pushFocusItemToFocusStack({
|
||||
focusId: firstFocusItem.focusId,
|
||||
component: {
|
||||
type: firstFocusItem.componentInstance.componentType,
|
||||
@ -71,7 +72,7 @@ describe('useRemoveFocusIdFromFocusStack', () => {
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
result.current.pushFocusItem({
|
||||
result.current.pushFocusItemToFocusStack({
|
||||
focusId: secondFocusItem.focusId,
|
||||
component: {
|
||||
type: secondFocusItem.componentInstance.componentType,
|
||||
@ -89,7 +90,7 @@ describe('useRemoveFocusIdFromFocusStack', () => {
|
||||
expect(result.current.currentFocusId).toEqual(secondFocusItem.focusId);
|
||||
|
||||
await act(async () => {
|
||||
result.current.removeFocusId({
|
||||
result.current.removeFocusItemFromFocusStack({
|
||||
focusId: firstFocusItem.focusId,
|
||||
memoizeKey: 'global',
|
||||
});
|
||||
@ -10,13 +10,13 @@ import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
const renderHooks = () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const pushFocusItem = usePushFocusItemToFocusStack();
|
||||
const resetFocusStack = useResetFocusStack();
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const { resetFocusStack } = useResetFocusStack();
|
||||
const focusStack = useRecoilValue(focusStackState);
|
||||
const currentFocusId = useRecoilValue(currentFocusIdSelector);
|
||||
|
||||
return {
|
||||
pushFocusItem,
|
||||
pushFocusItemToFocusStack,
|
||||
resetFocusStack,
|
||||
focusStack,
|
||||
currentFocusId,
|
||||
@ -47,7 +47,7 @@ describe('useResetFocusStack', () => {
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
result.current.pushFocusItem({
|
||||
result.current.pushFocusItemToFocusStack({
|
||||
focusId: focusItem.focusId,
|
||||
component: {
|
||||
type: focusItem.componentInstance.componentType,
|
||||
|
||||
@ -10,13 +10,13 @@ import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
const renderHooks = () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const pushFocusItem = usePushFocusItemToFocusStack();
|
||||
const resetFocusStackToFocusItem = useResetFocusStackToFocusItem();
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const { resetFocusStackToFocusItem } = useResetFocusStackToFocusItem();
|
||||
const focusStack = useRecoilValue(focusStackState);
|
||||
const currentFocusId = useRecoilValue(currentFocusIdSelector);
|
||||
|
||||
return {
|
||||
pushFocusItem,
|
||||
pushFocusItemToFocusStack,
|
||||
resetFocusStackToFocusItem,
|
||||
focusStack,
|
||||
currentFocusId,
|
||||
@ -59,7 +59,7 @@ describe('useResetFocusStackToFocusItem', () => {
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
result.current.pushFocusItem({
|
||||
result.current.pushFocusItemToFocusStack({
|
||||
focusId: firstFocusItem.focusId,
|
||||
component: {
|
||||
type: firstFocusItem.componentInstance.componentType,
|
||||
@ -71,7 +71,7 @@ describe('useResetFocusStackToFocusItem', () => {
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
result.current.pushFocusItem({
|
||||
result.current.pushFocusItemToFocusStack({
|
||||
focusId: secondFocusItem.focusId,
|
||||
component: {
|
||||
type: secondFocusItem.componentInstance.componentType,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { DEBUG_FOCUS_STACK } from '@/ui/utilities/focus/constants/DebugFocusStack';
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { FocusStackItem } from '@/ui/utilities/focus/types/FocusStackItem';
|
||||
@ -5,26 +6,27 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH
|
||||
import { GlobalHotkeysConfig } from '@/ui/utilities/hotkey/types/GlobalHotkeysConfig';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { logDebug } from '~/utils/logDebug';
|
||||
|
||||
const addOrMoveItemToTheTopOfTheStack = ({
|
||||
focusStackItem,
|
||||
currentFocusStack,
|
||||
}: {
|
||||
focusStackItem: FocusStackItem;
|
||||
currentFocusStack: FocusStackItem[];
|
||||
}) => [
|
||||
...currentFocusStack.filter(
|
||||
(currentFocusStackItem) =>
|
||||
currentFocusStackItem.focusId !== focusStackItem.focusId,
|
||||
),
|
||||
focusStackItem,
|
||||
];
|
||||
|
||||
export const usePushFocusItemToFocusStack = () => {
|
||||
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
|
||||
|
||||
const addOrMoveItemToTheTopOfTheStack = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(focusStackItem: FocusStackItem) => {
|
||||
set(focusStackState, (currentFocusStack) => [
|
||||
...currentFocusStack.filter(
|
||||
(currentFocusStackItem) =>
|
||||
currentFocusStackItem.focusId !== focusStackItem.focusId,
|
||||
),
|
||||
focusStackItem,
|
||||
]);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return useRecoilCallback(
|
||||
() =>
|
||||
const pushFocusItemToFocusStack = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
({
|
||||
focusId,
|
||||
component,
|
||||
@ -57,7 +59,23 @@ export const usePushFocusItemToFocusStack = () => {
|
||||
},
|
||||
};
|
||||
|
||||
addOrMoveItemToTheTopOfTheStack(focusStackItem);
|
||||
const currentFocusStack = snapshot
|
||||
.getLoadable(focusStackState)
|
||||
.getValue();
|
||||
|
||||
const newFocusStack = addOrMoveItemToTheTopOfTheStack({
|
||||
focusStackItem,
|
||||
currentFocusStack,
|
||||
});
|
||||
|
||||
set(focusStackState, newFocusStack);
|
||||
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(`DEBUG: pushFocusItemToFocusStack ${focusId}`, {
|
||||
focusStackItem,
|
||||
newFocusStack,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Remove this once we've migrated hotkey scopes to the new api
|
||||
setHotkeyScopeAndMemorizePreviousScope({
|
||||
@ -66,6 +84,8 @@ export const usePushFocusItemToFocusStack = () => {
|
||||
memoizeKey,
|
||||
});
|
||||
},
|
||||
[setHotkeyScopeAndMemorizePreviousScope, addOrMoveItemToTheTopOfTheStack],
|
||||
[setHotkeyScopeAndMemorizePreviousScope],
|
||||
);
|
||||
|
||||
return { pushFocusItemToFocusStack };
|
||||
};
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const useRemoveFocusIdFromFocusStack = () => {
|
||||
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
|
||||
|
||||
return useRecoilCallback(
|
||||
({ set }) =>
|
||||
({ focusId, memoizeKey }: { focusId: string; memoizeKey: string }) => {
|
||||
set(focusStackState, (previousFocusStack) =>
|
||||
previousFocusStack.filter(
|
||||
(focusStackItem) => focusStackItem.focusId !== focusId,
|
||||
),
|
||||
);
|
||||
|
||||
// TODO: Remove this once we've migrated hotkey scopes to the new api
|
||||
goBackToPreviousHotkeyScope(memoizeKey);
|
||||
},
|
||||
[goBackToPreviousHotkeyScope],
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,34 @@
|
||||
import { DEBUG_FOCUS_STACK } from '@/ui/utilities/focus/constants/DebugFocusStack';
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { logDebug } from '~/utils/logDebug';
|
||||
|
||||
export const useRemoveFocusItemFromFocusStack = () => {
|
||||
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
|
||||
|
||||
const removeFocusItemFromFocusStack = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
({ focusId, memoizeKey }: { focusId: string; memoizeKey: string }) => {
|
||||
const focusStack = snapshot.getLoadable(focusStackState).getValue();
|
||||
|
||||
const newFocusStack = focusStack.filter(
|
||||
(focusStackItem) => focusStackItem.focusId !== focusId,
|
||||
);
|
||||
|
||||
set(focusStackState, newFocusStack);
|
||||
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(`DEBUG: removeFocusItemFromFocusStack ${focusId}`, {
|
||||
newFocusStack,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Remove this once we've migrated hotkey scopes to the new api
|
||||
goBackToPreviousHotkeyScope(memoizeKey);
|
||||
},
|
||||
[goBackToPreviousHotkeyScope],
|
||||
);
|
||||
|
||||
return { removeFocusItemFromFocusStack };
|
||||
};
|
||||
@ -1,18 +1,26 @@
|
||||
import { DEBUG_FOCUS_STACK } from '@/ui/utilities/focus/constants/DebugFocusStack';
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
|
||||
import { previousHotkeyScopeFamilyState } from '@/ui/utilities/hotkey/states/internal/previousHotkeyScopeFamilyState';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { logDebug } from '~/utils/logDebug';
|
||||
|
||||
export const useResetFocusStack = () => {
|
||||
return useRecoilCallback(
|
||||
const resetFocusStack = useRecoilCallback(
|
||||
({ reset }) =>
|
||||
(memoizeKey = 'global') => {
|
||||
reset(focusStackState);
|
||||
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(`DEBUG: reset focus stack`);
|
||||
}
|
||||
|
||||
// TODO: Remove this once we've migrated hotkey scopes to the new api
|
||||
reset(previousHotkeyScopeFamilyState(memoizeKey as string));
|
||||
reset(currentHotkeyScopeState);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return { resetFocusStack };
|
||||
};
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import { DEBUG_FOCUS_STACK } from '@/ui/utilities/focus/constants/DebugFocusStack';
|
||||
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
|
||||
import { FocusStackItem } from '@/ui/utilities/focus/types/FocusStackItem';
|
||||
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
|
||||
import { previousHotkeyScopeFamilyState } from '@/ui/utilities/hotkey/states/internal/previousHotkeyScopeFamilyState';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { logDebug } from '~/utils/logDebug';
|
||||
|
||||
export const useResetFocusStackToFocusItem = () => {
|
||||
return useRecoilCallback(
|
||||
const resetFocusStackToFocusItem = useRecoilCallback(
|
||||
({ set }) =>
|
||||
({
|
||||
focusStackItem,
|
||||
@ -19,10 +21,18 @@ export const useResetFocusStackToFocusItem = () => {
|
||||
}) => {
|
||||
set(focusStackState, [focusStackItem]);
|
||||
|
||||
if (DEBUG_FOCUS_STACK) {
|
||||
logDebug(`DEBUG: reset focus stack to focus item`, {
|
||||
focusStackItem,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Remove this once we've migrated hotkey scopes to the new api
|
||||
set(previousHotkeyScopeFamilyState(memoizeKey), null);
|
||||
set(currentHotkeyScopeState, hotkeyScope);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return { resetFocusStackToFocusItem };
|
||||
};
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
export enum FocusComponentType {
|
||||
MODAL = 'modal',
|
||||
DROPDOWN = 'dropdown',
|
||||
SIDE_PANEL = 'side-panel',
|
||||
OPEN_FIELD_INPUT = 'open-field-input',
|
||||
}
|
||||
|
||||
@ -1,22 +1,28 @@
|
||||
import { Keys } from 'react-hotkeys-hook';
|
||||
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
|
||||
type HotkeyEffectProps = {
|
||||
hotkey: {
|
||||
key: Keys;
|
||||
scope: string;
|
||||
};
|
||||
onHotkeyTriggered: () => void;
|
||||
focusId: string;
|
||||
};
|
||||
|
||||
export const HotkeyEffect = ({
|
||||
hotkey,
|
||||
focusId,
|
||||
onHotkeyTriggered,
|
||||
}: HotkeyEffectProps) => {
|
||||
useScopedHotkeys(hotkey.key, () => onHotkeyTriggered(), hotkey.scope, [
|
||||
onHotkeyTriggered,
|
||||
]);
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: hotkey.key,
|
||||
callback: onHotkeyTriggered,
|
||||
focusId,
|
||||
scope: DropdownHotkeyScope.Dropdown,
|
||||
dependencies: [onHotkeyTriggered],
|
||||
});
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
@ -49,7 +49,7 @@ export const useHotkeysOnFocusedElementCallback = (
|
||||
hotkeysEvent.keys
|
||||
}) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join(
|
||||
', ',
|
||||
)}] and the current focus identifier is [${focusId}]`,
|
||||
)}] and the current focus identifier is [${currentFocusId}], and the focusId is [${focusId}]`,
|
||||
'color: gray; ',
|
||||
);
|
||||
}
|
||||
@ -63,7 +63,7 @@ export const useHotkeysOnFocusedElementCallback = (
|
||||
hotkeysEvent.keys
|
||||
}) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join(
|
||||
', ',
|
||||
)}], and the current focus identifier is [${focusId}]`,
|
||||
)}], and the current focus identifier is [${currentFocusId}], and the focusId is [${focusId}]`,
|
||||
'color: green;',
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user