From 8379661f1380a7722f1643b1252f62df55b5df8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fred=20Lef=C3=A9v=C3=A8re-Laoide?= <90181748+FredLL-Avaiga@users.noreply.github.com> Date: Wed, 9 Oct 2024 16:53:38 +0200 Subject: [PATCH] Component level Styling (#1872) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Component level Styling * function test * unused import * optimization for detecting callable property * Types init * test callable property * renaming get_class_name * use the right function * Update .gitignore Co-authored-by: Fabien Lelaquais <86590727+FabienLelaquais@users.noreply.github.com> * getStyle * include core comps * a few less characters :-) * remove closing tags when not needed * spelling * format * format * fix test * remove only jsx closing tags * html tag lower * fix tests * Fab is the king of RegExp --------- Co-authored-by: Fred Lefévère-Laoide Co-authored-by: Fabien Lelaquais <86590727+FabienLelaquais@users.noreply.github.com> --- .gitignore | 4 +- frontend/taipy-gui/packaging/taipy-gui.d.ts | 9 + .../src/components/Taipy/AutoLoadingTable.tsx | 4 +- .../taipy-gui/src/components/Taipy/Button.tsx | 4 +- .../src/components/Taipy/Chart.spec.tsx | 485 +++++++++--------- .../taipy-gui/src/components/Taipy/Chart.tsx | 79 +-- .../taipy-gui/src/components/Taipy/Chat.tsx | 4 +- .../src/components/Taipy/DateRange.tsx | 4 +- .../src/components/Taipy/DateSelector.tsx | 4 +- .../taipy-gui/src/components/Taipy/Dialog.tsx | 5 +- .../src/components/Taipy/Expandable.tsx | 3 +- .../taipy-gui/src/components/Taipy/Field.tsx | 39 +- .../src/components/Taipy/FileDownload.tsx | 4 +- .../src/components/Taipy/FileSelector.tsx | 4 +- .../taipy-gui/src/components/Taipy/Image.tsx | 60 ++- .../src/components/Taipy/Indicator.tsx | 74 +-- .../taipy-gui/src/components/Taipy/Input.tsx | 6 +- .../taipy-gui/src/components/Taipy/Layout.tsx | 3 +- .../taipy-gui/src/components/Taipy/Login.tsx | 37 +- .../taipy-gui/src/components/Taipy/Menu.tsx | 4 +- .../src/components/Taipy/Metric.spec.tsx | 82 +-- .../taipy-gui/src/components/Taipy/Metric.tsx | 5 +- .../taipy-gui/src/components/Taipy/NavBar.tsx | 22 +- .../src/components/Taipy/PaginatedTable.tsx | 4 +- .../taipy-gui/src/components/Taipy/Pane.tsx | 3 +- .../taipy-gui/src/components/Taipy/Part.tsx | 7 +- .../src/components/Taipy/Progress.tsx | 82 +-- .../src/components/Taipy/Selector.tsx | 358 +++++++------ .../taipy-gui/src/components/Taipy/Slider.tsx | 4 +- .../src/components/Taipy/StatusList.tsx | 6 +- .../src/components/Taipy/TaipyStyle.spec.tsx | 52 ++ .../src/components/Taipy/TaipyStyle.tsx | 64 +++ .../src/components/Taipy/ThemeToggle.tsx | 4 +- .../taipy-gui/src/components/Taipy/Toggle.tsx | 4 +- .../src/components/Taipy/TreeView.tsx | 9 +- .../taipy-gui/src/components/Taipy/index.ts | 63 +-- .../taipy-gui/src/components/Taipy/utils.ts | 4 +- frontend/taipy-gui/src/extensions/exports.ts | 2 + frontend/taipy/package-lock.json | 1 + frontend/taipy/src/DataNodeViewer.tsx | 3 +- frontend/taipy/src/JobSelector.tsx | 3 +- frontend/taipy/src/NodeSelector.tsx | 4 +- frontend/taipy/src/ScenarioDag.tsx | 3 +- frontend/taipy/src/ScenarioSelector.tsx | 3 +- frontend/taipy/src/ScenarioViewer.tsx | 4 +- taipy/gui/_page.py | 38 +- taipy/gui/_renderers/__init__.py | 2 +- taipy/gui/_renderers/_markdown/control.py | 4 +- taipy/gui/_renderers/builder.py | 18 +- taipy/gui/_renderers/factory.py | 2 +- taipy/gui/builder/_api_generator.py | 4 +- taipy/gui/builder/_element.py | 17 +- tests/gui/actions/test_actions.py | 43 ++ tests/gui/builder/control/test_content.py | 2 +- tests/gui/builder/control/test_html.py | 2 + tests/gui/builder/control/test_taipy_style.py | 28 + tests/gui/builder/test_on_action.py | 12 + tests/gui/control/test_chat.py | 2 +- tests/gui/control/test_taipy_style.py | 27 + tests/gui/extension/test_library.py | 4 +- tests/gui/renderers/test_md_parsing.py | 2 +- 61 files changed, 1122 insertions(+), 717 deletions(-) create mode 100644 frontend/taipy-gui/src/components/Taipy/TaipyStyle.spec.tsx create mode 100644 frontend/taipy-gui/src/components/Taipy/TaipyStyle.tsx create mode 100644 tests/gui/actions/test_actions.py create mode 100644 tests/gui/builder/control/test_taipy_style.py create mode 100644 tests/gui/control/test_taipy_style.py diff --git a/.gitignore b/.gitignore index ff4cffc675..b840ca50e6 100644 --- a/.gitignore +++ b/.gitignore @@ -89,8 +89,8 @@ user_data/ Data # demo files -demo-* -demo_* +demo[_\-]* +demo[_|-]*/ .airflow *.dags data_sources diff --git a/frontend/taipy-gui/packaging/taipy-gui.d.ts b/frontend/taipy-gui/packaging/taipy-gui.d.ts index 283187e19f..b8cf647b52 100644 --- a/frontend/taipy-gui/packaging/taipy-gui.d.ts +++ b/frontend/taipy-gui/packaging/taipy-gui.d.ts @@ -446,3 +446,12 @@ export declare const useDispatch: () => React.Dispatch; * @returns The page module. */ export declare const useModule: () => string | undefined; + +/** + * A function that retrieves the dynamic className associated + * to an instance of component through the style property + * + * @param children - The react children of the component + * @returns The associated className. + */ +export declare const getComponentClassName: (children: React.ReactNode) => string; diff --git a/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx b/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx index 4f67c3561a..1de4b9c924 100644 --- a/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx +++ b/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx @@ -79,6 +79,7 @@ import { import TableFilter from "./TableFilter"; import { getSuffixedClassNames, getUpdateVar } from "./utils"; import { emptyArray } from "../../utils"; +import { getComponentClassName } from "./TaipyStyle"; interface RowData { colsOrder: string[]; @@ -592,7 +593,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => { const boxSx = useMemo(() => ({ ...baseBoxSx, width: width }), [width]); return ( - + @@ -715,6 +716,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => { + {props.children} ); }; diff --git a/frontend/taipy-gui/src/components/Taipy/Button.tsx b/frontend/taipy-gui/src/components/Taipy/Button.tsx index 2fe87be101..a0642f36c7 100644 --- a/frontend/taipy-gui/src/components/Taipy/Button.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Button.tsx @@ -20,6 +20,7 @@ import { createSendActionNameAction } from "../../context/taipyReducers"; import { getCssSize, getSuffixedClassNames, TaipyActiveProps } from "./utils"; import { useClassNames, useDispatch, useDynamicProperty, useModule } from "../../utils/hooks"; import { stringIcon, Icon, IconAvatar } from "../../utils/icon"; +import { getComponentClassName } from "./TaipyStyle"; interface ButtonProps extends TaipyActiveProps { onAction?: string; @@ -67,7 +68,7 @@ const Button = (props: ButtonProps) => { { ) : ( )} + {props.children} ); diff --git a/frontend/taipy-gui/src/components/Taipy/Chart.spec.tsx b/frontend/taipy-gui/src/components/Taipy/Chart.spec.tsx index e5bf727804..9f86344998 100644 --- a/frontend/taipy-gui/src/components/Taipy/Chart.spec.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Chart.spec.tsx @@ -119,8 +119,8 @@ const useGetRealIndex = (data: Data, dataKey: DataKey, props: Props) => { ? props.figure ? index : data[dataKey] && data[dataKey].tp_index - ? data[dataKey]!.tp_index![index] - : index + ? data[dataKey]!.tp_index![index] + : index : 0, [data, dataKey, props.figure] ); @@ -128,35 +128,29 @@ const useGetRealIndex = (data: Data, dataKey: DataKey, props: Props) => { describe("Chart Component", () => { it("renders", async () => { - const { getByTestId } = render(); - const elt = getByTestId("test"); - expect(elt.tagName).toBe("DIV"); + const { container } = render(); + const elt = container.querySelector(".test"); + expect(elt?.tagName).toBe("DIV"); }); it("displays the right info for class", async () => { - const { getByTestId } = render( - - ); - const elt = getByTestId("test"); - expect(elt).toHaveClass("taipy-chart"); + const { container } = render(); + const elt = container.querySelector(".test"); + expect(elt).toBeInTheDocument(); }); it("is disabled", async () => { - const { getByTestId } = render( - - ); - const elt = getByTestId("test"); - expect(elt.querySelector(".modebar")).toBeNull(); + const { container } = render(); + const elt = container.querySelector(".test"); + expect(elt?.querySelector(".modebar")).toBeNull(); }); it("is enabled by default", async () => { - const { getByTestId } = render(); - const elt = getByTestId("test"); - await waitFor(() => expect(elt.querySelector(".modebar")).not.toBeNull()); + const { container } = render(); + const elt = container.querySelector(".test"); + await waitFor(() => expect(elt?.querySelector(".modebar")).not.toBeNull()); }); it("is enabled by active", async () => { - const { getByTestId } = render( - - ); - const elt = getByTestId("test"); - await waitFor(() => expect(elt.querySelector(".modebar")).not.toBeNull()); + const { container } = render(); + const elt = container.querySelector(".test"); + await waitFor(() => expect(elt?.querySelector(".modebar")).not.toBeNull()); }); it("dispatch 2 well formed messages at first render", async () => { const dispatch = jest.fn(); @@ -169,14 +163,14 @@ describe("Chart Component", () => { data={undefined} updateVarName="data_var" defaultConfig={chartConfig} - updateVars="varname=varname" + updateVars="varName=varName" {...selProps} /> ); expect(dispatch).toHaveBeenCalledWith({ name: "", - payload: { id: "chart", names: ["varname"], refresh: false }, + payload: { id: "chart", names: ["varName"], refresh: false }, type: "REQUEST_UPDATE", }); expect(dispatch).toHaveBeenCalledWith({ @@ -194,15 +188,15 @@ describe("Chart Component", () => { it("dispatch a well formed message on selection", async () => { const dispatch = jest.fn(); const state: TaipyState = INITIAL_STATE; - const { getByTestId } = render( + const { container } = render( - + ); - const elt = getByTestId("test"); - await waitFor(() => expect(elt.querySelector(".modebar")).not.toBeNull()); - const modebar = elt.querySelector(".modebar"); - modebar && (await userEvent.click(modebar)); + const elt = container.querySelector(".test"); + await waitFor(() => expect(elt?.querySelector(".modebar")).not.toBeNull()); + const modeBar = elt?.querySelector(".modebar"); + modeBar && (await userEvent.click(modeBar)); expect(dispatch).toHaveBeenCalledWith({ name: "data_var", payload: { @@ -224,7 +218,7 @@ describe("Chart Component", () => { updateVarName="data_var" data={state.data.table as undefined} defaultConfig={chartConfig} - updateVars="varname=varname" + updateVars="varName=varName" /> ); @@ -236,7 +230,7 @@ describe("Chart Component", () => { updateVarName="data_var" data={newState.data.table as Record} defaultConfig={chartConfig} - updateVars="varname=varname" + updateVars="varName=varName" /> ); @@ -258,27 +252,28 @@ describe("Chart Component", () => { }); xit("displays the received data", async () => { const { getAllByText, rerender } = render( - + ); - rerender(); - const elts = getAllByText("Austria"); - expect(elts.length).toBeGreaterThan(1); - expect(elts[0].tagName).toBe("TD"); + rerender(); + const elements = getAllByText("Austria"); + expect(elements.length).toBeGreaterThan(1); + expect(elements[0].tagName).toBe("TD"); }); it("Chart renders correctly", () => { const figure = [{ data: [], layout: { title: "Mock Title" } }]; - const { getByTestId } = render( + const { container } = render( ); - expect(getByTestId("chart")).toBeInTheDocument(); + const elt = container.querySelector(".test"); + expect(elt).toBeInTheDocument(); }); it("handles plotConfig prop correctly", () => { const consoleInfoSpy = jest.spyOn(console, "info"); @@ -297,240 +292,238 @@ describe("Chart Component", () => { }); it("handles fullscreen toggle correctly", async () => { // Render the Chart component - render(); + const { container } = render(); await waitFor(() => { - const fullscreenButton = document.querySelector(".modebar-btn[data-title='Full screen']"); + const fullscreenButton = container.querySelector(".modebar-btn[data-title='Full screen']"); fireEvent.click(fullscreenButton as Element); - const exitFullscreenButton = document.querySelector(".modebar-btn[data-title='Exit Full screen']"); + const exitFullscreenButton = container.querySelector(".modebar-btn[data-title='Exit Full screen']"); fireEvent.click(exitFullscreenButton as Element); - const divElement = document.querySelector(".js-plotly-plot"); + const divElement = container.querySelector(".js-plotly-plot"); expect(divElement).toHaveStyle("width: 100%"); }); }); -}); -describe("Chart Component as Map", () => { - it("renders", async () => { - const { getByTestId } = render( - - ); - const elt = getByTestId("test"); - await waitFor(() => expect(elt.querySelector(".modebar")).not.toBeNull()); + describe("as Map", () => { + it("renders", async () => { + const { container } = render(); + const elt = container.querySelector(".test"); + await waitFor(() => expect(elt?.querySelector(".modebar")).not.toBeNull()); + }); }); -}); -describe("getColNameFromIndexed function", () => { - it("returns the column name when the input string matches the pattern", () => { - const colName = "1/myColumn"; - const result = getColNameFromIndexed(colName); - expect(result).toBe("myColumn"); - }); - it("returns the input string when the input string does not match the pattern", () => { - const colName = "myColumn"; - const result = getColNameFromIndexed(colName); - expect(result).toBe("myColumn"); - }); - it("returns the input string when the input string is empty", () => { - const colName = ""; - const result = getColNameFromIndexed(colName); - expect(result).toBe(""); + describe("getColNameFromIndexed function", () => { + it("returns the column name when the input string matches the pattern", () => { + const colName = "1/myColumn"; + const result = getColNameFromIndexed(colName); + expect(result).toBe("myColumn"); + }); + it("returns the input string when the input string does not match the pattern", () => { + const colName = "myColumn"; + const result = getColNameFromIndexed(colName); + expect(result).toBe("myColumn"); + }); + it("returns the input string when the input string is empty", () => { + const colName = ""; + const result = getColNameFromIndexed(colName); + expect(result).toBe(""); + }); }); -}); -describe("getValue function", () => { - it("returns the value from column when the input string matches the pattern", () => { - const values: TraceValueType = { myColumn: [1, 2, 3] }; - const arr: string[] = ["myColumn"]; - const idx = 0; - const result = getValue(values, arr, idx); - expect(result).toEqual([1, 2, 3]); - }); + describe("getValue function", () => { + it("returns the value from column when the input string matches the pattern", () => { + const values: TraceValueType = { myColumn: [1, 2, 3] }; + const arr: string[] = ["myColumn"]; + const idx = 0; + const result = getValue(values, arr, idx); + expect(result).toEqual([1, 2, 3]); + }); - it("returns undefined when returnUndefined is true and value is empty", () => { - const values: TraceValueType = { myColumn: [] }; - const arr: string[] = ["myColumn"]; - const idx = 0; - const returnUndefined = true; - const result = getValue(values, arr, idx, returnUndefined); - expect(result).toBeUndefined(); - }); + it("returns undefined when returnUndefined is true and value is empty", () => { + const values: TraceValueType = { myColumn: [] }; + const arr: string[] = ["myColumn"]; + const idx = 0; + const returnUndefined = true; + const result = getValue(values, arr, idx, returnUndefined); + expect(result).toBeUndefined(); + }); - it("returns empty array when returnUndefined is false and value is empty", () => { - const values: TraceValueType = { myColumn: [] }; - const arr: string[] = ["myColumn"]; - const idx = 0; - const returnUndefined = false; - const result = getValue(values, arr, idx, returnUndefined); - expect(result).toEqual([]); + it("returns empty array when returnUndefined is false and value is empty", () => { + const values: TraceValueType = { myColumn: [] }; + const arr: string[] = ["myColumn"]; + const idx = 0; + const returnUndefined = false; + const result = getValue(values, arr, idx, returnUndefined); + expect(result).toEqual([]); + }); }); -}); -describe("getRealIndex function", () => { - it("should return 0 if index is not a number", () => { - const data = {}; - const dataKey = "someKey"; - const props = { figure: false }; + describe("getRealIndex function", () => { + it("should return 0 if index is not a number", () => { + const data = {}; + const dataKey = "someKey"; + const props = { figure: false }; - const { result } = renderHook(() => useGetRealIndex(data, dataKey, props)); - const getRealIndex = result.current; - expect(getRealIndex(undefined)).toBe(0); - }); + const { result } = renderHook(() => useGetRealIndex(data, dataKey, props)); + const getRealIndex = result.current; + expect(getRealIndex(undefined)).toBe(0); + }); - it("should return index if figure is true", () => { - const data = {}; - const dataKey = "someKey"; - const props = { figure: true }; + it("should return index if figure is true", () => { + const data = {}; + const dataKey = "someKey"; + const props = { figure: true }; - const { result } = renderHook(() => useGetRealIndex(data, dataKey, props)); - const getRealIndex = result.current; - expect(getRealIndex(5)).toBe(5); // index is a number - }); + const { result } = renderHook(() => useGetRealIndex(data, dataKey, props)); + const getRealIndex = result.current; + expect(getRealIndex(5)).toBe(5); // index is a number + }); - it("should return index if figure is false and tp_index does not exist", () => { - const data = { - someKey: {}, - }; - const dataKey = "someKey"; - const props = { figure: false }; + it("should return index if figure is false and tp_index does not exist", () => { + const data = { + someKey: {}, + }; + const dataKey = "someKey"; + const props = { figure: false }; - const { result } = renderHook(() => useGetRealIndex(data, dataKey, props)); - const getRealIndex = result.current; - expect(getRealIndex(2)).toBe(2); // index is a number + const { result } = renderHook(() => useGetRealIndex(data, dataKey, props)); + const getRealIndex = result.current; + expect(getRealIndex(2)).toBe(2); // index is a number + }); }); -}); -describe("getValueFromCol function", () => { - it("should return an empty array when values is undefined", () => { - const result = getValueFromCol(undefined, "test"); - expect(result).toEqual([]); - }); + describe("getValueFromCol function", () => { + it("should return an empty array when values is undefined", () => { + const result = getValueFromCol(undefined, "test"); + expect(result).toEqual([]); + }); - it("should return an empty array when values[col] is undefined", () => { - const values = { test: [1, 2, 3] }; - const result = getValueFromCol(values, "nonexistent"); - expect(result).toEqual([]); + it("should return an empty array when values[col] is undefined", () => { + const values = { test: [1, 2, 3] }; + const result = getValueFromCol(values, "nonexistent"); + expect(result).toEqual([]); + }); }); -}); -describe("getAxis function", () => { - it("should return undefined when traces length is less than idx", () => { - const traces = [["test"]]; - const columns: Record = { - test: { - dfid: "test", - type: "testType", - index: 0, - }, - }; - const result = getAxis(traces, 2, columns, 0); - expect(result).toBeUndefined(); - }); + describe("getAxis function", () => { + it("should return undefined when traces length is less than idx", () => { + const traces = [["test"]]; + const columns: Record = { + test: { + dfid: "test", + type: "testType", + index: 0, + }, + }; + const result = getAxis(traces, 2, columns, 0); + expect(result).toBeUndefined(); + }); - it("should return undefined when traces[idx] length is less than axis", () => { - const traces = [["test"]]; - const columns: Record = { - test: { - dfid: "test", - type: "testType", - index: 0, - }, - }; - const result = getAxis(traces, 0, columns, 2); - expect(result).toBeUndefined(); - }); + it("should return undefined when traces[idx] length is less than axis", () => { + const traces = [["test"]]; + const columns: Record = { + test: { + dfid: "test", + type: "testType", + index: 0, + }, + }; + const result = getAxis(traces, 0, columns, 2); + expect(result).toBeUndefined(); + }); - it("should return undefined when traces[idx][axis] is undefined", () => { - const traces = [["test"]]; - const columns: Record = { - test: { - dfid: "test", - type: "testType", - index: 0, - }, - }; - const result = getAxis(traces, 0, columns, 1); - expect(result).toBeUndefined(); - }); + it("should return undefined when traces[idx][axis] is undefined", () => { + const traces = [["test"]]; + const columns: Record = { + test: { + dfid: "test", + type: "testType", + index: 0, + }, + }; + const result = getAxis(traces, 0, columns, 1); + expect(result).toBeUndefined(); + }); - it("should return undefined when columns[traces[idx][axis]] is undefined", () => { - const traces = [["test"]]; - const columns: Record = { - test: { - dfid: "test", - type: "testType", - index: 0, - }, - }; - const result = getAxis(traces, 0, columns, 1); // changed axis from 0 to 1 - expect(result).toBeUndefined(); - }); + it("should return undefined when columns[traces[idx][axis]] is undefined", () => { + const traces = [["test"]]; + const columns: Record = { + test: { + dfid: "test", + type: "testType", + index: 0, + }, + }; + const result = getAxis(traces, 0, columns, 1); // changed axis from 0 to 1 + expect(result).toBeUndefined(); + }); - it("should return dfid when all conditions are met", () => { - const traces = [["test"]]; - const columns: Record = { - test: { - dfid: "test", - type: "testType", - index: 0, - }, - }; - const result = getAxis(traces, 0, columns, 0); - expect(result).toBe("test"); + it("should return dfid when all conditions are met", () => { + const traces = [["test"]]; + const columns: Record = { + test: { + dfid: "test", + type: "testType", + index: 0, + }, + }; + const result = getAxis(traces, 0, columns, 0); + expect(result).toBe("test"); + }); }); -}); -describe("click function", () => { - it("should return when div is not found", () => { - // Create a mock HTMLElement without 'div.svg-container' - const mockElement = document.createElement("div"); - // Create a mock Event - const mockEvent = new Event("click"); - // Call the click function with the mock HTMLElement and Event - (TaipyPlotlyButtons[0] as ModeBarButtonAny & Clickable).click(mockElement, mockEvent); - // Since there's no 'div.svg-container', the function should return without making any changes - // We can check this by verifying that no 'full-screen' class was added - expect(mockElement.classList.contains("full-screen")).toBe(false); - }); - it("should set data-height when data-height is not set", () => { - // Create a mock HTMLElement - const mockElement = document.createElement("div"); - - // Create a mock div with class 'svg-container' and append it to the mockElement - const mockDiv = document.createElement("div"); - mockDiv.className = "svg-container"; - mockElement.appendChild(mockDiv); - - // Create a mock Event - const mockEvent = { - ...new Event("click"), - currentTarget: document.createElement("div"), - }; - - // Call the click function with the mock HTMLElement and Event - (TaipyPlotlyButtons[0] as ModeBarButtonAny & Clickable).click(mockElement, mockEvent); - - // Check that the 'data-height' attribute was set - expect(mockElement.getAttribute("data-height")).not.toBeNull(); - }); - it("should set data-title attribute", () => { - // Create a mock HTMLElement - const mockElement = document.createElement("div"); - - // Create a mock div with class 'svg-container' and append it to the mockElement - const mockDiv = document.createElement("div"); - mockDiv.className = "svg-container"; - mockElement.appendChild(mockDiv); - - // Create a mock Event with a mock currentTarget - const mockEvent = { - ...new Event("click"), - currentTarget: mockElement, - }; - - // Call the click function with the mock HTMLElement and Event - (TaipyPlotlyButtons[0] as ModeBarButtonAny & Clickable).click(mockElement, mockEvent); - - // Check that the 'data-title' attribute was set - expect(mockElement.getAttribute("data-title")).toBe("Exit Full screen"); + describe("click function", () => { + it("should return when div is not found", () => { + // Create a mock HTMLElement without 'div.svg-container' + const mockElement = document.createElement("div"); + // Create a mock Event + const mockEvent = new Event("click"); + // Call the click function with the mock HTMLElement and Event + (TaipyPlotlyButtons[0] as ModeBarButtonAny & Clickable).click(mockElement, mockEvent); + // Since there's no 'div.svg-container', the function should return without making any changes + // We can check this by verifying that no 'full-screen' class was added + expect(mockElement.classList.contains("full-screen")).toBe(false); + }); + it("should set data-height when data-height is not set", () => { + // Create a mock HTMLElement + const mockElement = document.createElement("div"); + + // Create a mock div with class 'svg-container' and append it to the mockElement + const mockDiv = document.createElement("div"); + mockDiv.className = "svg-container"; + mockElement.appendChild(mockDiv); + + // Create a mock Event + const mockEvent = { + ...new Event("click"), + currentTarget: document.createElement("div"), + }; + + // Call the click function with the mock HTMLElement and Event + (TaipyPlotlyButtons[0] as ModeBarButtonAny & Clickable).click(mockElement, mockEvent); + + // Check that the 'data-height' attribute was set + expect(mockElement.getAttribute("data-height")).not.toBeNull(); + }); + it("should set data-title attribute", () => { + // Create a mock HTMLElement + const mockElement = document.createElement("div"); + + // Create a mock div with class 'svg-container' and append it to the mockElement + const mockDiv = document.createElement("div"); + mockDiv.className = "svg-container"; + mockElement.appendChild(mockDiv); + + // Create a mock Event with a mock currentTarget + const mockEvent = { + ...new Event("click"), + currentTarget: mockElement, + }; + + // Call the click function with the mock HTMLElement and Event + (TaipyPlotlyButtons[0] as ModeBarButtonAny & Clickable).click(mockElement, mockEvent); + + // Check that the 'data-title' attribute was set + expect(mockElement.getAttribute("data-title")).toBe("Exit Full screen"); + }); }); }); diff --git a/frontend/taipy-gui/src/components/Taipy/Chart.tsx b/frontend/taipy-gui/src/components/Taipy/Chart.tsx index 241dcb7d60..affdeb4129 100644 --- a/frontend/taipy-gui/src/components/Taipy/Chart.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Chart.tsx @@ -46,6 +46,7 @@ import { import { darkThemeTemplate } from "../../themes/darkThemeTemplate"; import { Figure } from "react-plotly.js"; import { lightenPayload } from "../../context/wsUtils"; +import { getComponentClassName } from "./TaipyStyle"; const Plot = lazy(() => import("react-plotly.js")); @@ -60,7 +61,6 @@ interface ChartProp extends TaipyActiveProps, TaipyChangeProps { layout?: string; plotConfig?: string; onRangeChange?: string; - testId?: string; render?: boolean; defaultRender?: boolean; template?: string; @@ -112,7 +112,7 @@ export const getValue = ( values: TraceValueType | undefined, arr: T[], idx: number, - returnUndefined = false, + returnUndefined = false ): (string | number)[] | undefined => { const value = getValueFromCol(values, getArrayValue(arr, idx) as unknown as string); if (!returnUndefined || value.length) { @@ -150,7 +150,7 @@ const getDecimatorsPayload = ( modes: string[], columns: Record, traces: string[][], - relayoutData?: PlotRelayoutEvent, + relayoutData?: PlotRelayoutEvent ) => { return decimators ? { @@ -170,7 +170,7 @@ const getDecimatorsPayload = ( yAxis: getAxis(traces, i, columns, 1), zAxis: getAxis(traces, i, columns, 2), chartMode: modes[i], - }, + } ), relayoutData: relayoutData, } @@ -200,15 +200,15 @@ interface PlotlyDiv extends HTMLDivElement { }; } -interface WithpointNumbers { +interface WithPointNumbers { pointNumbers: number[]; } export const getPlotIndex = (pt: PlotDatum) => pt.pointIndex === undefined ? pt.pointNumber === undefined - ? (pt as unknown as WithpointNumbers).pointNumbers?.length - ? (pt as unknown as WithpointNumbers).pointNumbers[0] + ? (pt as unknown as WithPointNumbers).pointNumbers?.length + ? (pt as unknown as WithPointNumbers).pointNumbers[0] : 0 : pt.pointNumber : pt.pointIndex; @@ -362,9 +362,9 @@ const Chart = (props: ChartProp) => { plotRef.current, config.modes, config.columns, - config.traces, - ), - ), + config.traces + ) + ) ); } } @@ -436,7 +436,7 @@ const Chart = (props: ChartProp) => { height === undefined ? ({ ...defaultStyle, width: width } as CSSProperties) : ({ ...defaultStyle, width: width, height: height } as CSSProperties), - [width, height], + [width, height] ); const skelStyle = useMemo(() => ({ ...style, minHeight: "7em" }), [style]); @@ -532,26 +532,26 @@ const Chart = (props: ChartProp) => { }, [props.figure, selected, data, config, dataKey]); const plotConfig = useMemo(() => { - let plconf: Partial = {}; + let plConf: Partial = {}; if (props.plotConfig) { try { - plconf = JSON.parse(props.plotConfig); + plConf = JSON.parse(props.plotConfig); } catch (e) { console.info(`Error while parsing Chart.plot_config\n${(e as Error).message || e}`); } - if (typeof plconf !== "object" || plconf === null || Array.isArray(plconf)) { + if (typeof plConf !== "object" || plConf === null || Array.isArray(plConf)) { console.info("Error Chart.plot_config is not a dictionary"); - plconf = {}; + plConf = {}; } } - plconf.displaylogo = !!plconf.displaylogo; - plconf.modeBarButtonsToAdd = TaipyPlotlyButtons; - // plconf.responsive = true; // this is the source of the on/off height ... - plconf.autosizable = true; + plConf.displaylogo = !!plConf.displaylogo; + plConf.modeBarButtonsToAdd = TaipyPlotlyButtons; + // plConf.responsive = true; // this is the source of the on/off height ... + plConf.autosizable = true; if (!active) { - plconf.staticPlot = true; + plConf.staticPlot = true; } - return plconf; + return plConf; }, [active, props.plotConfig]); const onRelayout = useCallback( @@ -581,9 +581,9 @@ const Chart = (props: ChartProp) => { config.modes, config.columns, config.traces, - eventData, - ), - ), + eventData + ) + ) ); } }, @@ -598,7 +598,7 @@ const Chart = (props: ChartProp) => { config.decimators, updateVarName, module, - ], + ] ); const clickHandler = useCallback( @@ -627,18 +627,18 @@ const Chart = (props: ChartProp) => { y: map ? undefined : transform(yaxis, "top")(evt?.clientY), lon: map ? xaxis.p2c() : undefined, x: map ? undefined : transform(xaxis, "left")(evt?.clientX), - }), - ), + }) + ) ); }, - [dispatch, module, id, onClick], + [dispatch, module, id, onClick] ); const onInitialized = useCallback( (figure: Readonly
, graphDiv: Readonly) => { onClick && graphDiv.addEventListener("click", clickHandler); }, - [onClick, clickHandler], + [onClick, clickHandler] ); const getRealIndex = useCallback( @@ -647,10 +647,10 @@ const Chart = (props: ChartProp) => { ? props.figure ? index : data[dataKey].tp_index - ? (data[dataKey].tp_index[index] as number) - : index + ? (data[dataKey].tp_index[index] as number) + : index : 0, - [data, dataKey, props.figure], + [data, dataKey, props.figure] ); const onSelect = useCallback( @@ -677,8 +677,8 @@ const Chart = (props: ChartProp) => { traces, module, props.onChange, - propagate, - ), + propagate + ) ); return; } @@ -688,19 +688,19 @@ const Chart = (props: ChartProp) => { } }); } else if (config.traces.length === 1) { - const upvar = getUpdateVar(updateVars, "selected0"); - if (upvar) { - dispatch(createSendUpdateAction(upvar, [], module, props.onChange, propagate)); + const upVar = getUpdateVar(updateVars, "selected0"); + if (upVar) { + dispatch(createSendUpdateAction(upVar, [], module, props.onChange, propagate)); } } } }, - [getRealIndex, dispatch, updateVars, propagate, props.onChange, config.traces.length, module], + [getRealIndex, dispatch, updateVars, propagate, props.onChange, config.traces.length, module] ); return render ? ( - + }> {Array.isArray(props.figure) && props.figure.length && props.figure[0].data !== undefined ? ( { /> )} + {props.children} ) : null; diff --git a/frontend/taipy-gui/src/components/Taipy/Chat.tsx b/frontend/taipy-gui/src/components/Taipy/Chat.tsx index 3a8467a395..9497bf4964 100644 --- a/frontend/taipy-gui/src/components/Taipy/Chat.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Chat.tsx @@ -46,6 +46,7 @@ import { IconAvatar, avatarSx } from "../../utils/icon"; import { emptyArray, getInitials } from "../../utils"; import { RowType, TableValueType } from "./tableUtils"; import { Stack } from "@mui/material"; +import { getComponentClassName } from "./TaipyStyle"; const Markdown = lazy(() => import("react-markdown")); @@ -402,7 +403,7 @@ const Chat = (props: ChatProps) => { return ( - + {rows.length && !rows[0] ? ( @@ -469,6 +470,7 @@ const Chat = (props: ChatProps) => { sx={inputSx} /> ) : null} + {props.children} ); diff --git a/frontend/taipy-gui/src/components/Taipy/DateRange.tsx b/frontend/taipy-gui/src/components/Taipy/DateRange.tsx index 73da917353..d5d38d2820 100644 --- a/frontend/taipy-gui/src/components/Taipy/DateRange.tsx +++ b/frontend/taipy-gui/src/components/Taipy/DateRange.tsx @@ -27,6 +27,7 @@ import { dateToString, getDateTime, getTimeZonedDate } from "../../utils"; import { useClassNames, useDispatch, useDynamicProperty, useFormatConfig, useModule } from "../../utils/hooks"; import Field from "./Field"; import ErrorFallback from "../../utils/ErrorBoundary"; +import { getComponentClassName } from "./TaipyStyle"; interface DateRangeProps extends TaipyActiveProps, TaipyChangeProps { withTime?: boolean; @@ -132,7 +133,7 @@ const DateRange = (props: DateRangeProps) => { { /> )} + {props.children} diff --git a/frontend/taipy-gui/src/components/Taipy/DateSelector.tsx b/frontend/taipy-gui/src/components/Taipy/DateSelector.tsx index 6d027a1857..d1eb1430a0 100644 --- a/frontend/taipy-gui/src/components/Taipy/DateSelector.tsx +++ b/frontend/taipy-gui/src/components/Taipy/DateSelector.tsx @@ -26,6 +26,7 @@ import { dateToString, getDateTime, getTimeZonedDate } from "../../utils"; import { useClassNames, useDispatch, useDynamicProperty, useFormatConfig, useModule } from "../../utils/hooks"; import Field from "./Field"; import ErrorFallback from "../../utils/ErrorBoundary"; +import { getComponentClassName } from "./TaipyStyle"; interface DateSelectorProps extends TaipyActiveProps, TaipyChangeProps { withTime?: boolean; @@ -105,7 +106,7 @@ const DateSelector = (props: DateSelectorProps) => { return ( - + {editable ? ( withTime ? ( { width={props.width} /> )} + {props.children} diff --git a/frontend/taipy-gui/src/components/Taipy/Dialog.tsx b/frontend/taipy-gui/src/components/Taipy/Dialog.tsx index 25dbbad263..00ea80d9cb 100644 --- a/frontend/taipy-gui/src/components/Taipy/Dialog.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Dialog.tsx @@ -26,6 +26,7 @@ import { createSendActionNameAction } from "../../context/taipyReducers"; import TaipyRendered from "../pages/TaipyRendered"; import { TaipyActiveProps } from "./utils"; import { useClassNames, useDispatch, useDynamicProperty, useModule } from "../../utils/hooks"; +import { getComponentClassName } from "./TaipyStyle"; interface DialogProps extends TaipyActiveProps { title: string; @@ -75,7 +76,7 @@ const Dialog = (props: DialogProps) => { (evt: MouseEvent) => { const { idx = "-1" } = evt.currentTarget.dataset; if (localAction) { - localAction(parseInt(idx, 10)); + localAction(parseInt(idx, 10)); } else { dispatch(createSendActionNameAction(id, module, onAction, parseInt(idx, 10))); } @@ -114,7 +115,7 @@ const Dialog = (props: DialogProps) => { id={id} onClose={handleAction} open={open === undefined ? defaultOpen === "true" || defaultOpen === true : !!open} - className={className} + className={`${className} ${getComponentClassName(props.children)}`} PaperProps={paperProps} > diff --git a/frontend/taipy-gui/src/components/Taipy/Expandable.tsx b/frontend/taipy-gui/src/components/Taipy/Expandable.tsx index e823a45616..5fc81a9439 100644 --- a/frontend/taipy-gui/src/components/Taipy/Expandable.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Expandable.tsx @@ -22,6 +22,7 @@ import { useClassNames, useDispatch, useDynamicProperty, useModule } from "../.. import { TaipyActiveProps, TaipyChangeProps, getUpdateVar } from "./utils"; import TaipyRendered from "../pages/TaipyRendered"; import { createSendUpdateAction } from "../../context/taipyReducers"; +import { getComponentClassName } from "./TaipyStyle"; interface ExpandableProps extends TaipyActiveProps, TaipyChangeProps { expanded?: boolean; @@ -62,7 +63,7 @@ const Expandable = (props: ExpandableProps) => { return ( - + {title || defaultTitle ? ( }>{title || defaultTitle} ) : null} diff --git a/frontend/taipy-gui/src/components/Taipy/Field.tsx b/frontend/taipy-gui/src/components/Taipy/Field.tsx index 4806b7eb4d..a485548557 100644 --- a/frontend/taipy-gui/src/components/Taipy/Field.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Field.tsx @@ -18,6 +18,7 @@ import Tooltip from "@mui/material/Tooltip"; import { formatWSValue } from "../../utils"; import { useClassNames, useDynamicProperty, useFormatConfig } from "../../utils/hooks"; import { TaipyBaseProps, TaipyHoverProps, getCssSize } from "./utils"; +import { getComponentClassName } from "./TaipyStyle"; interface TaipyFieldProps extends TaipyBaseProps, TaipyHoverProps { dataType?: string; @@ -65,21 +66,29 @@ const Field = (props: TaipyFieldProps) => { return ( - {mode == "pre" ? ( -
-                    {value}
-                
- ) : mode == "markdown" || mode == "md" ? ( - {value} - ) : raw || mode == "raw" ? ( - - {value} - - ) : ( - - {value} - - )} + <> + {mode == "pre" ? ( +
+                        {value}
+                    
+ ) : mode == "markdown" || mode == "md" ? ( + {value} + ) : raw || mode == "raw" ? ( + + {value} + + ) : ( + + {value} + + )} + {props.children} +
); }; diff --git a/frontend/taipy-gui/src/components/Taipy/FileDownload.tsx b/frontend/taipy-gui/src/components/Taipy/FileDownload.tsx index 9c3db564f5..825eb57230 100644 --- a/frontend/taipy-gui/src/components/Taipy/FileDownload.tsx +++ b/frontend/taipy-gui/src/components/Taipy/FileDownload.tsx @@ -21,6 +21,7 @@ import { useClassNames, useDispatch, useDynamicProperty, useModule } from "../.. import { getCssSize, noDisplayStyle, TaipyActiveProps } from "./utils"; import { createSendActionNameAction } from "../../context/taipyReducers"; import { runXHR } from "../../utils/downloads"; +import { getComponentClassName } from "./TaipyStyle"; interface FileDownloadProps extends TaipyActiveProps { content?: string; @@ -96,7 +97,7 @@ const FileDownload = (props: FileDownloadProps) => { const aProps = useMemo(() => (bypassPreview ? {} : { target: "_blank", rel: "noreferrer" }), [bypassPreview]); return render ? ( -
{upload ? : null} + {props.children} ); }; diff --git a/frontend/taipy-gui/src/components/Taipy/Image.tsx b/frontend/taipy-gui/src/components/Taipy/Image.tsx index a72149b136..c16fc4a38b 100644 --- a/frontend/taipy-gui/src/components/Taipy/Image.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Image.tsx @@ -19,6 +19,7 @@ import Tooltip from "@mui/material/Tooltip"; import { createSendActionNameAction } from "../../context/taipyReducers"; import { useClassNames, useDispatch, useDynamicProperty, useModule } from "../../utils/hooks"; import { TaipyActiveProps } from "./utils"; +import { getComponentClassName } from "./TaipyStyle"; interface ImageProps extends TaipyActiveProps { onAction?: string; @@ -65,7 +66,7 @@ const Image = (props: ImageProps) => { display: inlineSvg ? "inline-flex" : undefined, verticalAlign: inlineSvg ? "middle" : undefined, }), - [width, height, inlineSvg], + [width, height, inlineSvg] ); useEffect(() => { @@ -78,29 +79,44 @@ const Image = (props: ImageProps) => { return ( - {onAction ? ( - - + + ) : inlineSvg ? ( +
- {inlineSvg ? ( -
- ) : ( - {label} - )} - - - ) : inlineSvg ? ( -
- ) : ( - {label} - )} + >
+ ) : ( + {label} + )} + {props.children} + ); }; diff --git a/frontend/taipy-gui/src/components/Taipy/Indicator.tsx b/frontend/taipy-gui/src/components/Taipy/Indicator.tsx index b9fc098370..3faf98001f 100644 --- a/frontend/taipy-gui/src/components/Taipy/Indicator.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Indicator.tsx @@ -17,6 +17,7 @@ import { sprintf } from "sprintf-js"; import { TaipyBaseProps } from "./utils"; import { useClassNames } from "../../utils/hooks"; +import { getComponentClassName } from "./TaipyStyle"; interface IndicatorProps extends TaipyBaseProps { min?: number; @@ -55,47 +56,48 @@ const Indicator = (props: IndicatorProps) => { const sliderSx = useMemo( () => ({ - "&": { - width: horizontalOrientation ? width : undefined, - height: horizontalOrientation ? undefined : height, - }, - "&.Mui-disabled": { - color: "transparent", - }, - "& .MuiSlider-markLabel": { - transform: "initial", - }, - "& span:nth-of-type(6)": { - transform: horizontalOrientation ? "translateX(-100%)" : "translateY(100%)", - }, - "& .MuiSlider-rail": { - background: `linear-gradient(${ - horizontalOrientation ? 90 : 0 - }deg, rgba(255,0,0,1) 0%, rgba(0,255,0,1) 100%)`, - opacity: "unset", - }, - "& .MuiSlider-track": { - border: "none", - backgroundColor: "transparent", - }, - "& .MuiSlider-valueLabel": { - top: "unset", - }, - "& .MuiSlider-valueLabel.MuiSlider-valueLabelOpen": { - transform: horizontalOrientation ? "" : "translate(calc(50% + 10px))", - }, - "& .MuiSlider-valueLabel:before": { - left: horizontalOrientation ? "50%" : "0", - bottom: horizontalOrientation ? "0" : "50%", - }, - }), + "&": { + width: horizontalOrientation ? width : undefined, + height: horizontalOrientation ? undefined : height, + }, + "&.Mui-disabled": { + color: "transparent", + }, + "& .MuiSlider-markLabel": { + transform: "initial", + }, + "& span:nth-of-type(6)": { + transform: horizontalOrientation ? "translateX(-100%)" : "translateY(100%)", + }, + "& .MuiSlider-rail": { + background: `linear-gradient(${ + horizontalOrientation ? 90 : 0 + }deg, rgba(255,0,0,1) 0%, rgba(0,255,0,1) 100%)`, + opacity: "unset", + }, + "& .MuiSlider-track": { + border: "none", + backgroundColor: "transparent", + }, + "& .MuiSlider-valueLabel": { + top: "unset", + }, + "& .MuiSlider-valueLabel.MuiSlider-valueLabelOpen": { + transform: horizontalOrientation ? "" : "translate(calc(50% + 10px))", + }, + "& .MuiSlider-valueLabel:before": { + left: horizontalOrientation ? "50%" : "0", + bottom: horizontalOrientation ? "0" : "50%", + }, + }), [horizontalOrientation, width, height] ); return ( + <> { orientation={horizontalOrientation ? undefined : "vertical"} sx={sliderSx} > + {props.children} + ); }; diff --git a/frontend/taipy-gui/src/components/Taipy/Input.tsx b/frontend/taipy-gui/src/components/Taipy/Input.tsx index dba0d31d53..dd4ec43473 100644 --- a/frontend/taipy-gui/src/components/Taipy/Input.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Input.tsx @@ -23,6 +23,7 @@ import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; import { createSendActionNameAction, createSendUpdateAction } from "../../context/taipyReducers"; import { getCssSize, TaipyInputProps } from "./utils"; import { useClassNames, useDispatch, useDynamicProperty, useModule } from "../../utils/hooks"; +import { getComponentClassName } from "./TaipyStyle"; const AUTHORIZED_KEYS = ["Enter", "Escape", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"]; @@ -328,12 +329,13 @@ const Input = (props: TaipyInputProps) => { return ( + <> { multiline={multiline} minRows={linesShown} /> + {props.children} + ); }; diff --git a/frontend/taipy-gui/src/components/Taipy/Layout.tsx b/frontend/taipy-gui/src/components/Taipy/Layout.tsx index 1beca62cc3..d5459bbe4c 100644 --- a/frontend/taipy-gui/src/components/Taipy/Layout.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Layout.tsx @@ -16,6 +16,7 @@ import Box from "@mui/material/Box"; import { useClassNames, useIsMobile } from "../../utils/hooks"; import { TaipyBaseProps } from "./utils"; +import { getComponentClassName } from "./TaipyStyle"; interface LayoutProps extends TaipyBaseProps { columns?: string; @@ -51,7 +52,7 @@ const Layout = (props: LayoutProps) => { }, [columns, columns_Mobile_, gap, isMobile]); return ( - + {props.children} ); diff --git a/frontend/taipy-gui/src/components/Taipy/Login.tsx b/frontend/taipy-gui/src/components/Taipy/Login.tsx index a2058732c6..daefc82db1 100644 --- a/frontend/taipy-gui/src/components/Taipy/Login.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Login.tsx @@ -30,6 +30,7 @@ import { SxProps, Theme } from "@mui/system"; import { createSendActionNameAction } from "../../context/taipyReducers"; import { TaipyBaseProps, getSuffixedClassNames } from "./utils"; import { useClassNames, useDispatch, useModule } from "../../utils/hooks"; +import { getComponentClassName } from "./TaipyStyle"; // allow only one instance of this component let nbLogins = 0; @@ -48,7 +49,7 @@ const closeSx: SxProps = { alignSelf: "start", }; const titleSx = { m: 0, p: 2, display: "flex", paddingRight: "0.1em" }; -const userProps = { htmlInput: { autoComplete: "username" }}; +const userProps = { htmlInput: { autoComplete: "username" } }; const pwdProps = { autoComplete: "current-password" }; const Login = (props: LoginProps) => { @@ -97,20 +98,23 @@ const Login = (props: LoginProps) => { [] ); const passwordProps = useMemo( - () => ({input: { - endAdornment: ( - - - {showPassword ? : } - - - ), - }, htmlInput: pwdProps}), + () => ({ + input: { + endAdornment: ( + + + {showPassword ? : } + + + ), + }, + htmlInput: pwdProps, + }), [showPassword, handleClickShowPassword, handleMouseDownPassword] ); @@ -125,7 +129,7 @@ const Login = (props: LoginProps) => { }, []); return onlyOne ? ( - + {title} @@ -173,6 +177,7 @@ const Login = (props: LoginProps) => { {showProgress ? : "Log in"} + {props.children} ) : null; }; diff --git a/frontend/taipy-gui/src/components/Taipy/Menu.tsx b/frontend/taipy-gui/src/components/Taipy/Menu.tsx index 917b41c8cc..b98bf2304b 100644 --- a/frontend/taipy-gui/src/components/Taipy/Menu.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Menu.tsx @@ -28,6 +28,7 @@ import { createSendActionNameAction } from "../../context/taipyReducers"; import { MenuProps } from "../../utils/lov"; import { useClassNames, useDispatch, useModule } from "../../utils/hooks"; import { emptyArray } from "../../utils"; +import { getComponentClassName } from "./TaipyStyle"; const boxDrawerStyle = { overflowX: "hidden" } as CSSProperties; const headerSx = { padding: 0 }; @@ -78,7 +79,7 @@ const Menu = (props: MenuProps) => { }, [opened, width, theme]); return lov && lov.length ? ( - + @@ -109,6 +110,7 @@ const Menu = (props: MenuProps) => { ))} + {props.children} ) : null; }; diff --git a/frontend/taipy-gui/src/components/Taipy/Metric.spec.tsx b/frontend/taipy-gui/src/components/Taipy/Metric.spec.tsx index 2d463c4efb..545514ecb7 100644 --- a/frontend/taipy-gui/src/components/Taipy/Metric.spec.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Metric.spec.tsx @@ -163,15 +163,15 @@ const lightTemplate = { describe("Metric Component", () => { it("renders", async () => { - const { getByTestId } = render(); - const elt = getByTestId("test-id"); - expect(elt.tagName).toBe("DIV"); + const { container } = render(); + const elt = container.querySelector(".test"); + expect(elt?.tagName).toBe("DIV"); }); it("displays the right info for class", async () => { - const { getByTestId } = render(); - const elt = getByTestId("test-id"); - expect(elt).toHaveClass("taipy-gauge"); + const { container } = render(); + const elt = container.querySelector(".test"); + expect(elt).toHaveClass("test"); }); it("sets the title when provided", async () => { @@ -203,9 +203,9 @@ describe("Metric Component", () => { }); it("sets the template when provided", async () => { - render(); + const { container } = render(); await waitFor(() => { - const elt = document.querySelector(".main-svg"); + const elt = container.querySelector(".main-svg"); expect(elt).toHaveStyle({ backgroundColor: "rgb(255, 0, 0)", }); @@ -213,23 +213,23 @@ describe("Metric Component", () => { }); it("processes colorMap prop correctly", async () => { - render(); + const { container } = render(); await waitFor(() => { - const elts = document.querySelectorAll(".bg-arc"); - const redElt = Array.from(elts[1].children); + const elements = container.querySelectorAll(".bg-arc"); + const redElt = Array.from(elements[1].children); const redEltHasRedFill = redElt.some((elt) => (elt as HTMLElement).style.fill === "rgb(255, 0, 0)"); expect(redEltHasRedFill).toBeTruthy(); - const blueElt = Array.from(elts[2].children); + const blueElt = Array.from(elements[2].children); const blueEltHasBlueFill = blueElt.some((elt) => (elt as HTMLElement).style.fill === "rgb(0, 0, 255)"); expect(blueEltHasBlueFill).toBeTruthy(); }); }); it("processes delta prop correctly when delta is defined", async () => { - render(); + const { container } = render(); await waitFor(() => { - const elt = document.querySelector(".delta"); + const elt = container.querySelector(".delta"); if (elt) { expect(elt.textContent).toContain("10"); } else { @@ -239,45 +239,45 @@ describe("Metric Component", () => { }); it("applies style correctly when deltaColor is set", async () => { - render(); + const { container } = render(); await waitFor(() => { - const elt = document.querySelector(".delta"); + const elt = container.querySelector(".delta"); expect(elt).toHaveStyle({ - fill: "rgb(255, 65, 54)" - }); + fill: "rgb(255, 65, 54)", + }); }); }); it("applies style correctly when deltaColor is set invert", async () => { - render(); + const { container } = render(); await waitFor(() => { - const elt = document.querySelector(".delta"); + const elt = container.querySelector(".delta"); expect(elt).toHaveStyle({ - fill: "rgb(255, 65, 54)" - }); + fill: "rgb(255, 65, 54)", + }); }); }); it("processes type and threshold props correctly when type is linear", async () => { - render(); + const { container } = render(); await waitFor(() => { - const elt = document.querySelector(".bullet"); + const elt = container.querySelector(".bullet"); expect(elt).toBeInTheDocument(); }); }); it("processes type and threshold props correctly when type is not linear", async () => { - render(); + const { container } = render(); await waitFor(() => { - const elt = document.querySelector(".angular"); + const elt = container.querySelector(".angular"); expect(elt).toBeInTheDocument(); }); }); it("applies style correctly when height is undefined", async () => { - render(); + const { container } = render(); await waitFor(() => { - const elt = document.querySelector(".js-plotly-plot"); + const elt = container.querySelector(".js-plotly-plot"); expect(elt).toHaveStyle({ width: "100%", position: "relative", @@ -287,10 +287,10 @@ describe("Metric Component", () => { }); it("processes type prop correctly when type is none", async () => { - render(); + const { container } = render(); await waitFor(() => { - const angularElm = document.querySelector(".angular"); - const angularAxis = document.querySelector(".angularaxis"); + const angularElm = container.querySelector(".angular"); + const angularAxis = container.querySelector(".angularaxis"); expect(angularElm).not.toBeInTheDocument(); expect(angularAxis).not.toBeInTheDocument(); }); @@ -303,13 +303,13 @@ describe("Metric Component", () => { }, }); - render( + const { container } = render( - - , + + ); await waitFor(() => { - const elt = document.querySelector(".main-svg"); + const elt = container.querySelector(".main-svg"); expect(elt).toHaveStyle({ backgroundColor: "rgb(31, 47, 68)", }); @@ -323,13 +323,13 @@ describe("Metric Component", () => { }, }); - render( + const { container } = render( - - , + + ); await waitFor(() => { - const elt = document.querySelector(".main-svg"); + const elt = container.querySelector(".main-svg"); expect(elt).toHaveStyle({ backgroundColor: "rgb(255, 255, 255)", }); @@ -338,14 +338,14 @@ describe("Metric Component", () => { it.skip("logs an error when template_Dark_ prop is not a valid JSON string", () => { const consoleSpy = jest.spyOn(console, "info"); - render(); + render(); expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Error while parsing Metric.template")); consoleSpy.mockRestore(); }); // TODO: Not working at the moment, need to fix it("logs an error when template_Light_ prop is not a valid JSON string", () => { const consoleSpy = jest.spyOn(console, "info"); - render(); + render(); expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Error while parsing Metric.template")); consoleSpy.mockRestore(); }); diff --git a/frontend/taipy-gui/src/components/Taipy/Metric.tsx b/frontend/taipy-gui/src/components/Taipy/Metric.tsx index be715c5d5e..a837d9ad7a 100644 --- a/frontend/taipy-gui/src/components/Taipy/Metric.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Metric.tsx @@ -21,6 +21,7 @@ import { useClassNames, useDynamicJsonProperty, useDynamicProperty } from "../.. import { extractPrefix, extractSuffix, sprintfToD3Converter } from "../../utils/formatConversion"; import { TaipyBaseProps, TaipyHoverProps } from "./utils"; import { darkThemeTemplate } from "../../themes/darkThemeTemplate"; +import { getComponentClassName } from "./TaipyStyle"; const Plot = lazy(() => import("react-plotly.js")); @@ -42,7 +43,6 @@ interface MetricProps extends TaipyBaseProps, TaipyHoverProps { showValue?: boolean; colorMap?: string; title?: string; - testId?: string; layout?: string; defaultLayout?: string; width?: string | number; @@ -196,10 +196,11 @@ const Metric = (props: MetricProps) => { return ( - + }> + {props.children} ); diff --git a/frontend/taipy-gui/src/components/Taipy/NavBar.tsx b/frontend/taipy-gui/src/components/Taipy/NavBar.tsx index 828d160961..b594eb029e 100644 --- a/frontend/taipy-gui/src/components/Taipy/NavBar.tsx +++ b/frontend/taipy-gui/src/components/Taipy/NavBar.tsx @@ -30,6 +30,7 @@ import { useClassNames, useDynamicProperty, useIsMobile } from "../../utils/hook import { TaipyContext } from "../../context/taipyContext"; import { LovItem } from "../../utils/lov"; import { getBaseURL } from "../../utils"; +import { getComponentClassName } from "./TaipyStyle"; const boxSx = { borderBottom: 1, borderColor: "divider", width: "fit-content" }; @@ -65,15 +66,26 @@ const NavBar = (props: LovProps) => { [state.locations, navigate] ); - const selectedVal = lov.find((it) => (getBaseURL() + it.id.substring(1)) === location.pathname)?.id || (lov.length ? lov[0].id : false); + const selectedVal = + lov.find((it) => getBaseURL() + it.id.substring(1) === location.pathname)?.id || + (lov.length ? lov[0].id : false); return isMobile ? ( <> - setOpened(false)} className={className}> + setOpened(false)} + className={`${className} ${getComponentClassName(props.children)}`} + > {lov.map((val) => ( - setOpened(false)} disabled={!active} selected={selectedVal === val.id}> + setOpened(false)} + disabled={!active} + selected={selectedVal === val.id} + > {typeof val.item === "string" ? val.item : } @@ -86,10 +98,11 @@ const NavBar = (props: LovProps) => { setOpened((o) => !o)}> + {props.children} ) : ( - + {lov.map((val) => ( @@ -102,6 +115,7 @@ const NavBar = (props: LovProps) => { ))} + {props.children} ); }; diff --git a/frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx b/frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx index 7cd8f6018c..41c9a909e2 100644 --- a/frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx +++ b/frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx @@ -85,6 +85,7 @@ import { import TableFilter from "./TableFilter"; import { getSuffixedClassNames, getUpdateVar } from "./utils"; import { emptyArray } from "../../utils"; +import { getComponentClassName } from "./TaipyStyle"; const loadingStyle: CSSProperties = { width: "100%", height: "3em", textAlign: "right", verticalAlign: "center" }; const skeletonSx = { width: "100%", height: "3em" }; @@ -490,7 +491,7 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => { const boxSx = useMemo(() => ({ ...baseBoxSx, width: width }), [width]); return ( - + @@ -673,6 +674,7 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => { /> ))} + {props.children} ); }; diff --git a/frontend/taipy-gui/src/components/Taipy/Pane.tsx b/frontend/taipy-gui/src/components/Taipy/Pane.tsx index fcf4218cd8..6aa7eb3027 100644 --- a/frontend/taipy-gui/src/components/Taipy/Pane.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Pane.tsx @@ -27,6 +27,7 @@ import { createSendActionNameAction, createSendUpdateAction } from "../../contex import { useClassNames, useDispatch, useDynamicProperty, useModule } from "../../utils/hooks"; import TaipyRendered from "../pages/TaipyRendered"; import { getSuffixedClassNames, TaipyActiveProps, TaipyChangeProps } from "./utils"; +import { getComponentClassName } from "./TaipyStyle"; type AnchorType = "left" | "bottom" | "right" | "top" | undefined; @@ -141,7 +142,7 @@ const Pane = (props: PaneProps) => { anchor={anchor} open={open} onClose={handleClose} - className={className} + className={`${className} ${getComponentClassName(props.children)}`} > {persistent ? ( <> diff --git a/frontend/taipy-gui/src/components/Taipy/Part.tsx b/frontend/taipy-gui/src/components/Taipy/Part.tsx index c27776d789..3657b9593e 100644 --- a/frontend/taipy-gui/src/components/Taipy/Part.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Part.tsx @@ -18,6 +18,7 @@ import { useClassNames, useDynamicProperty } from "../../utils/hooks"; import TaipyRendered from "../pages/TaipyRendered"; import { expandSx, getCssSize, TaipyBaseProps } from "./utils"; import { TaipyContext } from "../../context/taipyContext"; +import { getComponentClassName } from "./TaipyStyle"; interface PartProps extends TaipyBaseProps { render?: boolean; @@ -38,7 +39,7 @@ const IframeStyle = { }; const Part = (props: PartProps) => { - const { id, children, partial, defaultPartial } = props; + const { id, partial, defaultPartial } = props; const { state } = useContext(TaipyContext); const className = useClassNames(props.libClassName, props.dynamicClassName, props.className); @@ -58,13 +59,13 @@ const Part = (props: PartProps) => { const boxSx = useMemo(() => expandSx(height ? { height: height } : undefined, props.width ? {width: getCssSize(props.width)}: undefined), [height, props.width]); return render ? ( - + {iFrame ? (