From 5da685b7ed491eec4e57c4d9bc2a7f252e77a8b6 Mon Sep 17 00:00:00 2001 From: Rahul Barwal Date: Fri, 8 Nov 2024 10:46:09 +0530 Subject: [PATCH 1/3] feat: Implement dynamic dropdown width in SelectField component for better responsiveness and usability --- .../JSONFormWidget/fields/SelectField.tsx | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/app/client/src/widgets/JSONFormWidget/fields/SelectField.tsx b/app/client/src/widgets/JSONFormWidget/fields/SelectField.tsx index 63bb33ae5451..d2ac95ea5c90 100644 --- a/app/client/src/widgets/JSONFormWidget/fields/SelectField.tsx +++ b/app/client/src/widgets/JSONFormWidget/fields/SelectField.tsx @@ -1,4 +1,11 @@ -import React, { useCallback, useContext, useMemo, useRef } from "react"; +import React, { + useCallback, + useContext, + useMemo, + useRef, + useEffect, + useState, +} from "react"; import styled from "styled-components"; import { useController } from "react-hook-form"; @@ -101,6 +108,7 @@ function SelectField({ schemaItem.defaultValue, passedDefaultValue as DefaultValue, ); + const [dropDownWidth, setDropDownWidth] = useState(10); useRegisterFieldValidity({ isValid: isValueValid, @@ -108,6 +116,29 @@ function SelectField({ fieldType: schemaItem.fieldType, }); useUnmountFieldValidation({ fieldName: name }); + useEffect(() => { + const updateWidth = () => { + if (wrapperRef.current) { + setDropDownWidth(wrapperRef.current.offsetWidth); + } + }; + + // Initial width + updateWidth(); + + // Create ResizeObserver instance + const resizeObserver = new ResizeObserver(updateWidth); + + // Start observing the trigger element + if (wrapperRef.current) { + resizeObserver.observe(wrapperRef.current); + } + + // Cleanup + return () => { + resizeObserver.disconnect(); + }; + }, [wrapperRef]); const [updateFilterText] = useUpdateInternalMetaState({ propertyName: `${name}.filterText`, @@ -158,7 +189,6 @@ function SelectField({ [onChange, schemaItem.onOptionChange, executeAction], ); - const dropdownWidth = wrapperRef.current?.clientWidth; const fieldComponent = useMemo( () => ( @@ -168,7 +198,7 @@ function SelectField({ boxShadow={schemaItem.boxShadow} compactMode={false} disabled={schemaItem.isDisabled} - dropDownWidth={dropdownWidth || 100} + dropDownWidth={dropDownWidth || 100} hasError={isDirtyRef.current ? !isValueValid : false} height={10} isFilterable={schemaItem.isFilterable} @@ -203,7 +233,7 @@ function SelectField({ isValueValid, onOptionSelected, selectedIndex, - dropdownWidth, + dropDownWidth, fieldClassName, ], ); From 383cc8c9d5f3bdf09ae351f8e8f188ad8fc22b18 Mon Sep 17 00:00:00 2001 From: Rahul Barwal Date: Fri, 8 Nov 2024 12:54:01 +0530 Subject: [PATCH 2/3] feat: Add ResizeObserver tests to SelectField for proper width handling and cleanup on unmount --- .../fields/SelectField.test.tsx | 207 +++++++++++++++++- 1 file changed, 206 insertions(+), 1 deletion(-) diff --git a/app/client/src/widgets/JSONFormWidget/fields/SelectField.test.tsx b/app/client/src/widgets/JSONFormWidget/fields/SelectField.test.tsx index 7f87b5030de3..d580734d7e4d 100644 --- a/app/client/src/widgets/JSONFormWidget/fields/SelectField.test.tsx +++ b/app/client/src/widgets/JSONFormWidget/fields/SelectField.test.tsx @@ -1,5 +1,11 @@ +import { act, render } from "@testing-library/react"; +import { RenderModes } from "constants/WidgetConstants"; +import React from "react"; +import { FormProvider, useForm } from "react-hook-form"; +import { DataType, FieldType, type Schema } from "../constants"; +import FormContext from "../FormContext"; import type { SelectFieldProps } from "./SelectField"; -import { isValid } from "./SelectField"; +import SelectField, { isValid } from "./SelectField"; describe(".isValid", () => { it("returns true when isRequired is false", () => { @@ -70,3 +76,202 @@ describe(".isValid", () => { }); }); }); + +describe("ResizeObserver", () => { + const MockFormWrapper = ({ children }: { children: React.ReactNode }) => { + const methods = useForm(); + + return ( + + + {children} + + + ); + }; + const defaultProps: SelectFieldProps = { + name: "testSelect", + fieldClassName: "test-select", + propertyPath: "testSelect", + schemaItem: { + fieldType: FieldType.SELECT, + isRequired: false, + isVisible: true, + isDisabled: false, + accessor: "testSelect", + identifier: "testSelect", + originalIdentifier: "testSelect", + position: 0, + label: "Test Select", + options: [ + { label: "Option 1", value: "1" }, + { label: "Option 2", value: "2" }, + ], + children: {} as Schema, // Assuming an empty Schema object or placeholder + dataType: DataType.STRING, + isCustomField: false, + sourceData: null, // Assuming sourceData as null or other default + isFilterable: false, + filterText: "", + serverSideFiltering: false, + }, + }; + let resizeObserver: ResizeObserverMock; + + beforeAll(() => { + ( + global as unknown as { ResizeObserver: typeof ResizeObserverMock } + ).ResizeObserver = ResizeObserverMock; + }); + + afterAll(() => { + delete (global as unknown as { ResizeObserver?: typeof ResizeObserverMock }) + .ResizeObserver; + }); + + beforeEach(() => { + // Capture the ResizeObserver instance + resizeObserver = null!; + ( + global as unknown as { ResizeObserver: typeof ResizeObserverMock } + ).ResizeObserver = class extends ResizeObserverMock { + constructor(callback: ResizeObserverCallback) { + super(callback); + resizeObserver = this as ResizeObserverMock; + } + }; + }); + + it("should cleanup ResizeObserver on unmount", () => { + const { unmount } = render( + + + , + ); + + const disconnectSpy = jest.spyOn(resizeObserver, "disconnect"); + + // Unmount component + unmount(); + + // Verify cleanup + expect(disconnectSpy).toHaveBeenCalled(); + }); + + it("initializes with correct width", () => { + // Mock offsetWidth + const mockOffsetWidth = 200; + + jest + .spyOn(HTMLElement.prototype, "offsetWidth", "get") + .mockImplementation(() => mockOffsetWidth); + + const { getByTestId } = render( + + + , + ); + const content = getByTestId("select-container"); + + expect(content.offsetWidth).toBe(mockOffsetWidth); + }); + + it("updates width when select component is resized", async () => { + const widths = [200, 300, 400, 250]; + + jest + .spyOn(HTMLElement.prototype, "offsetWidth", "get") + .mockImplementation(() => widths[0]); + + const { getByTestId } = render( + + + , + ); + let triggerElement = getByTestId("select-container"); + + widths.forEach((width, index) => { + let newWidth = widths[index + 1]; + + if (index === widths.length - 1) { + newWidth = widths[0]; + } + + // Verify initial width + expect(triggerElement.offsetWidth).toBe(width); + + // Update mock width + jest + .spyOn(HTMLElement.prototype, "offsetWidth", "get") + .mockImplementation(() => newWidth); + + // Trigger resize + act(() => { + resizeObserver.triggerResize(triggerElement, newWidth); + }); + + // Verify updated width + triggerElement = getByTestId("select-container"); + + expect(triggerElement.offsetWidth).toBe(newWidth); + }); + }); +}); + +type ResizeObserverCallback = (entries: ResizeObserverEntry[]) => void; + +class ResizeObserverMock implements ResizeObserver { + private callback: ResizeObserverCallback; + private elements: Set; + + constructor(callback: ResizeObserverCallback) { + this.callback = callback; + this.elements = new Set(); + } + + observe(element: Element): void { + this.elements.add(element); + } + + unobserve(element: Element): void { + this.elements.delete(element); + } + + disconnect(): void { + this.elements.clear(); + } + + // Utility method to trigger resize + triggerResize(element: Element, width: number): void { + if (this.elements.has(element)) { + this.callback([ + { + target: element, + contentRect: { + width, + bottom: 0, + height: 0, + left: 0, + right: 0, + top: 0, + x: 0, + y: 0, + toJSON: jest.fn(), + }, + borderBoxSize: [{ inlineSize: width, blockSize: 0 }], + contentBoxSize: [{ inlineSize: width, blockSize: 0 }], + devicePixelContentBoxSize: [{ inlineSize: width, blockSize: 0 }], + } as ResizeObserverEntry, + ]); + } + } +} From 1438c99fb6760f87879363ed1ad82bc0f3ddea54 Mon Sep 17 00:00:00 2001 From: Rahul Barwal Date: Fri, 8 Nov 2024 13:05:17 +0530 Subject: [PATCH 3/3] feat: Add test for ResizeObserver setup in SelectField to ensure observer is created on mount --- .../JSONFormWidget/fields/SelectField.test.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/client/src/widgets/JSONFormWidget/fields/SelectField.test.tsx b/app/client/src/widgets/JSONFormWidget/fields/SelectField.test.tsx index d580734d7e4d..6d49c89022e3 100644 --- a/app/client/src/widgets/JSONFormWidget/fields/SelectField.test.tsx +++ b/app/client/src/widgets/JSONFormWidget/fields/SelectField.test.tsx @@ -151,6 +151,23 @@ describe("ResizeObserver", () => { }; }); + it("should setup ResizeObserver on mount", () => { + const mockObserver = jest.fn(); + + window.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: mockObserver, + disconnect: jest.fn(), + unobserve: jest.fn(), + })); + render( + + + , + ); + + expect(mockObserver).toHaveBeenCalled(); + }); + it("should cleanup ResizeObserver on unmount", () => { const { unmount } = render(