Skip to content

Commit bf87ee9

Browse files
Fix 3609, by getting the antd theme's SelectWidget working in the playground (#4878)
1 parent 7d70d8d commit bf87ee9

File tree

4 files changed

+147
-8
lines changed

4 files changed

+147
-8
lines changed

CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,20 @@ should change the heading of the (upcoming) version to include a major version b
1717
-->
1818
# 6.1.2
1919

20+
## @rjsf/antd
21+
22+
- Updated `SelectWidget` to add a static `getPopupContainerCallback` to the `SelectWidget` component, partially fixing [#3609](https://github.com/rjsf-team/react-jsonschema-form/issues/3609)
23+
2024
## @rjsf/mantine
2125

22-
Align Mantine’s behavior with other themes when clearing string fields: clearing an input now removes the key from formData instead of setting it to an empty string. ([#4875](https://github.com/rjsf-team/react-jsonschema-form/pull/4875))
26+
- Align Mantine’s behavior with other themes when clearing string fields: clearing an input now removes the key from formData instead of setting it to an empty string. ([#4875](https://github.com/rjsf-team/react-jsonschema-form/pull/4875))
27+
28+
## Dev / docs / playground
29+
30+
- Updated `DemoFrame` as follows to fix [#3609](https://github.com/rjsf-team/react-jsonschema-form/issues/3609)
31+
- Override `antd`'s `SelectWidget.getPopupContainerCallback` callback function to return undefined
32+
- Added a `AntdSelectPatcher` component that observes the creation of `antd` select dropdowns and makes sure they open in the correct location
33+
- Update the `antd` theme wrapper to render the `AntdSelectPatcher`, `AntdStyleProvider` and `ConfigProvider` with it's own `getPopupContainer()` function inside of a `FrameContextConsumer`
2334

2435
# 6.1.1
2536

packages/antd/src/widgets/SelectWidget/index.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useMemo, useState } from 'react';
12
import { Select, SelectProps } from 'antd';
23
import {
34
ariaDescribedByIds,
@@ -11,7 +12,6 @@ import {
1112
} from '@rjsf/utils';
1213
import isString from 'lodash/isString';
1314
import { DefaultOptionType } from 'antd/es/select';
14-
import { useMemo } from 'react';
1515

1616
const SELECT_STYLE = {
1717
width: '100%',
@@ -42,6 +42,7 @@ export default function SelectWidget<
4242
value,
4343
schema,
4444
}: WidgetProps<T, S, F>) {
45+
const [open, setOpen] = useState(false);
4546
const { formContext } = registry;
4647
const { readonlyAsDisabled = true } = formContext as GenericObjectType;
4748

@@ -61,7 +62,7 @@ export default function SelectWidget<
6162
return false;
6263
};
6364

64-
const getPopupContainer = (node: any) => node.parentNode;
65+
const getPopupContainer = SelectWidget.getPopupContainerCallback();
6566

6667
const selectedIndexes = enumOptionsIndexForValue<S>(value, enumOptions, multiple);
6768

@@ -92,6 +93,7 @@ export default function SelectWidget<
9293

9394
return (
9495
<Select
96+
open={open}
9597
autoFocus={autofocus}
9698
disabled={disabled || (readonlyAsDisabled && readonly)}
9799
getPopupContainer={getPopupContainer}
@@ -104,9 +106,19 @@ export default function SelectWidget<
104106
style={SELECT_STYLE}
105107
value={selectedIndexes}
106108
{...extraProps}
109+
// When the open change is called, set the open state, needed so that the select opens properly in the playground
110+
onOpenChange={(open) => {
111+
setOpen(open);
112+
}}
107113
filterOption={filterOption}
108114
aria-describedby={ariaDescribedByIds(id)}
109115
options={selectOptions}
110116
/>
111117
);
112118
}
119+
120+
/** Give the playground a place to hook into the `getPopupContainer` callback generation function so that it can be
121+
* disabled while in the playground. Since the callback is a simple function, it can be returned by this static
122+
* "generator" function.
123+
*/
124+
SelectWidget.getPopupContainerCallback = () => (node: any) => node.parentElement;

packages/playground/src/components/DemoFrame.tsx

Lines changed: 120 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,28 @@
1-
import { useState, useRef, useCallback, cloneElement, ReactElement, ReactNode } from 'react';
1+
import { cloneElement, useCallback, useEffect, useRef, useState, ReactElement, ReactNode } from 'react';
22
import { CssBaseline } from '@mui/material';
33
import { CacheProvider } from '@emotion/react';
44
import createCache, { EmotionCache } from '@emotion/cache';
55
import Frame, { FrameComponentProps, FrameContextConsumer } from 'react-frame-component';
6+
import { Widgets } from '@rjsf/antd';
67
import { __createChakraFrameProvider } from '@rjsf/chakra-ui';
78
import { StyleProvider as AntdStyleProvider } from '@ant-design/cssinjs';
89
import { __createFluentUIRCFrameProvider } from '@rjsf/fluentui-rc';
910
import { __createDaisyUIFrameProvider } from '@rjsf/daisyui';
1011
import { MantineProvider } from '@mantine/core';
12+
import { ConfigProvider } from 'antd';
1113
import { PrimeReactProvider } from 'primereact/api';
1214

15+
const DEMO_FRAME_JSS = 'demo-frame-jss';
16+
17+
const { SelectWidget } = Widgets;
18+
19+
// Override the static function on the antd `SelectWidget` so that we can "disable" the getPopupContainer callback
20+
// function because, when it is active, the `SelectPatcher` code below along with the `ConfigProvider` for the antd
21+
// theme conditional branch won't take effect as the antd `Select` `getPopupContainer()` supercedes it, so we make it
22+
// return undefined to disable it.
23+
// @ts-expect-error TS2339 because the Widget interface doesn't have the static function on it
24+
SelectWidget.getPopupContainerCallback = () => undefined;
25+
1326
/*
1427
Adapted from https://github.com/mui-org/material-ui/blob/master/docs/src/modules/components/DemoSandboxed.js
1528
@@ -36,6 +49,96 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3649
SOFTWARE.
3750
*/
3851

52+
/** This is a hack to fix the antd `SelectWidget` so that the popup works properly within the iframe of the playground.
53+
* It basically observes when the `antd-select-dropdown` is created and attaches a dropdown positioning callback that is
54+
* tracking the scrolling of the iFrome document and fixing up the dropdown's `inset` style attribute so that it is
55+
* positioned properly.
56+
*
57+
* @param frameDoc - The iFrame document of the playground
58+
*/
59+
function AntdSelectPatcher({ frameDoc }: { frameDoc: Document }) {
60+
useEffect(() => {
61+
if (!frameDoc) {
62+
return;
63+
}
64+
65+
const handleDropdownPositioning = (dropdown: HTMLElement) => {
66+
const style = dropdown.style;
67+
68+
// Check if dropdown needs repositioning
69+
const isHidden = style.inset && style.inset.includes('-1000vh');
70+
if (isHidden) {
71+
const trigger = frameDoc.querySelector('.ant-select-focused, .ant-select-open');
72+
73+
if (trigger) {
74+
const rect = trigger.getBoundingClientRect();
75+
// Get scroll offsets
76+
const scrollTop = frameDoc.documentElement.scrollTop || frameDoc.body.scrollTop;
77+
const scrollLeft = frameDoc.documentElement.scrollLeft || frameDoc.body.scrollLeft;
78+
79+
// Calculate absolute position accounting for scroll
80+
const top = rect.bottom + scrollTop + 4;
81+
const left = rect.left + scrollLeft;
82+
83+
// Position the dropdown BELOW the select
84+
dropdown.style.inset = `${top}px auto auto ${left}px`;
85+
dropdown.style.position = 'absolute';
86+
}
87+
}
88+
};
89+
90+
const createObserver = () => {
91+
return new MutationObserver((mutations) => {
92+
mutations.forEach((mutation) => {
93+
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
94+
const dropdown = mutation.target as HTMLElement;
95+
96+
if (dropdown.classList.contains('ant-select-dropdown')) {
97+
handleDropdownPositioning(dropdown);
98+
}
99+
}
100+
101+
// Also check for newly added dropdowns
102+
if (mutation.type === 'childList') {
103+
mutation.addedNodes.forEach((node) => {
104+
if (node.nodeType === 1) {
105+
const element = node as HTMLElement;
106+
if (element.classList.contains('ant-select-dropdown')) {
107+
handleDropdownPositioning(element);
108+
}
109+
}
110+
});
111+
}
112+
});
113+
});
114+
};
115+
116+
// Observe iframe document
117+
const iframeObserver = createObserver();
118+
iframeObserver.observe(frameDoc.body, {
119+
childList: true,
120+
subtree: true,
121+
attributes: true,
122+
attributeFilter: ['style', 'class'],
123+
});
124+
125+
// Also reposition on scroll
126+
const handleScroll = () => {
127+
const dropdowns = frameDoc.querySelectorAll('.ant-select-dropdown:not(.ant-select-dropdown-hidden)');
128+
dropdowns.forEach((dropdown) => {
129+
handleDropdownPositioning(dropdown as HTMLElement);
130+
});
131+
};
132+
frameDoc.addEventListener('scroll', handleScroll, true);
133+
return () => {
134+
iframeObserver.disconnect();
135+
frameDoc.removeEventListener('scroll', handleScroll, true);
136+
};
137+
}, [frameDoc]);
138+
139+
return null;
140+
}
141+
39142
interface DemoFrameProps extends FrameComponentProps {
40143
theme: string;
41144
/** override children to be ReactElement to avoid Typescript issue. In this case we don't need to worry about
@@ -61,7 +164,7 @@ export default function DemoFrame(props: DemoFrameProps) {
61164
createCache({
62165
key: 'css',
63166
prepend: true,
64-
container: instanceRef.current.contentWindow['demo-frame-jss'],
167+
container: instanceRef.current.contentWindow[DEMO_FRAME_JSS],
65168
}),
66169
);
67170
setContainer(instanceRef.current.contentDocument.body);
@@ -85,7 +188,20 @@ export default function DemoFrame(props: DemoFrameProps) {
85188
body = <FrameContextConsumer>{__createChakraFrameProvider(props)}</FrameContextConsumer>;
86189
} else if (theme === 'antd') {
87190
body = ready ? (
88-
<AntdStyleProvider container={instanceRef.current.contentWindow['demo-frame-jss']}>{children}</AntdStyleProvider>
191+
<FrameContextConsumer>
192+
{({ document: frameDoc }) => {
193+
const jssContainer =
194+
frameDoc?.getElementById(DEMO_FRAME_JSS) || instanceRef.current.contentWindow[DEMO_FRAME_JSS];
195+
return (
196+
<>
197+
<AntdSelectPatcher frameDoc={frameDoc || instanceRef.current.contentDocument} />
198+
<AntdStyleProvider container={jssContainer}>
199+
<ConfigProvider getPopupContainer={() => jssContainer.parentElement}>{children}</ConfigProvider>
200+
</AntdStyleProvider>
201+
</>
202+
);
203+
}}
204+
</FrameContextConsumer>
89205
) : null;
90206
} else if (theme === 'daisy-ui') {
91207
body = ready ? (
@@ -118,7 +234,7 @@ export default function DemoFrame(props: DemoFrameProps) {
118234

119235
return (
120236
<Frame ref={instanceRef} contentDidMount={onContentDidMount} head={head} {...frameProps}>
121-
<div id='demo-frame-jss' />
237+
<div id={DEMO_FRAME_JSS} />
122238
{body}
123239
</Frame>
124240
);

packages/playground/src/components/Playground.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ export default function Playground({ themes, validators }: PlaygroundProps) {
234234
validator={validators[validator]}
235235
onChange={onFormDataChange}
236236
onSubmit={onFormDataSubmit}
237-
onBlur={(id: string, value: string) => console.log(`Touched ${id} with value ${value}`)}
237+
onBlur={(id: string, value: string) => console.log(`Blurred ${id} with value ${value}`)}
238238
onFocus={(id: string, value: string) => console.log(`Focused ${id} with value ${value}`)}
239239
onError={(errorList: RJSFValidationError[]) => console.log('errors', errorList)}
240240
ref={playGroundFormRef}

0 commit comments

Comments
 (0)