Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,20 @@ should change the heading of the (upcoming) version to include a major version b
-->
# 6.1.2

## @rjsf/antd

- Updated `SelectWidget` to add a static `getPopupContainerCallback` to the `SelectWidget` component, partially fixing [#3609](https://github.com/rjsf-team/react-jsonschema-form/issues/3609)

## @rjsf/mantine

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))
- 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))

## Dev / docs / playground

- Updated `DemoFrame` as follows to fix [#3609](https://github.com/rjsf-team/react-jsonschema-form/issues/3609)
- Override `antd`'s `SelectWidget.getPopupContainerCallback` callback function to return undefined
- Added a `AntdSelectPatcher` component that observes the creation of `antd` select dropdowns and makes sure they open in the correct location
- Update the `antd` theme wrapper to render the `AntdSelectPatcher`, `AntdStyleProvider` and `ConfigProvider` with it's own `getPopupContainer()` function inside of a `FrameContextConsumer`

# 6.1.1

Expand Down
16 changes: 14 additions & 2 deletions packages/antd/src/widgets/SelectWidget/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useMemo, useState } from 'react';
import { Select, SelectProps } from 'antd';
import {
ariaDescribedByIds,
Expand All @@ -11,7 +12,6 @@ import {
} from '@rjsf/utils';
import isString from 'lodash/isString';
import { DefaultOptionType } from 'antd/es/select';
import { useMemo } from 'react';

const SELECT_STYLE = {
width: '100%',
Expand Down Expand Up @@ -42,6 +42,7 @@ export default function SelectWidget<
value,
schema,
}: WidgetProps<T, S, F>) {
const [open, setOpen] = useState(false);
const { formContext } = registry;
const { readonlyAsDisabled = true } = formContext as GenericObjectType;

Expand All @@ -61,7 +62,7 @@ export default function SelectWidget<
return false;
};

const getPopupContainer = (node: any) => node.parentNode;
const getPopupContainer = SelectWidget.getPopupContainerCallback();

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

Expand Down Expand Up @@ -92,6 +93,7 @@ export default function SelectWidget<

return (
<Select
open={open}
autoFocus={autofocus}
disabled={disabled || (readonlyAsDisabled && readonly)}
getPopupContainer={getPopupContainer}
Expand All @@ -104,9 +106,19 @@ export default function SelectWidget<
style={SELECT_STYLE}
value={selectedIndexes}
{...extraProps}
// When the open change is called, set the open state, needed so that the select opens properly in the playground
onOpenChange={(open) => {
setOpen(open);
}}
filterOption={filterOption}
aria-describedby={ariaDescribedByIds(id)}
options={selectOptions}
/>
);
}

/** Give the playground a place to hook into the `getPopupContainer` callback generation function so that it can be
* disabled while in the playground. Since the callback is a simple function, it can be returned by this static
* "generator" function.
*/
SelectWidget.getPopupContainerCallback = () => (node: any) => node.parentElement;
124 changes: 120 additions & 4 deletions packages/playground/src/components/DemoFrame.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
import { useState, useRef, useCallback, cloneElement, ReactElement, ReactNode } from 'react';
import { cloneElement, useCallback, useEffect, useRef, useState, ReactElement, ReactNode } from 'react';
import { CssBaseline } from '@mui/material';
import { CacheProvider } from '@emotion/react';
import createCache, { EmotionCache } from '@emotion/cache';
import Frame, { FrameComponentProps, FrameContextConsumer } from 'react-frame-component';
import { Widgets } from '@rjsf/antd';
import { __createChakraFrameProvider } from '@rjsf/chakra-ui';
import { StyleProvider as AntdStyleProvider } from '@ant-design/cssinjs';
import { __createFluentUIRCFrameProvider } from '@rjsf/fluentui-rc';
import { __createDaisyUIFrameProvider } from '@rjsf/daisyui';
import { MantineProvider } from '@mantine/core';
import { ConfigProvider } from 'antd';
import { PrimeReactProvider } from 'primereact/api';

const DEMO_FRAME_JSS = 'demo-frame-jss';

const { SelectWidget } = Widgets;

// Override the static function on the antd `SelectWidget` so that we can "disable" the getPopupContainer callback
// function because, when it is active, the `SelectPatcher` code below along with the `ConfigProvider` for the antd
// theme conditional branch won't take effect as the antd `Select` `getPopupContainer()` supercedes it, so we make it
// return undefined to disable it.
// @ts-expect-error TS2339 because the Widget interface doesn't have the static function on it
SelectWidget.getPopupContainerCallback = () => undefined;

