Skip to content

Commit 83e4fa0

Browse files
[sync] fix(sdk): restore filter dropdown wheel scrolling T2325 (#1515) (#2822)
Synced from teableio/teable-ee@44552eb Co-authored-by: nichenqin <nichenqin@hotmail.com>
1 parent a72f9bb commit 83e4fa0

5 files changed

Lines changed: 75 additions & 6 deletions

File tree

packages/sdk/src/components/filter/view-filter/component/base/BaseMultipleSelect.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ import {
1414

1515
import { debounce } from 'lodash';
1616
import { Check, ChevronDown } from 'lucide-react';
17-
import { useState, useMemo, useCallback, useEffect } from 'react';
17+
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
1818
import { useTranslation } from '../../../../../context/app/i18n';
1919
import type { IOption, IBaseMultipleSelect } from './types';
20+
import { scrollListByWheel } from './wheel-scroll-list';
2021

2122
function BaseMultipleSelect<V extends string, O extends IOption<V> = IOption<V>>(
2223
props: IBaseMultipleSelect<V, O>
@@ -37,6 +38,7 @@ function BaseMultipleSelect<V extends string, O extends IOption<V> = IOption<V>>
3738
modal,
3839
} = props;
3940
const [open, setOpen] = useState(false);
41+
const listRef = useRef<HTMLDivElement>(null);
4042
const [searchValue, setSearchValue] = useState('');
4143
const [isComposing, setIsComposing] = useState(false);
4244

@@ -128,13 +130,17 @@ function BaseMultipleSelect<V extends string, O extends IOption<V> = IOption<V>>
128130
/>
129131
</Button>
130132
</PopoverTrigger>
131-
<PopoverContent align="start" className={cn('p-1', popoverClassName)}>
133+
<PopoverContent
134+
align="start"
135+
className={cn('p-1', popoverClassName)}
136+
onWheelCapture={(event) => scrollListByWheel(event, listRef.current)}
137+
>
132138
<Command
133139
className="rounded-sm"
134140
filter={onSearch ? undefined : commandFilter}
135141
shouldFilter={!onSearch}
136142
>
137-
<CommandList className="mt-1">
143+
<CommandList ref={listRef} className="mt-1">
138144
<CommandInput
139145
placeholder={t('common.search.placeholder')}
140146
className="placeholder:text-[13px]"

packages/sdk/src/components/filter/view-filter/component/base/BaseSingleSelect.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ import {
1313
} from '@teable/ui-lib';
1414
import { debounce } from 'lodash';
1515
import { Check, ChevronDown } from 'lucide-react';
16-
import { useState, useMemo, useCallback, useEffect } from 'react';
16+
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
1717
import { useTranslation } from '../../../../../context/app/i18n';
1818
import type { IOption, IBaseSelect } from './types';
19+
import { scrollListByWheel } from './wheel-scroll-list';
1920

2021
function BaseSingleSelect<V extends string, O extends IOption<V> = IOption<V>>(
2122
props: IBaseSelect<V, O>
@@ -44,6 +45,7 @@ function BaseSingleSelect<V extends string, O extends IOption<V> = IOption<V>>(
4445
groupHeading,
4546
} = props;
4647
const [open, setOpen] = useState(false);
48+
const listRef = useRef<HTMLDivElement>(null);
4749

4850
const label = useMemo(() => {
4951
return options.find((option) => option.value === value)?.label || defaultLabel;
@@ -139,7 +141,11 @@ function BaseSingleSelect<V extends string, O extends IOption<V> = IOption<V>>(
139141
/>
140142
</Button>
141143
</PopoverTrigger>
142-
<PopoverContent align="start" className={cn('p-1', popoverClassName)}>
144+
<PopoverContent
145+
align="start"
146+
className={cn('p-1', popoverClassName)}
147+
onWheelCapture={(event) => scrollListByWheel(event, listRef.current)}
148+
>
143149
<Command filter={onSearch ? undefined : commandFilter} shouldFilter={!onSearch}>
144150
{search ? (
145151
<CommandInput
@@ -151,7 +157,7 @@ function BaseSingleSelect<V extends string, O extends IOption<V> = IOption<V>>(
151157
/>
152158
) : null}
153159
<CommandEmpty>{notFoundText}</CommandEmpty>
154-
<CommandList className="mt-1">
160+
<CommandList ref={listRef} className="mt-1">
155161
{groupHeading ? (
156162
<CommandGroup heading={groupHeading}>{renderOptions()}</CommandGroup>
157163
) : (

packages/sdk/src/components/filter/view-filter/component/base/__tests__/BaseMultipleSelect.test.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,27 @@ describe('BaseMultipleSelect', () => {
6464
fireEvent.click(screen.getAllByRole('option')[1]);
6565
expect(selectHandler).toHaveBeenCalledTimes(2);
6666
});
67+
68+
it('should scroll the option list when wheeling over the popover container', async () => {
69+
const longOptions = Array.from({ length: 20 }, (_, index) => ({
70+
label: `label-${index}`,
71+
value: `value-${index}`,
72+
}));
73+
74+
render(<BaseMultipleSelect options={longOptions} onSelect={onSelect} value={null} />, {
75+
wrapper: createAppContext(),
76+
});
77+
toggleOpen();
78+
79+
const dialog = screen.getByRole('dialog');
80+
const list = screen.getByRole('listbox') as HTMLDivElement;
81+
82+
Object.defineProperty(list, 'clientHeight', { configurable: true, value: 272 });
83+
Object.defineProperty(list, 'scrollHeight', { configurable: true, value: 1200 });
84+
Object.defineProperty(list, 'scrollTop', { configurable: true, value: 0, writable: true });
85+
86+
fireEvent.wheel(dialog, { deltaY: 120 });
87+
88+
expect(list.scrollTop).toBe(120);
89+
});
6790
});

packages/sdk/src/components/filter/view-filter/component/base/__tests__/BaseSingleSelect.test.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,27 @@ describe('BaseSingleSelect', () => {
9090
expect(option).toHaveLength(1);
9191
expect(option[0]).toHaveTextContent('label-1');
9292
});
93+
94+
it('should scroll the option list when wheeling over the popover container', async () => {
95+
const longOptions = Array.from({ length: 20 }, (_, index) => ({
96+
label: `label-${index}`,
97+
value: `value-${index}`,
98+
}));
99+
100+
render(<BaseSingleSelect options={longOptions} onSelect={onSelect} value={null} />, {
101+
wrapper: createAppContext(),
102+
});
103+
toggleOpen();
104+
105+
const dialog = screen.getByRole('dialog');
106+
const list = screen.getByRole('listbox') as HTMLDivElement;
107+
108+
Object.defineProperty(list, 'clientHeight', { configurable: true, value: 272 });
109+
Object.defineProperty(list, 'scrollHeight', { configurable: true, value: 1200 });
110+
Object.defineProperty(list, 'scrollTop', { configurable: true, value: 0, writable: true });
111+
112+
fireEvent.wheel(dialog, { deltaY: 120 });
113+
114+
expect(list.scrollTop).toBe(120);
115+
});
93116
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { WheelEvent } from 'react';
2+
3+
export const scrollListByWheel = (event: WheelEvent<HTMLElement>, list: HTMLElement | null) => {
4+
if (!list || event.deltaY === 0 || list.scrollHeight <= list.clientHeight) {
5+
return;
6+
}
7+
8+
event.preventDefault();
9+
event.stopPropagation();
10+
list.scrollTop += event.deltaY;
11+
};

0 commit comments

Comments
 (0)