512 Ability to navigate dropdown menus with keyboard (#11735)
# Ability to navigate dropdown menus with keyboard The aim of this PR is to improve accessibility by allowing the user to navigate inside the dropdown menus with the keyboard. This PR refactors the `SelectableList` and `SelectableListItem` components to move the Enter event handling responsibility from `SelectableList` to the individual `SelectableListItem` components. Closes [512](https://github.com/twentyhq/core-team-issues/issues/512) ## Key Changes: - All dropdowns are now navigable with arrow keys ## Technical Implementation: - Each `SelectableListItem` now has direct access to its own `Enter` key handler, improving component encapsulation - Removed the central `Enter` key handler logic from `SelectableList` - Added `SelectableList` and `SelectableListItem` to all `Dropdown` components inside the app - Updated all component implementations to adapt to the new pattern: - Action menu components (`ActionDropdownItem`, `ActionListItem`) - Command menu components - Object filter, sort and options dropdowns - Record picker components - Select components --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -1,13 +1,18 @@
|
||||
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
|
||||
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
|
||||
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
|
||||
import { useSetViewTypeFromLayoutOptionsMenu } from '@/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu';
|
||||
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
||||
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
|
||||
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
||||
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
|
||||
@ -74,6 +79,18 @@ export const ObjectOptionsDropdownLayoutContent = () => {
|
||||
const isDefaultView = currentView?.key === 'INDEX';
|
||||
const nbsp = '\u00A0';
|
||||
|
||||
const selectableItemIdArray = [
|
||||
ViewType.Table,
|
||||
...(isDefaultView ? [] : [ViewType.Kanban]),
|
||||
ViewOpenRecordInType.SIDE_PANEL,
|
||||
...(currentView?.type === ViewType.Kanban ? ['Group', 'Compact view'] : []),
|
||||
];
|
||||
|
||||
const selectedItemId = useRecoilComponentValueV2(
|
||||
selectedItemIdComponentState,
|
||||
OBJECT_OPTIONS_DROPDOWN_ID,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader
|
||||
@ -86,81 +103,132 @@ export const ObjectOptionsDropdownLayoutContent = () => {
|
||||
>
|
||||
{t`Layout`}
|
||||
</DropdownMenuHeader>
|
||||
|
||||
{!!currentView && (
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItemSelect
|
||||
LeftIcon={IconTable}
|
||||
text={t`Table`}
|
||||
selected={currentView?.type === ViewType.Table}
|
||||
onClick={async () => {
|
||||
if (currentView?.type !== ViewType.Table) {
|
||||
await setAndPersistViewType(ViewType.Table);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<MenuItemSelect
|
||||
LeftIcon={viewTypeIconMapping(ViewType.Kanban)}
|
||||
text={t`Kanban`}
|
||||
disabled={isDefaultView}
|
||||
contextualText={
|
||||
isDefaultView ? (
|
||||
<>
|
||||
{nbsp}·{nbsp}
|
||||
<OverflowingTextWithTooltip
|
||||
text={t`Not available for default view`}
|
||||
/>
|
||||
</>
|
||||
) : availableFieldsForKanban.length === 0 ? (
|
||||
t`Create Select...`
|
||||
) : undefined
|
||||
}
|
||||
selected={currentView?.type === ViewType.Kanban}
|
||||
onClick={handleSelectKanbanViewType}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<MenuItem
|
||||
onClick={() => onContentChange('layoutOpenIn')}
|
||||
LeftIcon={
|
||||
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL
|
||||
? IconLayoutSidebarRight
|
||||
: IconLayoutNavbar
|
||||
}
|
||||
text={t`Open in`}
|
||||
contextualText={
|
||||
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL
|
||||
? t`Side Panel`
|
||||
: t`Record Page`
|
||||
}
|
||||
hasSubMenu
|
||||
/>
|
||||
{currentView?.type === ViewType.Kanban && (
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
isDefined(recordGroupFieldMetadata)
|
||||
? onContentChange('recordGroups')
|
||||
: onContentChange('recordGroupFields')
|
||||
<SelectableList
|
||||
selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
|
||||
hotkeyScope={TableOptionsHotkeyScope.Dropdown}
|
||||
selectableItemIdArray={selectableItemIdArray}
|
||||
>
|
||||
<SelectableListItem
|
||||
itemId={ViewType.Table}
|
||||
onEnter={() => {
|
||||
setAndPersistViewType(ViewType.Table);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
LeftIcon={IconTable}
|
||||
text={t`Table`}
|
||||
selected={currentView?.type === ViewType.Table}
|
||||
focused={selectedItemId === ViewType.Table}
|
||||
onClick={async () => {
|
||||
if (currentView?.type !== ViewType.Table) {
|
||||
await setAndPersistViewType(ViewType.Table);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem
|
||||
itemId={ViewType.Kanban}
|
||||
onEnter={() => {
|
||||
setAndPersistViewType(ViewType.Kanban);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
LeftIcon={viewTypeIconMapping(ViewType.Kanban)}
|
||||
text={t`Kanban`}
|
||||
disabled={isDefaultView}
|
||||
focused={selectedItemId === ViewType.Kanban}
|
||||
contextualText={
|
||||
isDefaultView ? (
|
||||
<>
|
||||
{nbsp}·{nbsp}
|
||||
<OverflowingTextWithTooltip
|
||||
text={t`Not available for default view`}
|
||||
/>
|
||||
</>
|
||||
) : availableFieldsForKanban.length === 0 ? (
|
||||
t`Create Select...`
|
||||
) : undefined
|
||||
}
|
||||
selected={currentView?.type === ViewType.Kanban}
|
||||
onClick={handleSelectKanbanViewType}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<DropdownMenuSeparator />
|
||||
<SelectableListItem
|
||||
itemId={ViewOpenRecordInType.SIDE_PANEL}
|
||||
onEnter={() => {
|
||||
onContentChange('layoutOpenIn');
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
focused={selectedItemId === ViewOpenRecordInType.SIDE_PANEL}
|
||||
LeftIcon={
|
||||
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL
|
||||
? IconLayoutSidebarRight
|
||||
: IconLayoutNavbar
|
||||
}
|
||||
text={t`Open in`}
|
||||
contextualText={
|
||||
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL
|
||||
? t`Side Panel`
|
||||
: t`Record Page`
|
||||
}
|
||||
LeftIcon={IconLayoutList}
|
||||
text={t`Group`}
|
||||
contextualText={recordGroupFieldMetadata?.label}
|
||||
hasSubMenu
|
||||
/>
|
||||
</SelectableListItem>
|
||||
{currentView?.type === ViewType.Kanban && (
|
||||
<>
|
||||
<SelectableListItem
|
||||
itemId={'Group'}
|
||||
onEnter={() => {
|
||||
isDefined(recordGroupFieldMetadata)
|
||||
? onContentChange('recordGroups')
|
||||
: onContentChange('recordGroupFields');
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
focused={selectedItemId === 'Group'}
|
||||
onClick={() =>
|
||||
isDefined(recordGroupFieldMetadata)
|
||||
? onContentChange('recordGroups')
|
||||
: onContentChange('recordGroupFields')
|
||||
}
|
||||
LeftIcon={IconLayoutList}
|
||||
text={t`Group`}
|
||||
contextualText={recordGroupFieldMetadata?.label}
|
||||
hasSubMenu
|
||||
/>
|
||||
</SelectableListItem>
|
||||
|
||||
<MenuItemToggle
|
||||
LeftIcon={IconBaselineDensitySmall}
|
||||
onToggleChange={() =>
|
||||
setAndPersistIsCompactModeActive(
|
||||
!isCompactModeActive,
|
||||
currentView,
|
||||
)
|
||||
}
|
||||
toggled={isCompactModeActive}
|
||||
text={t`Compact view`}
|
||||
toggleSize="small"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<SelectableListItem
|
||||
itemId={'Compact view'}
|
||||
onEnter={() => {
|
||||
setAndPersistIsCompactModeActive(
|
||||
!isCompactModeActive,
|
||||
currentView,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<MenuItemToggle
|
||||
focused={selectedItemId === 'Compact view'}
|
||||
LeftIcon={IconBaselineDensitySmall}
|
||||
onToggleChange={() =>
|
||||
setAndPersistIsCompactModeActive(
|
||||
!isCompactModeActive,
|
||||
currentView,
|
||||
)
|
||||
}
|
||||
toggled={isCompactModeActive}
|
||||
text={t`Compact view`}
|
||||
toggleSize="small"
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</>
|
||||
)}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -1,9 +1,15 @@
|
||||
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
|
||||
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
|
||||
import { useUpdateObjectViewOptions } from '@/object-record/object-options-dropdown/hooks/useUpdateObjectViewOptions';
|
||||
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
|
||||
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
||||
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
|
||||
import { t } from '@lingui/core/macro';
|
||||
@ -21,6 +27,16 @@ export const ObjectOptionsDropdownLayoutOpenInContent = () => {
|
||||
const { currentView } = useGetCurrentViewOnly();
|
||||
const { setAndPersistOpenRecordIn } = useUpdateObjectViewOptions();
|
||||
|
||||
const selectedItemId = useRecoilComponentValueV2(
|
||||
selectedItemIdComponentState,
|
||||
OBJECT_OPTIONS_DROPDOWN_ID,
|
||||
);
|
||||
|
||||
const selectableItemIdArray = [
|
||||
ViewOpenRecordInType.SIDE_PANEL,
|
||||
ViewOpenRecordInType.RECORD_PAGE,
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader
|
||||
@ -34,30 +50,60 @@ export const ObjectOptionsDropdownLayoutOpenInContent = () => {
|
||||
{t`Open in`}
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItemSelect
|
||||
LeftIcon={IconLayoutSidebarRight}
|
||||
text={t`Side Panel`}
|
||||
selected={recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL}
|
||||
onClick={() =>
|
||||
setAndPersistOpenRecordIn(
|
||||
ViewOpenRecordInType.SIDE_PANEL,
|
||||
currentView,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<MenuItemSelect
|
||||
LeftIcon={IconLayoutNavbar}
|
||||
text={t`Record Page`}
|
||||
selected={
|
||||
recordIndexOpenRecordIn === ViewOpenRecordInType.RECORD_PAGE
|
||||
}
|
||||
onClick={() =>
|
||||
setAndPersistOpenRecordIn(
|
||||
ViewOpenRecordInType.RECORD_PAGE,
|
||||
currentView,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<SelectableList
|
||||
selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
|
||||
hotkeyScope={TableOptionsHotkeyScope.Dropdown}
|
||||
selectableItemIdArray={selectableItemIdArray}
|
||||
>
|
||||
<SelectableListItem
|
||||
itemId={ViewOpenRecordInType.SIDE_PANEL}
|
||||
onEnter={() =>
|
||||
setAndPersistOpenRecordIn(
|
||||
ViewOpenRecordInType.SIDE_PANEL,
|
||||
currentView,
|
||||
)
|
||||
}
|
||||
>
|
||||
<MenuItemSelect
|
||||
LeftIcon={IconLayoutSidebarRight}
|
||||
text={t`Side Panel`}
|
||||
selected={
|
||||
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL
|
||||
}
|
||||
focused={selectedItemId === ViewOpenRecordInType.SIDE_PANEL}
|
||||
onClick={() =>
|
||||
setAndPersistOpenRecordIn(
|
||||
ViewOpenRecordInType.SIDE_PANEL,
|
||||
currentView,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem
|
||||
itemId={ViewOpenRecordInType.RECORD_PAGE}
|
||||
onEnter={() =>
|
||||
setAndPersistOpenRecordIn(
|
||||
ViewOpenRecordInType.RECORD_PAGE,
|
||||
currentView,
|
||||
)
|
||||
}
|
||||
>
|
||||
<MenuItemSelect
|
||||
LeftIcon={IconLayoutNavbar}
|
||||
text={t`Record Page`}
|
||||
selected={
|
||||
recordIndexOpenRecordIn === ViewOpenRecordInType.RECORD_PAGE
|
||||
}
|
||||
onClick={() =>
|
||||
setAndPersistOpenRecordIn(
|
||||
ViewOpenRecordInType.RECORD_PAGE,
|
||||
currentView,
|
||||
)
|
||||
}
|
||||
focused={selectedItemId === ViewOpenRecordInType.RECORD_PAGE}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { ObjectOptionsDropdownMenuViewName } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuViewName';
|
||||
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
|
||||
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
|
||||
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
|
||||
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
||||
@ -9,6 +10,9 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
@ -75,92 +79,155 @@ export const ObjectOptionsDropdownMenuContent = () => {
|
||||
|
||||
const isDefaultView = currentView?.key === 'INDEX';
|
||||
|
||||
const selectableItemIdArray = [
|
||||
'Layout',
|
||||
'Fields',
|
||||
...(isDefaultView ? [] : ['Group']),
|
||||
'Copy link to view',
|
||||
...(isDefaultView ? [] : ['Delete view']),
|
||||
];
|
||||
|
||||
const selectedItemId = useRecoilComponentValueV2(
|
||||
selectedItemIdComponentState,
|
||||
OBJECT_OPTIONS_DROPDOWN_ID,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{currentView && (
|
||||
<ObjectOptionsDropdownMenuViewName currentView={currentView} />
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<MenuItem
|
||||
onClick={() => onContentChange('layout')}
|
||||
LeftIcon={viewTypeIconMapping(currentView?.type ?? ViewType.Table)}
|
||||
text={t`Layout`}
|
||||
contextualText={`${capitalize(currentView?.type ?? '')}`}
|
||||
hasSubMenu
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<MenuItem
|
||||
onClick={() => onContentChange('fields')}
|
||||
LeftIcon={IconListDetails}
|
||||
text={t`Fields`}
|
||||
contextualText={`${visibleBoardFields.length} shown`}
|
||||
hasSubMenu
|
||||
/>
|
||||
|
||||
<div id="group-by-menu-item">
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
isDefined(recordGroupFieldMetadata)
|
||||
? onContentChange('recordGroups')
|
||||
: onContentChange('recordGroupFields')
|
||||
}
|
||||
LeftIcon={IconLayoutList}
|
||||
text={t`Group`}
|
||||
contextualText={
|
||||
isDefaultView
|
||||
? t`Not available on Default View`
|
||||
: recordGroupFieldMetadata?.label
|
||||
}
|
||||
hasSubMenu
|
||||
disabled={isDefaultView}
|
||||
/>
|
||||
</div>
|
||||
{!isGroupByEnabled && (
|
||||
<AppTooltip
|
||||
anchorSelect={`#group-by-menu-item`}
|
||||
content={t`Not available on Default View`}
|
||||
noArrow
|
||||
place="bottom"
|
||||
width="100%"
|
||||
/>
|
||||
)}
|
||||
<SelectableList
|
||||
selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
|
||||
hotkeyScope={TableOptionsHotkeyScope.Dropdown}
|
||||
selectableItemIdArray={selectableItemIdArray}
|
||||
>
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<SelectableListItem
|
||||
itemId="Layout"
|
||||
onEnter={() => onContentChange('layout')}
|
||||
>
|
||||
<MenuItem
|
||||
focused={selectedItemId === 'Layout'}
|
||||
onClick={() => onContentChange('layout')}
|
||||
LeftIcon={viewTypeIconMapping(
|
||||
currentView?.type ?? ViewType.Table,
|
||||
)}
|
||||
text={t`Layout`}
|
||||
contextualText={`${capitalize(currentView?.type ?? '')}`}
|
||||
hasSubMenu
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
const currentUrl = window.location.href;
|
||||
navigator.clipboard.writeText(currentUrl);
|
||||
enqueueSnackBar('Link copied to clipboard', {
|
||||
variant: SnackBarVariant.Success,
|
||||
icon: <IconCopy size={theme.icon.size.md} />,
|
||||
duration: 2000,
|
||||
});
|
||||
}}
|
||||
LeftIcon={IconCopy}
|
||||
text={t`Copy link to view`}
|
||||
/>
|
||||
<div id="delete-view-menu-item">
|
||||
<MenuItem
|
||||
onClick={() => handleDelete()}
|
||||
LeftIcon={IconTrash}
|
||||
text={t`Delete view`}
|
||||
disabled={currentView?.key === 'INDEX'}
|
||||
/>
|
||||
</div>
|
||||
{currentView?.key === 'INDEX' && (
|
||||
<AppTooltip
|
||||
anchorSelect={`#delete-view-menu-item`}
|
||||
content={t`Not available on Default View`}
|
||||
noArrow
|
||||
place="bottom"
|
||||
width="100%"
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<SelectableListItem
|
||||
itemId="Fields"
|
||||
onEnter={() => onContentChange('fields')}
|
||||
>
|
||||
<MenuItem
|
||||
focused={selectedItemId === 'Fields'}
|
||||
onClick={() => onContentChange('fields')}
|
||||
LeftIcon={IconListDetails}
|
||||
text={t`Fields`}
|
||||
contextualText={`${visibleBoardFields.length} shown`}
|
||||
hasSubMenu
|
||||
/>
|
||||
</SelectableListItem>
|
||||
|
||||
<div id="group-by-menu-item">
|
||||
<SelectableListItem
|
||||
itemId="Group"
|
||||
onEnter={() =>
|
||||
isDefined(recordGroupFieldMetadata)
|
||||
? onContentChange('recordGroups')
|
||||
: onContentChange('recordGroupFields')
|
||||
}
|
||||
>
|
||||
<MenuItem
|
||||
focused={selectedItemId === 'Group'}
|
||||
onClick={() =>
|
||||
isDefined(recordGroupFieldMetadata)
|
||||
? onContentChange('recordGroups')
|
||||
: onContentChange('recordGroupFields')
|
||||
}
|
||||
LeftIcon={IconLayoutList}
|
||||
text={t`Group`}
|
||||
contextualText={
|
||||
isDefaultView
|
||||
? t`Not available on Default View`
|
||||
: recordGroupFieldMetadata?.label
|
||||
}
|
||||
hasSubMenu
|
||||
disabled={isDefaultView}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</div>
|
||||
{!isGroupByEnabled && (
|
||||
<AppTooltip
|
||||
anchorSelect={`#group-by-menu-item`}
|
||||
content={t`Not available on Default View`}
|
||||
noArrow
|
||||
place="bottom"
|
||||
width="100%"
|
||||
/>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<SelectableListItem
|
||||
itemId="Copy link to view"
|
||||
onEnter={() => {
|
||||
const currentUrl = window.location.href;
|
||||
navigator.clipboard.writeText(currentUrl);
|
||||
enqueueSnackBar('Link copied to clipboard', {
|
||||
variant: SnackBarVariant.Success,
|
||||
icon: <IconCopy size={theme.icon.size.md} />,
|
||||
duration: 2000,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
focused={selectedItemId === 'Copy link to view'}
|
||||
onClick={() => {
|
||||
const currentUrl = window.location.href;
|
||||
navigator.clipboard.writeText(currentUrl);
|
||||
enqueueSnackBar('Link copied to clipboard', {
|
||||
variant: SnackBarVariant.Success,
|
||||
icon: <IconCopy size={theme.icon.size.md} />,
|
||||
duration: 2000,
|
||||
});
|
||||
}}
|
||||
LeftIcon={IconCopy}
|
||||
text={t`Copy link to view`}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<div id="delete-view-menu-item">
|
||||
<SelectableListItem
|
||||
itemId="Delete view"
|
||||
onEnter={() => handleDelete()}
|
||||
>
|
||||
<MenuItem
|
||||
focused={selectedItemId === 'Delete view'}
|
||||
onClick={() => handleDelete()}
|
||||
LeftIcon={IconTrash}
|
||||
text={t`Delete view`}
|
||||
disabled={currentView?.key === 'INDEX'}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</div>
|
||||
{currentView?.key === 'INDEX' && (
|
||||
<AppTooltip
|
||||
anchorSelect={`#delete-view-menu-item`}
|
||||
content={t`Not available on Default View`}
|
||||
noArrow
|
||||
place="bottom"
|
||||
width="100%"
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
</SelectableList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,14 +1,19 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
|
||||
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
|
||||
import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector';
|
||||
import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort';
|
||||
import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
|
||||
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||
import {
|
||||
IconChevronLeft,
|
||||
IconHandMove,
|
||||
@ -32,6 +37,11 @@ export const ObjectOptionsDropdownRecordGroupSortContent = () => {
|
||||
setRecordGroupSort(sort);
|
||||
};
|
||||
|
||||
const selectedItemId = useRecoilComponentValueV2(
|
||||
selectedItemIdComponentState,
|
||||
OBJECT_OPTIONS_DROPDOWN_ID,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
currentContentId === 'hiddenRecordGroups' &&
|
||||
@ -41,6 +51,12 @@ export const ObjectOptionsDropdownRecordGroupSortContent = () => {
|
||||
}
|
||||
}, [hiddenRecordGroupIds, currentContentId, onContentChange]);
|
||||
|
||||
const selectableItemIdArray = [
|
||||
RecordGroupSort.Manual,
|
||||
RecordGroupSort.Alphabetical,
|
||||
RecordGroupSort.ReverseAlphabetical,
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader
|
||||
@ -54,28 +70,58 @@ export const ObjectOptionsDropdownRecordGroupSortContent = () => {
|
||||
Sort
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItemSelect
|
||||
onClick={() => handleRecordGroupSortChange(RecordGroupSort.Manual)}
|
||||
LeftIcon={IconHandMove}
|
||||
text={RecordGroupSort.Manual}
|
||||
selected={recordGroupSort === RecordGroupSort.Manual}
|
||||
/>
|
||||
<MenuItemSelect
|
||||
onClick={() =>
|
||||
handleRecordGroupSortChange(RecordGroupSort.Alphabetical)
|
||||
}
|
||||
LeftIcon={IconSortAZ}
|
||||
text={RecordGroupSort.Alphabetical}
|
||||
selected={recordGroupSort === RecordGroupSort.Alphabetical}
|
||||
/>
|
||||
<MenuItemSelect
|
||||
onClick={() =>
|
||||
handleRecordGroupSortChange(RecordGroupSort.ReverseAlphabetical)
|
||||
}
|
||||
LeftIcon={IconSortZA}
|
||||
text={RecordGroupSort.ReverseAlphabetical}
|
||||
selected={recordGroupSort === RecordGroupSort.ReverseAlphabetical}
|
||||
/>
|
||||
<SelectableList
|
||||
selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
|
||||
hotkeyScope={TableOptionsHotkeyScope.Dropdown}
|
||||
selectableItemIdArray={selectableItemIdArray}
|
||||
>
|
||||
<SelectableListItem
|
||||
itemId={RecordGroupSort.Manual}
|
||||
onEnter={() => handleRecordGroupSortChange(RecordGroupSort.Manual)}
|
||||
>
|
||||
<MenuItemSelect
|
||||
onClick={() =>
|
||||
handleRecordGroupSortChange(RecordGroupSort.Manual)
|
||||
}
|
||||
LeftIcon={IconHandMove}
|
||||
text={RecordGroupSort.Manual}
|
||||
selected={recordGroupSort === RecordGroupSort.Manual}
|
||||
focused={selectedItemId === RecordGroupSort.Manual}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem
|
||||
itemId={RecordGroupSort.Alphabetical}
|
||||
onEnter={() =>
|
||||
handleRecordGroupSortChange(RecordGroupSort.Alphabetical)
|
||||
}
|
||||
>
|
||||
<MenuItemSelect
|
||||
onClick={() =>
|
||||
handleRecordGroupSortChange(RecordGroupSort.Alphabetical)
|
||||
}
|
||||
LeftIcon={IconSortAZ}
|
||||
text={RecordGroupSort.Alphabetical}
|
||||
selected={recordGroupSort === RecordGroupSort.Alphabetical}
|
||||
focused={selectedItemId === RecordGroupSort.Alphabetical}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem
|
||||
itemId={RecordGroupSort.ReverseAlphabetical}
|
||||
onEnter={() =>
|
||||
handleRecordGroupSortChange(RecordGroupSort.ReverseAlphabetical)
|
||||
}
|
||||
>
|
||||
<MenuItemSelect
|
||||
onClick={() =>
|
||||
handleRecordGroupSortChange(RecordGroupSort.ReverseAlphabetical)
|
||||
}
|
||||
LeftIcon={IconSortZA}
|
||||
text={RecordGroupSort.ReverseAlphabetical}
|
||||
selected={recordGroupSort === RecordGroupSort.ReverseAlphabetical}
|
||||
focused={selectedItemId === RecordGroupSort.ReverseAlphabetical}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
|
||||
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
|
||||
import { RecordGroupReorderConfirmationModal } from '@/object-record/record-group/components/RecordGroupReorderConfirmationModal';
|
||||
import { RecordGroupsVisibilityDropdownSection } from '@/object-record/record-group/components/RecordGroupsVisibilityDropdownSection';
|
||||
@ -10,10 +11,14 @@ import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-gr
|
||||
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
|
||||
import { recordIndexRecordGroupHideComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState';
|
||||
import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
|
||||
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
||||
@ -89,6 +94,17 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
|
||||
}
|
||||
}, [hiddenRecordGroupIds, currentContentId, onContentChange]);
|
||||
|
||||
const selectedItemId = useRecoilComponentValueV2(
|
||||
selectedItemIdComponentState,
|
||||
OBJECT_OPTIONS_DROPDOWN_ID,
|
||||
);
|
||||
|
||||
const selectableItemIdArray = [
|
||||
...(currentView?.key !== 'INDEX' ? ['GroupBy', 'Sort'] : []),
|
||||
'HideEmptyGroups',
|
||||
...(hiddenRecordGroupIds.length > 0 ? ['HiddenGroups'] : []),
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader
|
||||
@ -102,31 +118,55 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
|
||||
Group
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuItemsContainer>
|
||||
{currentView?.key !== 'INDEX' && (
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => onContentChange('recordGroupFields')}
|
||||
LeftIcon={IconLayoutList}
|
||||
text={t`Group by`}
|
||||
contextualText={recordGroupFieldMetadata?.label}
|
||||
hasSubMenu
|
||||
<SelectableList
|
||||
selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
|
||||
hotkeyScope={TableOptionsHotkeyScope.Dropdown}
|
||||
selectableItemIdArray={selectableItemIdArray}
|
||||
>
|
||||
{currentView?.key !== 'INDEX' && (
|
||||
<>
|
||||
<SelectableListItem
|
||||
itemId="GroupBy"
|
||||
onEnter={() => onContentChange('recordGroupFields')}
|
||||
>
|
||||
<MenuItem
|
||||
focused={selectedItemId === 'GroupBy'}
|
||||
onClick={() => onContentChange('recordGroupFields')}
|
||||
LeftIcon={IconLayoutList}
|
||||
text={t`Group by`}
|
||||
contextualText={recordGroupFieldMetadata?.label}
|
||||
hasSubMenu
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem
|
||||
itemId="Sort"
|
||||
onEnter={() => onContentChange('recordGroupSort')}
|
||||
>
|
||||
<MenuItem
|
||||
focused={selectedItemId === 'Sort'}
|
||||
onClick={() => onContentChange('recordGroupSort')}
|
||||
LeftIcon={IconSortDescending}
|
||||
text={t`Sort`}
|
||||
contextualText={recordGroupSort}
|
||||
hasSubMenu
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</>
|
||||
)}
|
||||
<SelectableListItem
|
||||
itemId="HideEmptyGroups"
|
||||
onEnter={() => handleHideEmptyRecordGroupChange()}
|
||||
>
|
||||
<MenuItemToggle
|
||||
focused={selectedItemId === 'HideEmptyGroups'}
|
||||
LeftIcon={IconCircleOff}
|
||||
onToggleChange={handleHideEmptyRecordGroupChange}
|
||||
toggled={hideEmptyRecordGroup}
|
||||
text={t`Hide empty groups`}
|
||||
toggleSize="small"
|
||||
/>
|
||||
<MenuItem
|
||||
onClick={() => onContentChange('recordGroupSort')}
|
||||
LeftIcon={IconSortDescending}
|
||||
text={t`Sort`}
|
||||
contextualText={recordGroupSort}
|
||||
hasSubMenu
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<MenuItemToggle
|
||||
LeftIcon={IconCircleOff}
|
||||
onToggleChange={handleHideEmptyRecordGroupChange}
|
||||
toggled={hideEmptyRecordGroup}
|
||||
text={t`Hide empty groups`}
|
||||
toggleSize="small"
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
{visibleRecordGroupIds.length > 0 && (
|
||||
<>
|
||||
@ -145,11 +185,16 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<MenuItemNavigate
|
||||
onClick={() => onContentChange('hiddenRecordGroups')}
|
||||
LeftIcon={IconEyeOff}
|
||||
text={`Hidden ${recordGroupFieldMetadata?.label ?? ''}`}
|
||||
/>
|
||||
<SelectableListItem
|
||||
itemId="HiddenGroups"
|
||||
onEnter={() => onContentChange('hiddenRecordGroups')}
|
||||
>
|
||||
<MenuItemNavigate
|
||||
onClick={() => onContentChange('hiddenRecordGroups')}
|
||||
LeftIcon={IconEyeOff}
|
||||
text={`Hidden ${recordGroupFieldMetadata?.label ?? ''}`}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user