fix: scroll dropdown listing in hidden fields (#8738)

Fixes: #8716 

[Screencast from 2024-11-25
22-06-24.webm](https://github.com/user-attachments/assets/35bd66cc-942f-4903-abda-0d67a75b6582)

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Harsh Singh
2024-11-27 22:36:11 +05:30
committed by GitHub
parent a9cb1e9b0d
commit 3ad1113173
9 changed files with 106 additions and 88 deletions

View File

@ -15,6 +15,7 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { onToggleColumnFilterComponentState } from '@/object-record/record-table/states/onToggleColumnFilterComponentState';
import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useTableColumns } from '../../hooks/useTableColumns';
import { ColumnDefinition } from '../../types/ColumnDefinition';
@ -91,43 +92,45 @@ export const RecordTableColumnHeadDropdownMenu = ({
const canHide = column.isLabelIdentifier !== true;
return (
<DropdownMenuItemsContainer>
{isFilterable && (
<MenuItem
LeftIcon={IconFilter}
onClick={handleFilterClick}
text="Filter"
/>
)}
{isSortable && (
<MenuItem
LeftIcon={IconSortDescending}
onClick={handleSortClick}
text="Sort"
/>
)}
{showSeparator && <DropdownMenuSeparator />}
{canMoveLeft && (
<MenuItem
LeftIcon={IconArrowLeft}
onClick={handleColumnMoveLeft}
text="Move left"
/>
)}
{canMoveRight && (
<MenuItem
LeftIcon={IconArrowRight}
onClick={handleColumnMoveRight}
text="Move right"
/>
)}
{canHide && (
<MenuItem
LeftIcon={IconEyeOff}
onClick={handleColumnVisibility}
text="Hide"
/>
)}
</DropdownMenuItemsContainer>
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<DropdownMenuItemsContainer>
{isFilterable && (
<MenuItem
LeftIcon={IconFilter}
onClick={handleFilterClick}
text="Filter"
/>
)}
{isSortable && (
<MenuItem
LeftIcon={IconSortDescending}
onClick={handleSortClick}
text="Sort"
/>
)}
{showSeparator && <DropdownMenuSeparator />}
{canMoveLeft && (
<MenuItem
LeftIcon={IconArrowLeft}
onClick={handleColumnMoveLeft}
text="Move left"
/>
)}
{canMoveRight && (
<MenuItem
LeftIcon={IconArrowRight}
onClick={handleColumnMoveRight}
text="Move right"
/>
)}
{canHide && (
<MenuItem
LeftIcon={IconEyeOff}
onClick={handleColumnVisibility}
text="Hide"
/>
)}
</DropdownMenuItemsContainer>
</ScrollWrapper>
);
};

View File

@ -65,6 +65,8 @@ const StyledColumnHeaderCell = styled.th<{
}`;
}
}};
// TODO: refactor this, each component should own its CSS
div {
overflow: hidden;
}

View File

@ -13,6 +13,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordTableHeaderPlusButtonContent = () => {
@ -41,21 +42,19 @@ export const RecordTableHeaderPlusButtonContent = () => {
return (
<>
{hiddenTableColumns.length > 0 && (
<>
<DropdownMenuItemsContainer>
{hiddenTableColumns.map((column) => (
<MenuItem
key={column.fieldMetadataId}
onClick={() => handleAddColumn(column)}
LeftIcon={getIcon(column.iconName)}
text={column.label}
/>
))}
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
</>
)}
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<DropdownMenuItemsContainer>
{hiddenTableColumns.map((column) => (
<MenuItem
key={column.fieldMetadataId}
onClick={() => handleAddColumn(column)}
LeftIcon={getIcon(column.iconName)}
text={column.label}
/>
))}
</DropdownMenuItemsContainer>
</ScrollWrapper>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<UndecoratedLink
fullWidth

View File

@ -1,3 +1,8 @@
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
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 { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import {
autoUpdate,
flip,
@ -8,21 +13,19 @@ import {
useFloating,
} from '@floating-ui/react';
import { MouseEvent, ReactNode, useEffect, useRef } from 'react';
import { flushSync } from 'react-dom';
import { Keys } from 'react-hotkeys-hook';
import { Key } from 'ts-key-enum';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
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 { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { isDefined } from '~/utils/isDefined';
import { useDropdown } from '../hooks/useDropdown';
import { useInternalHotkeyScopeManagement } from '../hooks/useInternalHotkeyScopeManagement';
import { DropdownUnmountEffect } from '@/ui/layout/dropdown/components/DropdownUnmountEffect';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext';
import { dropdownMaxHeightComponentStateV2 } from '@/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2';
import { useListenClickOutsideV2 } from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { DropdownMenu } from './DropdownMenu';
import { DropdownOnToggleEffect } from './DropdownOnToggleEffect';
@ -76,6 +79,11 @@ export const Dropdown = ({
const offsetMiddlewares = [];
const [dropdownMaxHeight, setDropdownMaxHeight] = useRecoilComponentStateV2(
dropdownMaxHeightComponentStateV2,
dropdownId,
);
if (isDefined(dropdownOffset.x)) {
offsetMiddlewares.push(offset({ crossAxis: dropdownOffset.x }));
}
@ -90,13 +98,10 @@ export const Dropdown = ({
flip(),
size({
padding: 32,
apply: ({ availableHeight, elements }) => {
elements.floating.style.maxHeight =
availableHeight >= elements.floating.scrollHeight
? ''
: `${availableHeight}px`;
elements.floating.style.height = 'auto';
apply: ({ availableHeight }) => {
flushSync(() => {
setDropdownMaxHeight(availableHeight);
});
},
boundary: document.querySelector('#root') ?? undefined,
}),
@ -149,8 +154,15 @@ export const Dropdown = ({
[closeDropdown, isDropdownOpen],
);
const dropdownMenuStyles = {
...floatingStyles,
maxHeight: dropdownMaxHeight,
};
return (
<>
<DropdownComponentInstanceContext.Provider
value={{ instanceId: dropdownId }}
>
<DropdownScope dropdownScopeId={getScopeIdFromComponentId(dropdownId)}>
<div ref={containerRef} className={className}>
{clickableComponent && (
@ -175,7 +187,7 @@ export const Dropdown = ({
width={dropdownMenuWidth ?? dropdownWidth}
data-select-disable
ref={refs.setFloating}
style={floatingStyles}
style={dropdownMenuStyles}
>
{dropdownComponents}
</DropdownMenu>
@ -187,7 +199,7 @@ export const Dropdown = ({
width={dropdownMenuWidth ?? dropdownWidth}
data-select-disable
ref={refs.setFloating}
style={floatingStyles}
style={dropdownMenuStyles}
>
{dropdownComponents}
</DropdownMenu>
@ -199,6 +211,6 @@ export const Dropdown = ({
</div>
</DropdownScope>
<DropdownUnmountEffect dropdownId={dropdownId} />
</>
</DropdownComponentInstanceContext.Provider>
);
};

View File

@ -26,6 +26,8 @@ const StyledDropdownMenu = styled.div<{
z-index: 30;
width: ${({ width = 200 }) =>
typeof width === 'number' ? `${width}px` : width};
overflow: hidden;
`;
export const DropdownMenu = StyledDropdownMenu;

View File

@ -1,7 +1,5 @@
import styled from '@emotion/styled';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
const StyledDropdownMenuItemsExternalContainer = styled.div<{
hasMaxHeight?: boolean;
}>`
@ -18,10 +16,6 @@ const StyledDropdownMenuItemsExternalContainer = styled.div<{
width: calc(100% - 2 * var(--padding));
`;
const StyledScrollWrapper = styled(ScrollWrapper)`
width: 100%;
`;
const StyledDropdownMenuItemsInternalContainer = styled.div`
align-items: stretch;
display: flex;
@ -48,17 +42,9 @@ export const DropdownMenuItemsContainer = ({
hasMaxHeight={hasMaxHeight}
className={className}
>
{hasMaxHeight ? (
<StyledScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<StyledDropdownMenuItemsInternalContainer>
{children}
</StyledDropdownMenuItemsInternalContainer>
</StyledScrollWrapper>
) : (
<StyledDropdownMenuItemsInternalContainer>
{children}
</StyledDropdownMenuItemsInternalContainer>
)}
<StyledDropdownMenuItemsInternalContainer>
{children}
</StyledDropdownMenuItemsInternalContainer>
</StyledDropdownMenuItemsExternalContainer>
);
};

View File

@ -2,7 +2,7 @@ import styled from '@emotion/styled';
const StyledDropdownMenuSeparator = styled.div`
background-color: ${({ theme }) => theme.border.color.light};
height: 1px;
min-height: 1px;
width: 100%;
`;

View File

@ -0,0 +1,4 @@
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
export const DropdownComponentInstanceContext =
createComponentInstanceContext();

View File

@ -0,0 +1,10 @@
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const dropdownMaxHeightComponentStateV2 = createComponentStateV2<
number | undefined
>({
key: 'dropdownMaxHeightComponentStateV2',
componentInstanceContext: DropdownComponentInstanceContext,
defaultValue: undefined,
});