From 5697279805751f9d3e4a2515dcaf42896755fce7 Mon Sep 17 00:00:00 2001 From: Martynas <43886789+MartynasStrazdas@users.noreply.github.com> Date: Mon, 11 Nov 2024 09:07:24 +0200 Subject: [PATCH] [Property-grid]: add callback to show only editor (#1090) * Initial * extract-api * fix * extract-api * extract-api * add test * change * adjustments * fixes * extract-api * extract-api * NextVersion md (cherry picked from commit fa31495e9e3abc446bd3a8d69ac76f18ed8d1383) # Conflicts: # docs/changehistory/NextVersion.md --- common/api/components-react.api.md | 8 ++ ...-to-show-only-editor_2024-10-31-10-15.json | 10 +++ docs/changehistory/NextVersion.md | 11 +++ .../src/components/PropertyGrid.stories.tsx | 76 ++++++++++++++++++- .../editors/EditorContainer.tsx | 11 ++- .../properties/renderers/PropertyRenderer.tsx | 23 +++++- .../component/PropertyGridCommons.ts | 2 + ...PropertyGridEventsRelatedPropsSupplier.tsx | 1 - .../component/VirtualizedPropertyGrid.tsx | 6 ++ .../flat-properties/FlatPropertyRenderer.tsx | 74 +++++++----------- .../renderers/PropertyRenderer.test.tsx | 48 +++++++++++- .../internal/FlatPropertyRenderer.test.tsx | 75 +++++++++++++++++- 12 files changed, 291 insertions(+), 54 deletions(-) create mode 100644 common/changes/@itwin/components-react/mast-property-grid-callback-to-show-only-editor_2024-10-31-10-15.json diff --git a/common/api/components-react.api.md b/common/api/components-react.api.md index 823d338ad43..6ba757bbca9 100644 --- a/common/api/components-react.api.md +++ b/common/api/components-react.api.md @@ -246,6 +246,7 @@ export interface CheckboxStateChange { export interface CommonPropertyGridProps extends CommonProps { actionButtonRenderers?: ActionButtonRenderer[]; actionButtonWidth?: number; + alwaysShowEditor?: (property: PropertyRecord) => boolean; horizontalOrientationMinWidth?: number; isOrientationFixed?: boolean; isPropertyEditingEnabled?: boolean; @@ -547,6 +548,7 @@ export interface EditorContainerProps extends CommonProps { // @internal (undocumented) ignoreEditorBlur?: boolean; onCancel: () => void; + onClick?: () => void; onCommit: (args: PropertyUpdatedArgs) => void; propertyRecord: PropertyRecord; setFocus?: boolean; @@ -2325,9 +2327,11 @@ export enum SelectionModeFlags { // @public export interface SharedRendererProps { actionButtonRenderers?: ActionButtonRenderer[]; + alwaysShowEditor?: (property: PropertyRecord) => boolean; columnInfo?: PropertyGridColumnInfo; columnRatio?: number; isHoverable?: boolean; + isPropertyEditingEnabled?: boolean; isResizeHandleBeingDragged?: boolean; isResizeHandleHovered?: boolean; isSelectable?: boolean; @@ -3261,6 +3265,8 @@ export interface VirtualizedPropertyGridContext { // (undocumented) actionButtonRenderers?: ActionButtonRenderer[]; // (undocumented) + alwaysShowEditor?: (property: PropertyRecord) => boolean; + // (undocumented) columnInfo: PropertyGridColumnInfo; // (undocumented) columnRatio: number; @@ -3275,6 +3281,8 @@ export interface VirtualizedPropertyGridContext { // (undocumented) highlight?: PropertyGridContentHighlightProps; // (undocumented) + isPropertyEditingEnabled?: boolean; + // (undocumented) isPropertyHoverEnabled: boolean; // (undocumented) isPropertySelectionEnabled: boolean; diff --git a/common/changes/@itwin/components-react/mast-property-grid-callback-to-show-only-editor_2024-10-31-10-15.json b/common/changes/@itwin/components-react/mast-property-grid-callback-to-show-only-editor_2024-10-31-10-15.json new file mode 100644 index 00000000000..5fcf3fd6581 --- /dev/null +++ b/common/changes/@itwin/components-react/mast-property-grid-callback-to-show-only-editor_2024-10-31-10-15.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/components-react", + "comment": "Added a callback to `VirtualizedPropertyGrid` which determines which editors should always be visible", + "type": "none" + } + ], + "packageName": "@itwin/components-react" +} \ No newline at end of file diff --git a/docs/changehistory/NextVersion.md b/docs/changehistory/NextVersion.md index e6d05c39f9b..2a18504ab4d 100644 --- a/docs/changehistory/NextVersion.md +++ b/docs/changehistory/NextVersion.md @@ -1,5 +1,6 @@ # NextVersion +<<<<<<< HEAD Table of contents: - [Drop support for iTwin.js 3.x](#drop-support-for-itwinjs-3x) @@ -165,3 +166,13 @@ AppUI packages now specify `@itwin/itwinui-react` as a [peer dependency](https:/ ### Changes - Removed the `resize-observer-polyfill` dependency because `ResizeObserver` is well supported by modern browsers, eliminating the need for a polyfill. [#1045](https://github.com/iTwin/appui/pull/1045) +======= +- [@itwin/components-react](#itwincomponents-react) + - [Additions](#additions) + +## @itwin/components-react + +### Additions + +- Added a callback to `VirtualizedPropertyGrid` which determines which editors should always be visible. [#1090](https://github.com/iTwin/appui/pull/1090) +>>>>>>> fa31495e9 ([Property-grid]: add callback to show only editor (#1090)) diff --git a/docs/storybook/src/components/PropertyGrid.stories.tsx b/docs/storybook/src/components/PropertyGrid.stories.tsx index 878c3edf92a..6c7d536ec42 100644 --- a/docs/storybook/src/components/PropertyGrid.stories.tsx +++ b/docs/storybook/src/components/PropertyGrid.stories.tsx @@ -15,7 +15,7 @@ import { PropertyDataChangeEvent, PropertyValueRendererManager, VirtualizedPropertyGridWithDataProvider, -} from "@itwin/components-react-internal/src/components-react"; +} from "@itwin/components-react"; import { MultilineTextPropertyValueRenderer } from "@itwin/components-react-internal/src/components-react/properties/renderers/value/MultilineTextPropertyValueRenderer"; import { AppUiDecorator } from "../Decorators"; @@ -364,6 +364,78 @@ export const Links: Story = { }, }; +export const AlwaysVisibleEditor: Story = { + args: { + dataProvider: { + getData: async () => ({ + label: PropertyRecord.fromString("Record 1"), + categories: [ + { name: "Group_1", label: "Group 1", expand: true }, + { name: "Group_2", label: "Group 2", expand: true }, + ], + records: { + Group_1: [ + new PropertyRecord( + { + valueFormat: PropertyValueFormat.Primitive, + value: true, + }, + { + name: "toggleProperty", + displayLabel: "Always visible toggle editor", + typename: "boolean", + editor: { name: "toggle" }, + } + ), + new PropertyRecord( + { + valueFormat: PropertyValueFormat.Primitive, + value: false, + }, + { + name: "toggleProperty2", + displayLabel: "Always visible toggle editor", + typename: "boolean", + editor: { name: "toggle" }, + } + ), + ], + Group_2: [ + new PropertyRecord( + { + valueFormat: PropertyValueFormat.Primitive, + value: true, + }, + { + name: "toggleProperty3", + displayLabel: "Not always visible boolean editor", + typename: "boolean", + } + ), + new PropertyRecord( + { + valueFormat: PropertyValueFormat.Primitive, + value: "Text", + }, + { + name: "stringProperty", + displayLabel: "Not always visible string editor", + typename: "string", + } + ), + ], + Group_3: [], + }, + }), + onDataChanged: new PropertyDataChangeEvent(), + }, + onPropertyContextMenu: undefined, + isPropertyEditingEnabled: true, + alwaysShowEditor: (propertyRecord: PropertyRecord) => + propertyRecord.property.editor?.name === "toggle", + }, +}; + rendererManager.registerRenderer("customRendererStructPropertyRenderer", { canRender: () => true, render: (record) => { @@ -401,7 +473,7 @@ rendererManager.registerRenderer("customRendererArrayPropertyRenderer", { rendererManager.registerRenderer("defaultRendererPropertyRenderer", { canRender: () => false, render: () => { -
Should not render
; + return
Should not render
; }, }); diff --git a/ui/components-react/src/components-react/editors/EditorContainer.tsx b/ui/components-react/src/components-react/editors/EditorContainer.tsx index 28f5274669a..85a82c5e5da 100644 --- a/ui/components-react/src/components-react/editors/EditorContainer.tsx +++ b/ui/components-react/src/components-react/editors/EditorContainer.tsx @@ -71,6 +71,8 @@ export interface EditorContainerProps extends CommonProps { onCancel: () => void; /** Indicates whether the Property Editor should set focus */ setFocus?: boolean; + /** Handler for click */ + onClick?: () => void; /** @internal */ ignoreEditorBlur?: boolean; @@ -107,13 +109,18 @@ export function EditorContainer(props: EditorContainerProps) { setFocus, onCommit, onCancel, + onClick, ...rest } = props; const editorRef = React.useRef(); const propertyEditorRef = React.useRef(); - const handleClick = (e: React.MouseEvent) => e.stopPropagation(); + const handleClick = (e: React.MouseEvent) => { + onClick?.(); + e.stopPropagation(); + }; + const handleContextMenu = (e: React.MouseEvent) => e.stopPropagation(); const handleContainerBlur = (e: React.FocusEvent) => e.stopPropagation(); const handleEditorCommit = (args: PropertyUpdatedArgs) => void commit(args); @@ -269,7 +276,7 @@ export function EditorContainer(props: EditorContainerProps) { onBlur={handleContainerBlur} onKeyDown={handleKeyDown} onClick={handleClick} - onContextMenu={handleClick} + onContextMenu={handleContextMenu} title={title} data-testid="editor-container" role="presentation" diff --git a/ui/components-react/src/components-react/properties/renderers/PropertyRenderer.tsx b/ui/components-react/src/components-react/properties/renderers/PropertyRenderer.tsx index 7a378e46831..f1ac13e03f9 100644 --- a/ui/components-react/src/components-react/properties/renderers/PropertyRenderer.tsx +++ b/ui/components-react/src/components-react/properties/renderers/PropertyRenderer.tsx @@ -56,6 +56,8 @@ export interface SharedRendererProps { actionButtonRenderers?: ActionButtonRenderer[]; /** Is resize handle hovered */ isResizeHandleHovered?: boolean; + /** Enables/disables property editing */ + isPropertyEditingEnabled?: boolean; /** Callback to hover event change */ onResizeHandleHoverChanged?: (isHovered: boolean) => void; /** Is resize handle being dragged */ @@ -64,6 +66,8 @@ export interface SharedRendererProps { onResizeHandleDragChanged?: (isDragStarted: boolean) => void; /** Information for styling property grid columns */ columnInfo?: PropertyGridColumnInfo; + /** Callback to determine which editors should be always visible */ + alwaysShowEditor?: (property: PropertyRecord) => boolean; } /** Properties of [[PropertyRenderer]] React component @@ -101,6 +105,10 @@ export const PropertyRenderer = (props: PropertyRendererProps) => { propertyValueRendererManager, onEditCommit, onEditCancel, + alwaysShowEditor, + isPropertyEditingEnabled, + onClick, + uniqueKey, ...restProps } = props; @@ -115,14 +123,19 @@ export const PropertyRenderer = (props: PropertyRendererProps) => { if (onEditCancel) onEditCancel(); }, [onEditCancel]); + const alwaysShowsEditor = props.alwaysShowEditor + ? props.alwaysShowEditor(props.propertyRecord) + : false; + React.useEffect(() => { - if (isEditing) { + if (isEditing || (alwaysShowsEditor && isPropertyEditingEnabled)) { setDisplayValue( onClick?.(propertyRecord, uniqueKey)} /> ); return; @@ -144,10 +157,16 @@ export const PropertyRenderer = (props: PropertyRendererProps) => { onCommit, onCancel, isEditing, + alwaysShowsEditor, + isPropertyEditingEnabled, + onClick, + uniqueKey, ]); const primitiveRendererProps: PrimitiveRendererProps = { ...restProps, + onClick, + uniqueKey, propertyRecord, orientation, indentation, diff --git a/ui/components-react/src/components-react/propertygrid/component/PropertyGridCommons.ts b/ui/components-react/src/components-react/propertygrid/component/PropertyGridCommons.ts index 0316fd11919..ebf0a80c57f 100644 --- a/ui/components-react/src/components-react/propertygrid/component/PropertyGridCommons.ts +++ b/ui/components-react/src/components-react/propertygrid/component/PropertyGridCommons.ts @@ -92,6 +92,8 @@ export interface CommonPropertyGridProps extends CommonProps { * to render an action button for the property or not. */ actionButtonRenderers?: ActionButtonRenderer[]; + /** Callback to determine which editors should be always visible */ + alwaysShowEditor?: (property: PropertyRecord) => boolean; } /** diff --git a/ui/components-react/src/components-react/propertygrid/component/PropertyGridEventsRelatedPropsSupplier.tsx b/ui/components-react/src/components-react/propertygrid/component/PropertyGridEventsRelatedPropsSupplier.tsx index 3060d9e67bd..12b55b105f8 100644 --- a/ui/components-react/src/components-react/propertygrid/component/PropertyGridEventsRelatedPropsSupplier.tsx +++ b/ui/components-react/src/components-react/propertygrid/component/PropertyGridEventsRelatedPropsSupplier.tsx @@ -43,7 +43,6 @@ export type PropertyGridEventsRelatedPropsSupplierProps = Pick< CommonPropertyGridProps, | "onPropertyContextMenu" | "isPropertySelectionOnRightClickEnabled" - | "isPropertySelectionOnRightClickEnabled" | "onPropertySelectionChanged" | "isPropertyEditingEnabled" | "onPropertyUpdated" diff --git a/ui/components-react/src/components-react/propertygrid/component/VirtualizedPropertyGrid.tsx b/ui/components-react/src/components-react/propertygrid/component/VirtualizedPropertyGrid.tsx index 9b59710b766..fb8afb6b839 100644 --- a/ui/components-react/src/components-react/propertygrid/component/VirtualizedPropertyGrid.tsx +++ b/ui/components-react/src/components-react/propertygrid/component/VirtualizedPropertyGrid.tsx @@ -109,7 +109,9 @@ export interface VirtualizedPropertyGridContext { isPropertyHoverEnabled: boolean; isPropertySelectionEnabled: boolean; selectedPropertyKey?: string; + isPropertyEditingEnabled?: boolean; + alwaysShowEditor?: (property: PropertyRecord) => boolean; onPropertyClicked?: (property: PropertyRecord, key?: string) => void; onPropertyRightClicked?: (property: PropertyRecord, key?: string) => void; onPropertyContextMenu?: ( @@ -372,7 +374,9 @@ export class VirtualizedPropertyGrid extends React.Component< isPropertySelectionEnabled: selectionContext.isPropertySelectionEnabled, selectedPropertyKey: selectionContext.selectedPropertyKey, + isPropertyEditingEnabled: this.props.isPropertyEditingEnabled, + alwaysShowEditor: this.props.alwaysShowEditor, onPropertyClicked: selectionContext.onPropertyClicked, onPropertyRightClicked: selectionContext.onPropertyRightClicked, onPropertyContextMenu: selectionContext.onPropertyContextMenu, @@ -602,6 +606,8 @@ const FlatGridItemNode = React.memo( onContextMenu={gridContext.onPropertyContextMenu} category={parentCategoryItem.derivedCategory} isEditing={selectionKey === gridContext.editingPropertyKey} + isPropertyEditingEnabled={gridContext.isPropertyEditingEnabled} + alwaysShowEditor={gridContext.alwaysShowEditor} onEditCommit={gridContext.onEditCommit} onEditCancel={gridContext.onEditCancel} isExpanded={node.isExpanded} diff --git a/ui/components-react/src/components-react/propertygrid/internal/flat-properties/FlatPropertyRenderer.tsx b/ui/components-react/src/components-react/propertygrid/internal/flat-properties/FlatPropertyRenderer.tsx index 98f913eb67f..35ab2acfd73 100644 --- a/ui/components-react/src/components-react/propertygrid/internal/flat-properties/FlatPropertyRenderer.tsx +++ b/ui/components-react/src/components-react/propertygrid/internal/flat-properties/FlatPropertyRenderer.tsx @@ -32,6 +32,8 @@ export interface FlatPropertyRendererProps extends SharedRendererProps { indentation?: number; /** Indicates property is being edited */ isEditing?: boolean; + /** Callback to determine which editors should be always visible */ + alwaysShowEditor?: (property: PropertyRecord) => boolean; /** Called when property edit is committed. */ onEditCommit?: ( args: PropertyUpdatedArgs, @@ -60,35 +62,10 @@ export interface FlatPropertyRendererProps extends SharedRendererProps { export const FlatPropertyRenderer: React.FC = ( props ) => { - const { - category, - propertyValueRendererManager, - isEditing, - onEditCommit, - onEditCancel, - onHeightChanged, - highlight, - ...passthroughProps - } = props; - - const valueElementRenderer = () => ( - - ); + const { propertyValueRendererManager, highlight, ...passthroughProps } = + props; + + const valueElementRenderer = () => ; const primitiveRendererProps: PrimitiveRendererProps = { ...passthroughProps, @@ -147,6 +124,8 @@ export const FlatPropertyRenderer: React.FC = ( interface DisplayValueProps { isEditing?: boolean; + isPropertyEditingEnabled?: boolean; + alwaysShowEditor?: (property: PropertyRecord) => boolean; propertyRecord: PropertyRecord; orientation: Orientation; @@ -158,7 +137,8 @@ interface DisplayValueProps { isExpanded?: boolean; onExpansionToggled?: () => void; onHeightChanged?: (newHeight: number) => void; - + onClick?: (property: PropertyRecord, key?: string) => void; + uniqueKey?: string; category?: PropertyCategory; onEditCancel?: () => void; onEditCommit?: ( @@ -179,7 +159,14 @@ const DisplayValue: React.FC = (props) => { props.onHeightChanged ); - if (props.isEditing) { + const alwaysShowsEditor = props.alwaysShowEditor + ? props.alwaysShowEditor(props.propertyRecord) + : false; + + if ( + props.isEditing || + (alwaysShowsEditor && props.isPropertyEditingEnabled) + ) { const _onEditCommit = (args: PropertyUpdatedArgs) => { if (props.category) props.onEditCommit?.(args, props.category); }; @@ -189,24 +176,21 @@ const DisplayValue: React.FC = (props) => { propertyRecord={props.propertyRecord} onCommit={_onEditCommit} onCancel={props.onEditCancel ?? (() => {})} - setFocus={true} + setFocus={props.isEditing} + onClick={() => props.onClick?.(props.propertyRecord, props.uniqueKey)} /> ); } - return ( - <> - {CommonPropertyRenderer.createNewDisplayValue( - props.orientation, - props.propertyRecord, - props.indentation, - props.propertyValueRendererManager, - props.isExpanded, - props.onExpansionToggled, - props.onHeightChanged, - props.highlight - )} - + return CommonPropertyRenderer.createNewDisplayValue( + props.orientation, + props.propertyRecord, + props.indentation, + props.propertyValueRendererManager, + props.isExpanded, + props.onExpansionToggled, + props.onHeightChanged, + props.highlight ); }; diff --git a/ui/components-react/src/test/properties/renderers/PropertyRenderer.test.tsx b/ui/components-react/src/test/properties/renderers/PropertyRenderer.test.tsx index 24b0a2810b2..0ed0d9a3fba 100644 --- a/ui/components-react/src/test/properties/renderers/PropertyRenderer.test.tsx +++ b/ui/components-react/src/test/properties/renderers/PropertyRenderer.test.tsx @@ -11,7 +11,7 @@ import TestUtils, { styleMatch, userEvent, } from "../../TestUtils.js"; -import { fireEvent, render, screen } from "@testing-library/react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import type { PropertyRecord } from "@itwin/appui-abstract"; describe("PropertyRenderer", () => { @@ -282,6 +282,52 @@ describe("PropertyRenderer", () => { ); }); + it("renders an editor at all times", () => { + const textPropertyRecord = TestUtils.createPrimitiveStringProperty( + "Label", + "Model", + "DisplayValue", + { name: "textEditor" } + ); + render( + + property.property.editor?.name === "textEditor" + } + /> + ); + + expect(screen.getByRole("textbox")).satisfy( + selectorMatches(".components-text-editor") + ); + }); + + it("calls on click when clicking on an editor", async () => { + const spy = vi.fn(); + const textPropertyRecord = TestUtils.createPrimitiveStringProperty( + "Label", + "Model", + "DisplayValue", + { name: "textEditor" } + ); + render( + + ); + + const editor = await waitFor(() => screen.getByRole("textbox")); + fireEvent.click(editor); + expect(spy).toHaveBeenCalledOnce(); + }); + it("calls onEditCommit on Enter key when editing", async () => { const spy = vi.fn(); const propertyRenderer = render( diff --git a/ui/components-react/src/test/propertygrid/component/internal/FlatPropertyRenderer.test.tsx b/ui/components-react/src/test/propertygrid/component/internal/FlatPropertyRenderer.test.tsx index 3e06c54a662..84124cc39ba 100644 --- a/ui/components-react/src/test/propertygrid/component/internal/FlatPropertyRenderer.test.tsx +++ b/ui/components-react/src/test/propertygrid/component/internal/FlatPropertyRenderer.test.tsx @@ -5,7 +5,7 @@ import * as React from "react"; import type { PropertyRecord } from "@itwin/appui-abstract"; import { Orientation } from "@itwin/core-react"; -import { fireEvent, render, screen } from "@testing-library/react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { PropertyValueRendererManager } from "../../../../components-react/properties/ValueRendererManager.js"; import { FlatPropertyRenderer } from "../../../../components-react/propertygrid/internal/flat-properties/FlatPropertyRenderer.js"; import TestUtils, { selectorMatches, userEvent } from "../../../TestUtils.js"; @@ -362,6 +362,79 @@ describe("FlatPropertyRenderer", () => { ); }); + it("renders an editor at all times", () => { + const textPropertyRecord = TestUtils.createPrimitiveStringProperty( + "Label", + "Model", + "DisplayValue", + { name: "textEditor" } + ); + render( + + property.property.editor?.name === "textEditor" + } + isExpanded={false} + onExpansionToggled={() => {}} + /> + ); + + expect(screen.getByRole("textbox")).satisfy( + selectorMatches(".components-text-editor") + ); + }); + + it("calls on click when clicking on an editor", async () => { + const spy = vi.fn(); + const textPropertyRecord = TestUtils.createPrimitiveStringProperty( + "Label", + "Model", + "DisplayValue", + { name: "textEditor" } + ); + render( + {}} + /> + ); + + const editor = await waitFor(() => screen.getByRole("textbox")); + fireEvent.click(editor); + expect(spy).toHaveBeenCalledOnce(); + }); + + it("renders an editor with focus when alwaysShowEditor is defined and isEditing is true", async () => { + const textPropertyRecord = TestUtils.createPrimitiveStringProperty( + "Label", + "Model", + "DisplayValue", + { name: "textEditor" } + ); + render( + false} + isExpanded={false} + onExpansionToggled={() => {}} + /> + ); + + const input = await waitFor(() => screen.getByRole("textbox")); + await waitFor(() => expect(input).to.be.eq(document.activeElement)); + }); + it("calls onEditCommit on Enter key when editing", async () => { const spy = vi.fn(); const propertyRenderer = render(