/*
Adapted from https://github.com/mui-org/material-ui/blob/master/docs/src/modules/components/DemoSandboxed.js

Expand All @@ -36,6 +49,96 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

/** This is a hack to fix the antd `SelectWidget` so that the popup works properly within the iframe of the playground.
* It basically observes when the `antd-select-dropdown` is created and attaches a dropdown positioning callback that is
* tracking the scrolling of the iFrome document and fixing up the dropdown's `inset` style attribute so that it is
* positioned properly.
*
* @param frameDoc - The iFrame document of the playground
*/
function AntdSelectPatcher({ frameDoc }: { frameDoc: Document }) {
useEffect(() => {
if (!frameDoc) {
return;
}

const handleDropdownPositioning = (dropdown: HTMLElement) => {
const style = dropdown.style;

// Check if dropdown needs repositioning
const isHidden = style.inset && style.inset.includes('-1000vh');
if (isHidden) {
const trigger = frameDoc.querySelector('.ant-select-focused, .ant-select-open');

if (trigger) {
const rect = trigger.getBoundingClientRect();
// Get scroll offsets
const scrollTop = frameDoc.documentElement.scrollTop || frameDoc.body.scrollTop;
const scrollLeft = frameDoc.documentElement.scrollLeft || frameDoc.body.scrollLeft;

// Calculate absolute position accounting for scroll
const top = rect.bottom + scrollTop + 4;
const left = rect.left + scrollLeft;

// Position the dropdown BELOW the select
dropdown.style.inset = `${top}px auto auto ${left}px`;
dropdown.style.position = 'absolute';
}
}
};

const createObserver = () => {
return new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
const dropdown = mutation.target as HTMLElement;

if (dropdown.classList.contains('ant-select-dropdown')) {
handleDropdownPositioning(dropdown);
}
}

// Also check for newly added dropdowns
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) {
const element = node as HTMLElement;
if (element.classList.contains('ant-select-dropdown')) {
handleDropdownPositioning(element);
}
}
});
}
});
});
};

// Observe iframe document
const iframeObserver = createObserver();
iframeObserver.observe(frameDoc.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class'],
});

// Also reposition on scroll
const handleScroll = () => {
const dropdowns = frameDoc.querySelectorAll('.ant-select-dropdown:not(.ant-select-dropdown-hidden)');
dropdowns.forEach((dropdown) => {
handleDropdownPositioning(dropdown as HTMLElement);
});
};
frameDoc.addEventListener('scroll', handleScroll, true);
return () => {
iframeObserver.disconnect();
frameDoc.removeEventListener('scroll', handleScroll, true);
};
}, [frameDoc]);

return null;
}

interface DemoFrameProps extends FrameComponentProps {
theme: string;
/** override children to be ReactElement to avoid Typescript issue. In this case we don't need to worry about
Expand All @@ -61,7 +164,7 @@ export default function DemoFrame(props: DemoFrameProps) {
createCache({
key: 'css',
prepend: true,
container: instanceRef.current.contentWindow['demo-frame-jss'],
container: instanceRef.current.contentWindow[DEMO_FRAME_JSS],
}),
);
setContainer(instanceRef.current.contentDocument.body);
Expand All @@ -85,7 +188,20 @@ export default function DemoFrame(props: DemoFrameProps) {
body = <FrameContextConsumer>{__createChakraFrameProvider(props)}</FrameContextConsumer>;
} else if (theme === 'antd') {
body = ready ? (
<AntdStyleProvider container={instanceRef.current.contentWindow['demo-frame-jss']}>{children}</AntdStyleProvider>
<FrameContextConsumer>
{({ document: frameDoc }) => {
const jssContainer =
frameDoc?.getElementById(DEMO_FRAME_JSS) || instanceRef.current.contentWindow[DEMO_FRAME_JSS];
return (
<>
<AntdSelectPatcher frameDoc={frameDoc || instanceRef.current.contentDocument} />
<AntdStyleProvider container={jssContainer}>
<ConfigProvider getPopupContainer={() => jssContainer.parentElement}>{children}</ConfigProvider>
</AntdStyleProvider>
</>
);
}}
</FrameContextConsumer>
) : null;
} else if (theme === 'daisy-ui') {
body = ready ? (
Expand Down Expand Up @@ -118,7 +234,7 @@ export default function DemoFrame(props: DemoFrameProps) {

return (
<Frame ref={instanceRef} contentDidMount={onContentDidMount} head={head} {...frameProps}>
<div id='demo-frame-jss' />
<div id={DEMO_FRAME_JSS} />
{body}
</Frame>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/playground/src/components/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ export default function Playground({ themes, validators }: PlaygroundProps) {
validator={validators[validator]}
onChange={onFormDataChange}
onSubmit={onFormDataSubmit}
onBlur={(id: string, value: string) => console.log(`Touched ${id} with value ${value}`)}
onBlur={(id: string, value: string) => console.log(`Blurred ${id} with value ${value}`)}
onFocus={(id: string, value: string) => console.log(`Focused ${id} with value ${value}`)}
onError={(errorList: RJSFValidationError[]) => console.log('errors', errorList)}
ref={playGroundFormRef}
Expand Down