diff --git a/package.json b/package.json index 1af5ec92f..91b72f562 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "@rc-component/trigger": "^2.1.1", "classnames": "2.x", "rc-motion": "^2.0.1", - "rc-overflow": "^1.3.1", + "rc-overflow": "^1.4.0", "rc-util": "^5.16.1", "rc-virtual-list": "^3.5.2" }, diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index a2a15e8c7..786583ede 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -68,6 +68,7 @@ export type CustomTagProps = { onClose: (event?: React.MouseEvent) => void; closable: boolean; isMaxTag: boolean; + index: number; }; export interface BaseSelectRef { @@ -135,7 +136,7 @@ export interface BaseSelectProps extends BaseSelectPrivateProps, React.AriaAttri tagRender?: (props: CustomTagProps) => React.ReactElement; direction?: 'ltr' | 'rtl'; maxLength?: number; - + showScrollBar?: boolean | 'optional'; // MISC tabIndex?: number; autoFocus?: boolean; @@ -222,7 +223,7 @@ const BaseSelect = React.forwardRef((props, ref) tagRender, direction, omitDomProps, - + showScrollBar = 'optional', // Value displayValues, onDisplayValuesChange, @@ -685,6 +686,7 @@ const BaseSelect = React.forwardRef((props, ref) showSearch: mergedShowSearch, multiple, toggleOpen: onToggleOpen, + showScrollBar, }), [props, notFoundContent, triggerOpen, mergedOpen, id, mergedShowSearch, multiple, onToggleOpen], ); diff --git a/src/OptionList.tsx b/src/OptionList.tsx index d4c0109fb..7932cf34f 100644 --- a/src/OptionList.tsx +++ b/src/OptionList.tsx @@ -44,6 +44,7 @@ const OptionList: React.ForwardRefRenderFunction = (_, r toggleOpen, notFoundContent, onPopupScroll, + showScrollBar, } = useBaseProps(); const { maxCount, @@ -325,6 +326,7 @@ const OptionList: React.ForwardRefRenderFunction = (_, r virtual={virtual} direction={direction} innerProps={virtual ? null : a11yProps} + showScrollBar={showScrollBar} > {(item, itemIndex) => { const { group, groupOption, data, label, value } = item; diff --git a/src/Selector/MultipleSelector.tsx b/src/Selector/MultipleSelector.tsx index 5c7fac704..e6a2643fd 100644 --- a/src/Selector/MultipleSelector.tsx +++ b/src/Selector/MultipleSelector.tsx @@ -131,6 +131,7 @@ const SelectSelector: React.FC = (props) => { closable?: boolean, onClose?: React.MouseEventHandler, isMaxTag?: boolean, + info?: { index: number }, ) => { const onMouseDown = (e: React.MouseEvent) => { onPreventMouseDown(e); @@ -141,6 +142,7 @@ const SelectSelector: React.FC = (props) => { {tagRender({ label: content, value, + index: info?.index, disabled: itemDisabled, closable, onClose, @@ -150,7 +152,7 @@ const SelectSelector: React.FC = (props) => { ); }; - const renderItem = (valueItem: DisplayValueType) => { + const renderItem = (valueItem: DisplayValueType, info: { index: number }) => { const { disabled: itemDisabled, label, value } = valueItem; const closable = !disabled && !itemDisabled; @@ -173,7 +175,15 @@ const SelectSelector: React.FC = (props) => { }; return typeof tagRender === 'function' - ? customizeRenderSelector(value, displayLabel, itemDisabled, closable, onClose) + ? customizeRenderSelector( + value, + displayLabel, + itemDisabled, + closable, + onClose, + undefined, + info, + ) : defaultRenderSelector(valueItem, displayLabel, itemDisabled, closable, onClose); }; diff --git a/src/interface.ts b/src/interface.ts index 0f3b3eefa..1f7df705d 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -16,6 +16,7 @@ export interface DisplayValueType { label?: React.ReactNode; title?: React.ReactNode; disabled?: boolean; + index?: number; } export type RenderNode = React.ReactNode | ((props: any) => React.ReactNode); diff --git a/tests/ListScrollBar.test.tsx b/tests/ListScrollBar.test.tsx new file mode 100644 index 000000000..99c5055c7 --- /dev/null +++ b/tests/ListScrollBar.test.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { spyElementPrototypes } from './utils/domHook'; +import Select from '../src'; + +jest.mock('../src/utils/platformUtil'); +// Mock VirtualList +jest.mock('rc-virtual-list', () => { + const OriReact = jest.requireActual('react'); + const OriList = jest.requireActual('rc-virtual-list').default; + + return OriReact.forwardRef((props, ref) => { + const oriRef = OriReact.useRef(); + + OriReact.useImperativeHandle(ref, () => ({ + ...oriRef.current, + scrollTo: (arg) => { + global.scrollToArgs = arg; + oriRef.current.scrollTo(arg); + }, + })); + + return ; + }); +}); + +describe('List.Scroll', () => { + let mockElement; + let boundingRect = { + top: 0, + bottom: 0, + width: 100, + height: 50, + }; + + beforeAll(() => { + // Mock the required properties + mockElement = spyElementPrototypes(HTMLElement, { + offsetHeight: { + get: () => 100, // Ensure this indicates there is enough height for content + }, + clientHeight: { + get: () => 50, // This is typically the visible height + }, + getBoundingClientRect: () => boundingRect, // Setup for the bounding rectangle + offsetParent: { + get: () => document.body, + }, + }); + }); + + afterAll(() => { + mockElement.mockRestore(); + }); + + beforeEach(() => { + boundingRect = { + top: 0, + bottom: 0, + width: 100, + height: 50, + }; + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should show scrollbar when showScrollBar is true', async () => { + const options = Array.from({ length: 10 }, (_, index) => ({ + label: `${index + 1}`, + value: `${index + 1}`, + })); + + // Render the Select component with a scrollbar + const { container } = render(, + ); + values.forEach((value, index) => { + const expectedText = `.${value}-${index}-test`; + const nodes = container.querySelectorAll(expectedText); + expect(nodes).toHaveLength(1); + }); + }); + it('disabled', () => { const tagRender = jest.fn(); render( diff --git a/tests/utils/domHook.ts b/tests/utils/domHook.ts new file mode 100644 index 000000000..61e9c76cf --- /dev/null +++ b/tests/utils/domHook.ts @@ -0,0 +1,61 @@ +/* eslint-disable no-param-reassign */ +const NO_EXIST = { __NOT_EXIST: true }; + +export function spyElementPrototypes(Element, properties) { + const propNames = Object.keys(properties); + const originDescriptors = {}; + + propNames.forEach((propName) => { + const originDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, propName); + originDescriptors[propName] = originDescriptor || NO_EXIST; + + const spyProp = properties[propName]; + + if (typeof spyProp === 'function') { + // If is a function + Element.prototype[propName] = function spyFunc(...args) { + return spyProp.call(this, originDescriptor, ...args); + }; + } else { + // Otherwise tread as a property + Object.defineProperty(Element.prototype, propName, { + ...spyProp, + set(value) { + if (spyProp.set) { + return spyProp.set.call(this, originDescriptor, value); + } + return originDescriptor.set(value); + }, + get() { + if (spyProp.get) { + return spyProp.get.call(this, originDescriptor); + } + return originDescriptor.get(); + }, + configurable: true, + }); + } + }); + + return { + mockRestore() { + propNames.forEach((propName) => { + const originDescriptor = originDescriptors[propName]; + if (originDescriptor === NO_EXIST) { + delete Element.prototype[propName]; + } else if (typeof originDescriptor === 'function') { + Element.prototype[propName] = originDescriptor; + } else { + Object.defineProperty(Element.prototype, propName, originDescriptor); + } + }); + }, + }; +} + +export function spyElementPrototype(Element, propName, property) { + return spyElementPrototypes(Element, { + [propName]: property, + }); +} +/* eslint-enable no-param-reassign */