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

View File

@ -65,6 +65,8 @@ const StyledColumnHeaderCell = styled.th<{
}`; }`;
} }
}}; }};
// TODO: refactor this, each component should own its CSS
div { div {
overflow: hidden; 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 { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; 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'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordTableHeaderPlusButtonContent = () => { export const RecordTableHeaderPlusButtonContent = () => {
@ -41,21 +42,19 @@ export const RecordTableHeaderPlusButtonContent = () => {
return ( return (
<> <>
{hiddenTableColumns.length > 0 && ( <ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<> <DropdownMenuItemsContainer>
<DropdownMenuItemsContainer> {hiddenTableColumns.map((column) => (
{hiddenTableColumns.map((column) => ( <MenuItem
<MenuItem key={column.fieldMetadataId}
key={column.fieldMetadataId} onClick={() => handleAddColumn(column)}
onClick={() => handleAddColumn(column)} LeftIcon={getIcon(column.iconName)}
LeftIcon={getIcon(column.iconName)} text={column.label}
text={column.label} />
/> ))}
))} </DropdownMenuItemsContainer>
</DropdownMenuItemsContainer> </ScrollWrapper>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
</>
)}
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
<UndecoratedLink <UndecoratedLink
fullWidth 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 { import {
autoUpdate, autoUpdate,
flip, flip,
@ -8,21 +13,19 @@ import {
useFloating, useFloating,
} from '@floating-ui/react'; } from '@floating-ui/react';
import { MouseEvent, ReactNode, useEffect, useRef } from 'react'; import { MouseEvent, ReactNode, useEffect, useRef } from 'react';
import { flushSync } from 'react-dom';
import { Keys } from 'react-hotkeys-hook'; import { Keys } from 'react-hotkeys-hook';
import { Key } from 'ts-key-enum'; 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 { isDefined } from '~/utils/isDefined';
import { useDropdown } from '../hooks/useDropdown'; import { useDropdown } from '../hooks/useDropdown';
import { useInternalHotkeyScopeManagement } from '../hooks/useInternalHotkeyScopeManagement'; import { useInternalHotkeyScopeManagement } from '../hooks/useInternalHotkeyScopeManagement';
import { DropdownUnmountEffect } from '@/ui/layout/dropdown/components/DropdownUnmountEffect'; 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 { useListenClickOutsideV2 } from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { DropdownMenu } from './DropdownMenu'; import { DropdownMenu } from './DropdownMenu';
import { DropdownOnToggleEffect } from './DropdownOnToggleEffect'; import { DropdownOnToggleEffect } from './DropdownOnToggleEffect';
@ -76,6 +79,11 @@ export const Dropdown = ({
const offsetMiddlewares = []; const offsetMiddlewares = [];
const [dropdownMaxHeight, setDropdownMaxHeight] = useRecoilComponentStateV2(
dropdownMaxHeightComponentStateV2,
dropdownId,
);
if (isDefined(dropdownOffset.x)) { if (isDefined(dropdownOffset.x)) {
offsetMiddlewares.push(offset({ crossAxis: dropdownOffset.x })); offsetMiddlewares.push(offset({ crossAxis: dropdownOffset.x }));
} }
@ -90,13 +98,10 @@ export const Dropdown = ({
flip(), flip(),
size({ size({
padding: 32, padding: 32,
apply: ({ availableHeight, elements }) => { apply: ({ availableHeight }) => {
elements.floating.style.maxHeight = flushSync(() => {
availableHeight >= elements.floating.scrollHeight setDropdownMaxHeight(availableHeight);
? '' });
: `${availableHeight}px`;
elements.floating.style.height = 'auto';
}, },
boundary: document.querySelector('#root') ?? undefined, boundary: document.querySelector('#root') ?? undefined,
}), }),
@ -149,8 +154,15 @@ export const Dropdown = ({
[closeDropdown, isDropdownOpen], [closeDropdown, isDropdownOpen],
); );
const dropdownMenuStyles = {
...floatingStyles,
maxHeight: dropdownMaxHeight,
};
return ( return (
<> <DropdownComponentInstanceContext.Provider
value={{ instanceId: dropdownId }}
>
<DropdownScope dropdownScopeId={getScopeIdFromComponentId(dropdownId)}> <DropdownScope dropdownScopeId={getScopeIdFromComponentId(dropdownId)}>
<div ref={containerRef} className={className}> <div ref={containerRef} className={className}>
{clickableComponent && ( {clickableComponent && (
@ -175,7 +187,7 @@ export const Dropdown = ({
width={dropdownMenuWidth ?? dropdownWidth} width={dropdownMenuWidth ?? dropdownWidth}
data-select-disable data-select-disable
ref={refs.setFloating} ref={refs.setFloating}
style={floatingStyles} style={dropdownMenuStyles}
> >
{dropdownComponents} {dropdownComponents}
</DropdownMenu> </DropdownMenu>
@ -187,7 +199,7 @@ export const Dropdown = ({
width={dropdownMenuWidth ?? dropdownWidth} width={dropdownMenuWidth ?? dropdownWidth}
data-select-disable data-select-disable
ref={refs.setFloating} ref={refs.setFloating}
style={floatingStyles} style={dropdownMenuStyles}
> >
{dropdownComponents} {dropdownComponents}
</DropdownMenu> </DropdownMenu>
@ -199,6 +211,6 @@ export const Dropdown = ({
</div> </div>
</DropdownScope> </DropdownScope>
<DropdownUnmountEffect dropdownId={dropdownId} /> <DropdownUnmountEffect dropdownId={dropdownId} />
</> </DropdownComponentInstanceContext.Provider>
); );
}; };

View File

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

View File

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

View File

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