From 83717a6e14c5312c8565a676402544f9dd0a2df2 Mon Sep 17 00:00:00 2001 From: lijianan <574980606@qq.com> Date: Mon, 13 Apr 2026 12:00:47 +0800 Subject: [PATCH] feat: enhance panelRender API support components --- docs/examples/panelRender.tsx | 4 +- src/PickerInput/Popup/PopupPanel.tsx | 68 +++++++++++++------------ src/PickerInput/Popup/index.tsx | 76 ++++++++++++++++++++++++++-- src/interface.tsx | 19 ++++++- tests/picker.spec.tsx | 25 +++++++++ 5 files changed, 155 insertions(+), 37 deletions(-) diff --git a/docs/examples/panelRender.tsx b/docs/examples/panelRender.tsx index f44065e4b..77f29ded0 100644 --- a/docs/examples/panelRender.tsx +++ b/docs/examples/panelRender.tsx @@ -25,7 +25,7 @@ export default () => { locale={zhCN} allowClear defaultValue={defaultStartValue} - panelRender={(node) => ( + panelRender={(node, { components: { Panel } }) => ( <> - + {customizeNode ? My Panel : node} )} diff --git a/src/PickerInput/Popup/PopupPanel.tsx b/src/PickerInput/Popup/PopupPanel.tsx index e59b4fba3..478cf8cc2 100644 --- a/src/PickerInput/Popup/PopupPanel.tsx +++ b/src/PickerInput/Popup/PopupPanel.tsx @@ -18,19 +18,44 @@ export type PopupPanelProps = MustProp onPickerValueChange: (date: DateType) => void; }; +export function getPopupPanelSharedContext( + needConfirm: boolean, + onSubmit: VoidFunction, +): PickerHackContextProps { + return { + onCellDblClick: () => { + if (needConfirm) { + onSubmit(); + } + }, + }; +} + +export function getPopupPanelPickerProps( + props: PopupPanelProps, +): PickerPanelProps { + const { picker, hoverValue, range } = props; + + const pickerProps = { + ...props, + hoverValue: null, + hoverRangeValue: null, + hideHeader: picker === 'time', + } as PickerPanelProps; + + if (range) { + pickerProps.hoverRangeValue = hoverValue as PickerPanelProps['hoverRangeValue']; + } else { + pickerProps.hoverValue = hoverValue as PickerPanelProps['hoverValue']; + } + + return pickerProps; +} + export default function PopupPanel( props: PopupPanelProps, ) { - const { - picker, - multiplePanel, - pickerValue, - onPickerValueChange, - needConfirm, - onSubmit, - range, - hoverValue, - } = props; + const { picker, multiplePanel, pickerValue, onPickerValueChange, needConfirm, onSubmit } = props; const { prefixCls, generateConfig } = React.useContext(PickerContext); // ======================== Offset ======================== @@ -52,29 +77,10 @@ export default function PopupPanel( }; // ======================= Context ======================== - const sharedContext: PickerHackContextProps = { - onCellDblClick: () => { - if (needConfirm) { - onSubmit(); - } - }, - }; - - const hideHeader = picker === 'time'; + const sharedContext = getPopupPanelSharedContext(needConfirm, onSubmit); // ======================== Props ========================= - const pickerProps = { - ...props, - hoverValue: null, - hoverRangeValue: null, - hideHeader, - }; - - if (range) { - pickerProps.hoverRangeValue = hoverValue; - } else { - pickerProps.hoverValue = hoverValue; - } + const pickerProps = getPopupPanelPickerProps(props); // ======================== Render ======================== // Multiple diff --git a/src/PickerInput/Popup/index.tsx b/src/PickerInput/Popup/index.tsx index f02810390..aa0709071 100644 --- a/src/PickerInput/Popup/index.tsx +++ b/src/PickerInput/Popup/index.tsx @@ -2,17 +2,75 @@ import { clsx } from 'clsx'; import ResizeObserver, { type ResizeObserverProps } from '@rc-component/resize-observer'; import * as React from 'react'; import type { + PanelRenderPanelProps, RangeTimeProps, SharedPickerProps, SharedTimeProps, ValueDate, } from '../../interface'; +import PickerPanel, { type PickerPanelProps, type PickerPanelRef } from '../../PickerPanel'; +import { PickerHackContext, type PickerHackContextProps } from '../../PickerPanel/context'; import { toArray } from '../../utils/miscUtil'; import PickerContext from '../context'; import Footer, { type FooterProps } from './Footer'; -import PopupPanel, { type PopupPanelProps } from './PopupPanel'; +import PopupPanel, { + getPopupPanelPickerProps, + getPopupPanelSharedContext, + type PopupPanelProps, +} from './PopupPanel'; import PresetPanel from './PresetPanel'; +interface PanelRenderContextProps { + pickerProps: PickerPanelProps; + sharedContext: PickerHackContextProps; +} + +const PanelRenderContext = React.createContext(null!); + +const InternalPanelRenderPanel = React.forwardRef( + ( + props: PanelRenderPanelProps, + ref: React.Ref, + ) => { + const context = React.useContext>(PanelRenderContext); + + if (!context) { + return )} />; + } + + const { pickerProps, sharedContext } = context; + const mergedPicker = props.picker ?? pickerProps.picker; + const mergedProps = { + ...pickerProps, + ...props, + picker: mergedPicker, + mode: props.mode ?? (props.picker !== undefined ? mergedPicker : pickerProps.mode), + hideHeader: props.hideHeader ?? mergedPicker === 'time', + components: props.components + ? { ...pickerProps.components, ...props.components } + : pickerProps.components, + classNames: props.classNames + ? { ...pickerProps.classNames, ...props.classNames } + : pickerProps.classNames, + styles: props.styles ? { ...pickerProps.styles, ...props.styles } : pickerProps.styles, + } as PickerPanelProps; + + return ( + + + + ); + }, +); + +const PanelRenderPanel = InternalPanelRenderPanel as ( + props: PanelRenderPanelProps & React.RefAttributes, +) => React.ReactElement; + +if (process.env.NODE_ENV !== 'production') { + InternalPanelRenderPanel.displayName = 'PanelRenderPanel'; +} + export type PopupShowTimeConfig = Omit< RangeTimeProps, 'defaultValue' | 'defaultOpenValue' | 'disabledTime' @@ -78,6 +136,7 @@ export default function Popup(props: PopupProps(props: PopupProps {/* `any` here since PresetPanel is reused for both Single & Range Picker which means return type is not stable */} @@ -203,10 +267,16 @@ export default function Popup(props: PopupProps ); - if (panelRender) { - mergedNodes = panelRender(mergedNodes); + if (typeof panelRender === 'function') { + mergedNodes = panelRender(mergedNodes, { components: { Panel: PanelRenderPanel } }); } + mergedNodes = ( + + {mergedNodes} + + ); + // ======================== Render ======================== const containerPrefixCls = `${panelPrefixCls}-container`; diff --git a/src/interface.tsx b/src/interface.tsx index 964f80c2b..be7146740 100644 --- a/src/interface.tsx +++ b/src/interface.tsx @@ -1,8 +1,22 @@ import type { AlignType, BuildInPlacements } from '@rc-component/trigger'; +import type * as React from 'react'; import type { GenerateConfig } from './generate'; +import type { PickerPanelProps, PickerPanelRef } from './PickerPanel'; export type NullableDateType = DateType | null | undefined; +export type PanelRenderPanelProps = Partial< + PickerPanelProps +>; + +export interface PanelRenderExtra { + components: { + Panel: React.ComponentType< + PanelRenderPanelProps & React.RefAttributes + >; + }; +} + export type Locale = { locale: string; @@ -460,7 +474,10 @@ export interface SharedPickerProps showNow?: boolean; /** @deprecated Please use `showNow` instead */ showToday?: boolean; - panelRender?: (originPanel: React.ReactNode) => React.ReactNode; + panelRender?: ( + originPanel: React.ReactNode, + extra: PanelRenderExtra, + ) => React.ReactNode; renderExtraFooter?: (mode: PanelMode) => React.ReactNode; } diff --git a/tests/picker.spec.tsx b/tests/picker.spec.tsx index 76bca254c..146de3f75 100644 --- a/tests/picker.spec.tsx +++ b/tests/picker.spec.tsx @@ -952,6 +952,31 @@ describe('Picker.Basic', () => { expect(document.querySelector('.rc-picker')).toMatchSnapshot(); }); + it('panelRender Panel uses popup context by default', () => { + const onChange = jest.fn(); + const { container } = render( + } + />, + ); + + selectCell(11); + + expect(onChange).toHaveBeenCalled(); + expect(isSame(onChange.mock.calls[0][0], '1990-09-11')).toBeTruthy(); + expect(container.querySelector('input').value).toEqual('1990-09-11'); + }); + + it('panelRender Panel allows picker override', () => { + render( + } />, + ); + + expect(document.querySelector('.rc-picker-time-panel')).toBeTruthy(); + }); + it('change panel when `picker` changed', () => { const { rerender } = render(); expect(document.querySelector('.rc-picker-week-panel')).toBeTruthy();