diff --git a/.vscode/cSpell.json b/.vscode/cSpell.json index 58a9d055002..ab24753bafb 100644 --- a/.vscode/cSpell.json +++ b/.vscode/cSpell.json @@ -247,6 +247,7 @@ "toolsettings", "treediv", "tspan", + "typeguards", "typemoq", "typograpy", "uiadmin", diff --git a/apps/test-app/package.json b/apps/test-app/package.json index 62dd55d112b..564899bb4f4 100644 --- a/apps/test-app/package.json +++ b/apps/test-app/package.json @@ -103,7 +103,6 @@ "@itwin/itwinui-icons-react": "^2.8.0", "@itwin/itwinui-layouts-react": "~0.4.1", "@itwin/itwinui-layouts-css": "~0.4.0", - "@itwin/presentation-common": "^4.0.0", "@itwin/reality-data-client": "1.2.2", "@itwin/webgl-compatibility": "^4.0.0", "@tanstack/react-router": "^1.87.6", diff --git a/apps/test-providers/src/tools/SampleTool.ts b/apps/test-providers/src/tools/SampleTool.ts index d2fa18af698..12549d9810c 100644 --- a/apps/test-providers/src/tools/SampleTool.ts +++ b/apps/test-providers/src/tools/SampleTool.ts @@ -79,8 +79,7 @@ export class SampleTool extends PrimitiveTool { // Tool Setting Properties // ------------- Enum based picklist --------------- - // Example of async method used to populate enum values - private _getChoices = async () => { + private _getChoices = () => { return [ { label: SampleTool.getOptionString("Red"), value: ToolOptions.Red }, { label: SampleTool.getOptionString("White"), value: ToolOptions.White }, diff --git a/apps/test-providers/src/ui/components/EditorExampleComponent.tsx b/apps/test-providers/src/ui/components/EditorExampleComponent.tsx index d4436704763..1fabd4cac0f 100644 --- a/apps/test-providers/src/ui/components/EditorExampleComponent.tsx +++ b/apps/test-providers/src/ui/components/EditorExampleComponent.tsx @@ -20,8 +20,9 @@ import { SliderEditorParams, StandardTypeNames as Type, } from "@itwin/appui-abstract"; -import { EditorContainer } from "@itwin/components-react"; +import { EditorContainer, PropertyRecordEditor } from "@itwin/components-react"; import { + Divider, DropdownMenu, Flex, IconButton, @@ -89,164 +90,218 @@ const inputEditorSizeParams = { maxLength: 5, } as InputEditorSizeParams; +const propertyRecords = [ + createPropertyRecord(Type.String, "hi"), + createPropertyRecord(Type.String, "hi", { + name: Editor.MultiLine, + params: [ + { + type: EditorParam.MultilineText, + rows: 5, + } as MultilineTextEditorParams, + ], + }), + // BROKEN! + // createPropertyRecord(Type.String, "icon-app-2", { + // name: Editor.IconPicker, + // params: [ + // { + // type: EditorParam.IconListData, + // iconValue: "icon-app-2", + // numColumns: 2, + // iconValues: ["icon-app-1", "icon-app-2", "icon-apps-itwin"], + // } as IconListEditorParams, + // ], + // }), + createPropertyRecord(Type.DateTime, new Date(2018, 0, 1)), + createPropertyRecord(Type.ShortDate, new Date(2018, 0, 1)), + createPropertyRecord(Type.Number, 1, { + name: Editor.Slider, + params: [ + { + type: EditorParam.Slider, + minimum: 0, + maximum: 10, + } as SliderEditorParams, + ], + }), + createPropertyRecord(Type.Number, 1, { + name: Editor.NumberCustom, + params: [customFormattedNumberParams], + }), + createPropertyRecord(Type.Number, 1, { + name: Editor.NumberCustom, + params: [ + customFormattedNumberParams, + { + type: EditorParam.Icon, + definition: { iconSpec: "icon-placeholder" }, + } as IconEditorParams, + ], + }), + createPropertyRecord(Type.Number, 1, Editor.NumericInput), + createPropertyRecord(Type.Number, 1, { + name: Editor.NumericInput, + params: [inputEditorSizeParams], + }), + createPropertyRecord(Type.Number, 1, { + name: Editor.NumericInput, + params: [ + { + type: EditorParam.Range, + minimum: 0, + maximum: 10, + step: 0.5, + precision: 1, + } as RangeEditorParams, + ], + }), + createPropertyRecord(Type.Number, 1, { + name: Editor.NumericInput, + params: [ + inputEditorSizeParams, + { + type: EditorParam.Range, + minimum: 0, + maximum: 10, + step: 0.25, + precision: 2, + } as RangeEditorParams, + ], + }), + createPropertyRecord(Type.Boolean, true), + createPropertyRecord(Type.Boolean, true, Editor.Toggle), + createPropertyRecord(Type.Boolean, true, { + name: "image-check-box", + params: [ + { + type: EditorParam.CheckBoxImages, + imageOff: "icon-visibility-hide-2", + imageOn: "icon-visibility", + } as ImageCheckBoxParams, + ], + }), + createEnumProperty(), + createEnumProperty(Editor.EnumButtonGroup), + createEnumProperty({ + name: Editor.EnumButtonGroup, + params: [ + { + type: EditorParam.ButtonGroupData, + buttons: [ + { + iconSpec: "icon-app-1", + }, + { + iconSpec: "icon-app-2", + }, + { + iconSpec: "icon-apps-itwin", + }, + ], + } as ButtonGroupEditorParams, + ], + }), +]; + /** Component that display at least 1 of each variety of editors registered by default in Components-react. */ export function EditorExampleComponent() { return ( - - {[ - createPropertyRecord(Type.String, "hi"), - createPropertyRecord(Type.String, "hi", { - name: Editor.MultiLine, - params: [ - { - type: EditorParam.MultilineText, - rows: 5, - } as MultilineTextEditorParams, - ], - }), - // BROKEN! - // createPropertyRecord(Type.String, "icon-app-2", { - // name: Editor.IconPicker, - // params: [ - // { - // type: EditorParam.IconListData, - // iconValue: "icon-app-2", - // numColumns: 2, - // iconValues: ["icon-app-1", "icon-app-2", "icon-apps-itwin"], - // } as IconListEditorParams, - // ], - // }), - createPropertyRecord(Type.DateTime, new Date(2018, 0, 1)), - createPropertyRecord(Type.ShortDate, new Date(2018, 0, 1)), - createPropertyRecord(Type.Number, 1, { - name: Editor.Slider, - params: [ - { - type: EditorParam.Slider, - minimum: 0, - maximum: 10, - } as SliderEditorParams, - ], - }), - createPropertyRecord(Type.Number, 1, { - name: Editor.NumberCustom, - params: [customFormattedNumberParams], - }), - createPropertyRecord(Type.Number, 1, { - name: Editor.NumberCustom, - params: [ - customFormattedNumberParams, - { - type: EditorParam.Icon, - definition: { iconSpec: "icon-placeholder" }, - } as IconEditorParams, - ], - }), - createPropertyRecord(Type.Number, 1, Editor.NumericInput), - createPropertyRecord(Type.Number, 1, { - name: Editor.NumericInput, - params: [inputEditorSizeParams], - }), - createPropertyRecord(Type.Number, 1, { - name: Editor.NumericInput, - params: [ - { - type: EditorParam.Range, - minimum: 0, - maximum: 10, - step: 0.5, - precision: 1, - } as RangeEditorParams, - ], - }), - createPropertyRecord(Type.Number, 1, { - name: Editor.NumericInput, - params: [ - inputEditorSizeParams, - { - type: EditorParam.Range, - minimum: 0, - maximum: 10, - step: 0.25, - precision: 2, - } as RangeEditorParams, - ], - }), - createPropertyRecord(Type.Boolean, true), - createPropertyRecord(Type.Boolean, true, Editor.Toggle), - createPropertyRecord(Type.Boolean, true, { - name: "image-check-box", - params: [ - { - type: EditorParam.CheckBoxImages, - imageOff: "icon-visibility-hide-2", - imageOn: "icon-visibility", - } as ImageCheckBoxParams, - ], - }), - createEnumProperty(), - createEnumProperty(Editor.EnumButtonGroup), - createEnumProperty({ - name: Editor.EnumButtonGroup, - params: [ - { - type: EditorParam.ButtonGroupData, - buttons: [ - { - iconSpec: "icon-app-1", - }, - { - iconSpec: "icon-app-2", - }, - { - iconSpec: "icon-apps-itwin", - }, - ], - } as ButtonGroupEditorParams, - ], - }), - ].map((record) => { - const key = `${PropertyValueFormat[record.value.valueFormat]}:${ - record.property.typename - }:${record.property.editor?.name ?? "Default"}[${ - record.property.editor?.params?.map((p) => p.type).join(",") ?? "" - }]`.replace("[]", ""); + + {propertyRecords.map((record, index) => { + const editorKey = createEditorKey(record); + const editorId = editorKey.replace(/[^A-Za-z]/g, ""); return ( - - - {availableSizes.map((localSize) => ( - - undefined} - onCancel={() => undefined} - // Use when merging #576 size={localSize === "small" ? undefined : localSize} - /> - - ))} + + + + + + + + + - - {key} - {record.property.editor && ( - [ - - Editor config: - - {JSON.stringify(record.property.editor, undefined, 2)} - - , - ]} - > - - - - - )} - - + + + {editorKey} + {record.property.editor && ( + [ + + Editor config: + + {JSON.stringify(record.property.editor, undefined, 2)} + + , + ]} + > + + + + + )} + + + ); })} ); } + +function OldEditorRenderer({ record }: { record: PropertyRecord }) { + return ( + + {availableSizes.map((localSize) => ( + + undefined} + onCancel={() => undefined} + // Use when merging #576 size={localSize === "small" ? undefined : localSize} + /> + + ))} + + ); +} + +function NewEditorRenderer({ record }: { record: PropertyRecord }) { + return ( + + {availableSizes.map((localSize) => ( + + undefined} + onCancel={() => undefined} + editorSystem="new" + size="small" // size={localSize} + /> + + ))} + + ); +} + +function createEditorKey(record: PropertyRecord) { + return `${PropertyValueFormat[record.value.valueFormat]}:${ + record.property.typename + }:${record.property.editor?.name ?? "Default"}[${ + record.property.editor?.params?.map((p) => p.type).join(",") ?? "" + }]`.replace("[]", ""); +} diff --git a/common/api/components-react.api.md b/common/api/components-react.api.md index 5e17f76b200..b9c4d3f5ddd 100644 --- a/common/api/components-react.api.md +++ b/common/api/components-react.api.md @@ -190,6 +190,12 @@ export class BooleanTypeConverter extends TypeConverter { sortCompare(a: Primitives.Boolean, b: Primitives.Boolean, _ignoreCase?: boolean): number; } +// @beta +export interface BooleanValue { + // (undocumented) + value: boolean; +} + // @beta export interface BuildFilterOptions { ignoreErrors?: boolean; @@ -358,6 +364,13 @@ export namespace ConvertedPrimitives { export type Value = boolean | number | string | Date | Point | Id64String; } +// @beta +export function createEditorSpec({ Editor, isMetadataSupported, isValueSupported, }: { + isMetadataSupported: (metadata: ValueMetadata) => metadata is TMetadata; + isValueSupported: (value: Value) => value is TValue; + Editor: React.ComponentType>; +}): EditorSpec; + // @public export function createMergedPropertyDataProvider(providers: IPropertyDataProvider[]): IMergingPropertyDataProvider; @@ -492,6 +505,12 @@ export abstract class DateTimeTypeConverterBase extends TypeConverter implements sortCompare(valueA: Date, valueB: Date, _ignoreCase?: boolean): number; } +// @beta +export interface DateValue { + // (undocumented) + value: Date; +} + // @public export const DEFAULT_LINKS_HANDLER: LinkElementsInfo; @@ -557,6 +576,39 @@ export interface EditorContainerProps extends CommonProps { title?: string; } +// @beta +export interface EditorProps { + cancel?: () => void; + commit?: () => void; + // (undocumented) + disabled?: boolean; + // (undocumented) + metadata: TMetadata; + // (undocumented) + onChange: (value?: TValue) => void; + // (undocumented) + size?: "small" | "large"; + // (undocumented) + value?: TValue; +} + +// @beta +export function EditorRenderer(props: EditorProps): React_3.JSX.Element | null; + +// @beta +export interface EditorSpec { + // (undocumented) + applies: (metaData: ValueMetadata, value: Value | undefined) => boolean; + // (undocumented) + Editor: React.ComponentType; +} + +// @beta +export function EditorsRegistryProvider({ children, editors, }: { + children: React_3.ReactNode; + editors: EditorSpec[]; +}): React_3.JSX.Element; + // @public export class EnumButtonGroupEditor extends React_3.Component implements TypeEditor { // (undocumented) @@ -575,6 +627,14 @@ export class EnumButtonGroupEditor extends React_3.Component; } +// @beta +export interface EnumChoice { + // (undocumented) + label: string; + // (undocumented) + value: number | string; +} + // @public export class EnumEditor extends React_3.PureComponent implements TypeEditor { // (undocumented) @@ -619,6 +679,22 @@ export class EnumTypeConverter extends TypeConverter { sortCompare(a: Primitives.Enum, b: Primitives.Enum, ignoreCase?: boolean): number; } +// @beta +export interface EnumValue { + // (undocumented) + choice: number | string; +} + +// @beta +export interface EnumValueMetadata extends ValueMetadata { + // (undocumented) + choices: EnumChoice[]; + // (undocumented) + isStrict: boolean; + // (undocumented) + type: "enum"; +} + // @public export interface ErrorObserver { // (undocumented) @@ -761,6 +837,9 @@ export class FloatTypeConverter extends NumericTypeConverterBase { convertToString(value?: Primitives.Float): string; } +// @beta +export function FormattedNumericInput({ onChange, value, parseValue, formatValue, disabled, size, }: FormattedNumericInputProps): React_3.JSX.Element; + // @public export function from(iterable: Iterable | PromiseLike): Observable; @@ -1023,6 +1102,17 @@ export interface IMutablePropertyGridModel { getVisibleFlatGrid: () => IMutableFlatGridItem[]; } +// @beta +export interface InstanceKeyValue { + // (undocumented) + key: { + id: Id64String; + className: string; + }; + // (undocumented) + label: string; +} + // @alpha @deprecated export class IntlFormatter implements DateFormatter { constructor(_intlFormatter?: Intl.DateTimeFormat | undefined); @@ -1568,6 +1658,16 @@ export abstract class NumericTypeConverterBase extends TypeConverter implements sortCompare(a: Primitives.Numeric, b: Primitives.Numeric, _ignoreCase?: boolean): number; } +// @beta +export interface NumericValue { + // (undocumented) + displayValue: string; + // (undocumented) + rawValue: number | undefined; + // (undocumented) + roundingError?: number; +} + // @public export interface Observable extends Subscribable { } @@ -2172,6 +2272,9 @@ export abstract class PropertyRecordDataFiltererBase extends PropertyDataFiltere categoryMatchesFilter(): Promise; } +// @beta +export function PropertyRecordEditor({ propertyRecord, onCommit, onCancel, onClick, setFocus, size, editorSystem, }: PropertyRecordEditorProps): React_3.JSX.Element; + // @public export const PropertyRenderer: { (props: PropertyRendererProps): React_3.JSX.Element; @@ -2607,6 +2710,12 @@ export class TextEditor extends React_3.PureComponent; } +// @beta +export interface TextValue { + // (undocumented) + value: string; +} + // @public export type TimeFormat = TimeFormat_2; @@ -3155,6 +3264,15 @@ export class UrlPropertyValueRenderer implements IPropertyValueRenderer { // @public export function useAsyncValue(value: T | PromiseLike): T | undefined; +// @beta +export function useCommittableValue({ initialValue, onCancel, onCommit, }: UseCommittableValueProps): { + onChange: (newValue?: Value) => void; + onKeydown: (e: React_3.KeyboardEvent) => void; + commit: () => void; + cancel: () => void; + value: Value | undefined; +}; + // @public export function useControlledTreeEventsHandler(factoryOrParams: (() => TEventsHandler) | TreeEventHandlerParams): TreeEventHandler | undefined; @@ -3249,6 +3367,32 @@ export function useVirtualizedPropertyGridLayoutStorage(): { restore: () => void; }; +// @beta +export type Value = NumericValue | InstanceKeyValue | TextValue | BooleanValue | DateValue | EnumValue; + +// @beta (undocumented) +export namespace Value { + export function isBoolean(value: Value): value is BooleanValue; + export function isDate(value: Value): value is DateValue; + export function isEnum(value: Value): value is EnumValue; + export function isInstanceKey(value: Value): value is InstanceKeyValue; + export function isNumeric(value: Value): value is NumericValue; + export function isText(value: Value): value is TextValue; +} + +// @beta +export interface ValueMetadata { + // (undocumented) + isNullable?: boolean; + // (undocumented) + preferredEditor?: string; + // (undocumented) + type: ValueType; +} + +// @beta +export type ValueType = "string" | "number" | "bool" | "date" | "dateTime" | "enum" | "instanceKey"; + // @public export class VirtualizedPropertyGrid extends React_3.Component { constructor(props: VirtualizedPropertyGridProps); @@ -3275,6 +3419,8 @@ export interface VirtualizedPropertyGridContext { // (undocumented) editingPropertyKey?: string; // (undocumented) + editorSystem: "legacy" | "new"; + // (undocumented) eventHandler: IPropertyGridEventHandler; // (undocumented) gridWidth: number; @@ -3321,6 +3467,8 @@ export interface VirtualizedPropertyGridContext { // @public export interface VirtualizedPropertyGridProps extends CommonPropertyGridProps { dataProvider: IPropertyDataProvider; + // @beta + editorSystem?: "legacy" | "new"; eventHandler: IPropertyGridEventHandler; height: number; highlight?: PropertyGridContentHighlightProps; @@ -3335,6 +3483,8 @@ export function VirtualizedPropertyGridWithDataProvider(props: VirtualizedProper // @public @deprecated export interface VirtualizedPropertyGridWithDataProviderProps extends CommonPropertyGridProps { dataProvider: IPropertyDataProvider; + // @beta + editorSystem?: "legacy" | "new"; height: number; highlight?: PropertyGridContentHighlightProps; propertyCategoryRendererManager?: PropertyCategoryRendererManager; diff --git a/common/api/imodel-components-react.api.md b/common/api/imodel-components-react.api.md index f0fb75dd575..b1c76d21bd7 100644 --- a/common/api/imodel-components-react.api.md +++ b/common/api/imodel-components-react.api.md @@ -8,6 +8,7 @@ import { Cartographic } from '@itwin/core-common'; import { ColorDef } from '@itwin/core-common'; import type { CommonProps } from '@itwin/core-react'; import type { DateFormatOptions } from '@itwin/components-react'; +import { EditorSpec } from '@itwin/components-react'; import type { FormatProps } from '@itwin/core-quantity'; import { FormatterSpec } from '@itwin/core-quantity'; import { HSVColor } from '@itwin/core-common'; @@ -548,6 +549,9 @@ export interface PlaybackSettings { // @public export type PlaybackSettingsChangeHandler = (settingsChange: PlaybackSettings) => void; +// @beta +export const QuantityEditorSpec: EditorSpec; + // @alpha export function QuantityFormatPanel(props: QuantityFormatPanelProps): React_2.JSX.Element; @@ -892,6 +896,9 @@ export class WeightEditor extends React_2.PureComponent; } +// @beta +export const WeightEditorSpec: EditorSpec; + // @public export class WeightPickerButton extends React_2.PureComponent { constructor(props: WeightPickerProps); diff --git a/common/api/summary/components-react.exports.csv b/common/api/summary/components-react.exports.csv index 71268b99dc9..48723ce4db3 100644 --- a/common/api/summary/components-react.exports.csv +++ b/common/api/summary/components-react.exports.csv @@ -16,6 +16,7 @@ public;class;BasicPropertyEditor public;class;BooleanEditor public;class;BooleanPropertyEditor public;class;BooleanTypeConverter +beta;interface;BooleanValue beta;interface;BuildFilterOptions public;interface;CategorizedPropertyItem public;type;CategorizedPropertyTypes @@ -36,6 +37,7 @@ deprecated;interface;ControlledSelectableContentProps public;function;ControlledTree public;interface;ControlledTreeProps public;namespace;ConvertedPrimitives +beta;function;createEditorSpec public;function;createMergedPropertyDataProvider public;function;CustomizablePropertyRenderer alpha;class;CustomNumberEditor @@ -55,6 +57,7 @@ alpha;interface;DatePickerProps deprecated;interface;DatePickerProps public;class;DateTimeTypeConverter public;class;DateTimeTypeConverterBase +beta;interface;DateValue public;const;DEFAULT_LINKS_HANDLER beta;function;defaultPropertyFilterBuilderRuleValidator deprecated;function;defaultPropertyFilterBuilderRuleValidator @@ -67,11 +70,18 @@ public;interface;EditableTreeDataProvider public;function;EditorContainer public;interface;EditorContainerProps deprecated;interface;EditorContainerProps +beta;interface;EditorProps +beta;function;EditorRenderer +beta;interface;EditorSpec +beta;function;EditorsRegistryProvider public;class;EnumButtonGroupEditor +beta;interface;EnumChoice public;class;EnumEditor public;class;EnumPropertyButtonGroupEditor public;class;EnumPropertyEditor public;class;EnumTypeConverter +beta;interface;EnumValue +beta;interface;EnumValueMetadata public;interface;ErrorObserver public;class;FavoritePropertiesRenderer alpha;function;FavoritePropertyList @@ -87,6 +97,7 @@ public;type;FlatGridItem public;interface;FlatGridItemBase public;enum;FlatGridItemType public;class;FloatTypeConverter +beta;function;FormattedNumericInput public;function;from beta;function;getPropertyFilterBuilderOperators public;function;getVisibleDescendants @@ -117,6 +128,7 @@ public;interface;IMutableFlatPropertyGridItem public;interface;IMutableGridCategoryItem public;interface;IMutableGridItemFactory public;interface;IMutablePropertyGridModel +beta;interface;InstanceKeyValue alpha;class;IntlFormatter deprecated;class;IntlFormatter public;class;IntTypeConverter @@ -179,6 +191,7 @@ public;interface;NullableOperatorProcessor public;class;NumericInputEditor public;class;NumericInputPropertyEditor public;class;NumericTypeConverterBase +beta;interface;NumericValue public;interface;Observable public;type;Observer public;type;OnItemsDeselectedCallback @@ -264,6 +277,7 @@ public;class;PropertyList public;interface;PropertyListProps public;interface;PropertyPopupState public;class;PropertyRecordDataFiltererBase +beta;function;PropertyRecordEditor public;const;PropertyRenderer public;interface;PropertyRendererProps public;interface;PropertyUpdatedArgs @@ -304,6 +318,7 @@ public;class;TableStructValueRenderer public;class;TextareaEditor public;class;TextareaPropertyEditor public;class;TextEditor +beta;interface;TextValue public;type;TimeFormat public;const;TimeFormat public;const;toDateString @@ -378,6 +393,7 @@ public;class;UiComponents public;interface;Unsubscribable public;class;UrlPropertyValueRenderer public;function;useAsyncValue +beta;function;useCommittableValue public;function;useControlledTreeEventsHandler public;function;useControlledTreeLayoutStorage beta;function;useDebouncedAsyncValue @@ -401,6 +417,10 @@ public;function;useTreeModel public;function;useTreeModelSource public;function;useTreeNodeLoader public;function;useVirtualizedPropertyGridLayoutStorage +beta;type;Value +beta;namespace;Value +beta;interface;ValueMetadata +beta;type;ValueType public;class;VirtualizedPropertyGrid public;interface;VirtualizedPropertyGridContext public;interface;VirtualizedPropertyGridProps diff --git a/common/api/summary/imodel-components-react.exports.csv b/common/api/summary/imodel-components-react.exports.csv index 7a58a0dbef4..9dbc6452e7d 100644 --- a/common/api/summary/imodel-components-react.exports.csv +++ b/common/api/summary/imodel-components-react.exports.csv @@ -72,6 +72,7 @@ alpha;interface;MiscFormatOptionsProps deprecated;interface;MiscFormatOptionsProps public;interface;PlaybackSettings public;type;PlaybackSettingsChangeHandler +beta;const;QuantityEditorSpec alpha;function;QuantityFormatPanel alpha;interface;QuantityFormatPanelProps deprecated;interface;QuantityFormatPanelProps @@ -120,6 +121,7 @@ public;interface;ViewRotationChangeEventArgs deprecated;interface;ViewRotationChangeEventArgs public;type;ViewStateProp public;class;WeightEditor +beta;const;WeightEditorSpec public;class;WeightPickerButton public;interface;WeightPickerProps public;class;WeightPropertyEditor \ No newline at end of file diff --git a/common/changes/@itwin/components-react/editors-new-system_2025-02-28-11-40.json b/common/changes/@itwin/components-react/editors-new-system_2025-02-28-11-40.json new file mode 100644 index 00000000000..28d56e32144 --- /dev/null +++ b/common/changes/@itwin/components-react/editors-new-system_2025-02-28-11-40.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/components-react", + "comment": "Added new system for rendering property value editor components.", + "type": "none" + } + ], + "packageName": "@itwin/components-react" +} \ No newline at end of file diff --git a/common/changes/@itwin/imodel-components-react/editors-new-system_2025-02-28-11-40.json b/common/changes/@itwin/imodel-components-react/editors-new-system_2025-02-28-11-40.json new file mode 100644 index 00000000000..49632d72056 --- /dev/null +++ b/common/changes/@itwin/imodel-components-react/editors-new-system_2025-02-28-11-40.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/imodel-components-react", + "comment": "Added Quantity and Weight property editor specifications for the new editors system.", + "type": "none" + } + ], + "packageName": "@itwin/imodel-components-react" +} \ No newline at end of file diff --git a/common/config/rush/browser-approved-packages.json b/common/config/rush/browser-approved-packages.json index 911a6c07f64..b3f3904b75e 100644 --- a/common/config/rush/browser-approved-packages.json +++ b/common/config/rush/browser-approved-packages.json @@ -174,10 +174,6 @@ "name": "@itwin/itwinui-react", "allowedCategories": [ "frontend", "internal" ] }, - { - "name": "@itwin/presentation-common", - "allowedCategories": [ "internal" ] - }, { "name": "@itwin/reality-data-client", "allowedCategories": [ "internal" ] diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 5b797ff0f0a..df018d33d36 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -126,9 +126,6 @@ importers: '@itwin/itwinui-react-v5': specifier: npm:@itwin/itwinui-react@5.0.0-alpha.5 version: '@itwin/itwinui-react@5.0.0-alpha.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' - '@itwin/presentation-common': - specifier: ^4.0.0 - version: 4.4.0(@itwin/core-bentley@4.4.0)(@itwin/core-common@4.4.0(@itwin/core-bentley@4.4.0)(@itwin/core-geometry@4.4.0))(@itwin/core-quantity@4.4.0(@itwin/core-bentley@4.4.0))(@itwin/ecschema-metadata@4.4.0(@itwin/core-bentley@4.4.0)(@itwin/core-quantity@4.4.0(@itwin/core-bentley@4.4.0))) '@itwin/reality-data-client': specifier: 1.2.2 version: 1.2.2(@itwin/core-bentley@4.4.0) @@ -2629,14 +2626,6 @@ packages: reflect-metadata: optional: true - '@itwin/presentation-common@4.4.0': - resolution: {integrity: sha512-k4cZQyMc3uTJ5BvGa2a7qT7tZiS09jKoKGQctzDpDnuMueLcW2+TG7nogKIKl+ded4tKT0zsCBsKXLmAXTQu6Q==} - peerDependencies: - '@itwin/core-bentley': ^4.4.0 - '@itwin/core-common': ^4.4.0 - '@itwin/core-quantity': ^4.4.0 - '@itwin/ecschema-metadata': ^4.4.0 - '@itwin/reality-data-client@1.2.2': resolution: {integrity: sha512-Zbm6ooV4auni6yWNaIxOgd+/9H9TIeDH7kIAivepuFFLnJq2atzAj54SvsNfmNxdH6FDftC5U1NFMjnprwo89Q==} peerDependencies: @@ -9820,13 +9809,6 @@ snapshots: transitivePeerDependencies: - debug - '@itwin/presentation-common@4.4.0(@itwin/core-bentley@4.4.0)(@itwin/core-common@4.4.0(@itwin/core-bentley@4.4.0)(@itwin/core-geometry@4.4.0))(@itwin/core-quantity@4.4.0(@itwin/core-bentley@4.4.0))(@itwin/ecschema-metadata@4.4.0(@itwin/core-bentley@4.4.0)(@itwin/core-quantity@4.4.0(@itwin/core-bentley@4.4.0)))': - dependencies: - '@itwin/core-bentley': 4.4.0 - '@itwin/core-common': 4.4.0(@itwin/core-bentley@4.4.0)(@itwin/core-geometry@4.4.0) - '@itwin/core-quantity': 4.4.0(@itwin/core-bentley@4.4.0) - '@itwin/ecschema-metadata': 4.4.0(@itwin/core-bentley@4.4.0)(@itwin/core-quantity@4.4.0(@itwin/core-bentley@4.4.0)) - '@itwin/reality-data-client@1.2.2(@itwin/core-bentley@4.4.0)': dependencies: '@itwin/core-bentley': 4.4.0 diff --git a/docs/changehistory/NextVersion.md b/docs/changehistory/NextVersion.md index 5ec6e734bda..7a553f85f11 100644 --- a/docs/changehistory/NextVersion.md +++ b/docs/changehistory/NextVersion.md @@ -1 +1,244 @@ # NextVersion + +Table of contents: + +- [@itwin/components-react](#itwincomponents-react) + - [Additions](#additions) +- [@itwin/imodel-components-react](#itwinimodel-components-react) + - [Additions](#additions-1) + +## @itwin/components-react + +### Additions + +- Added new system for rendering property value editing components. [#1166](https://github.com/iTwin/appui/pull/1166) + + #### API overview + + - `EditorsRegistryProvider` - adds supplied editors to the registry held in `React` context. It supports nesting multiple `EditorsRegistryProvider` to allow registering custom editors specific for some component that have higher priority than the ones registered near the root of the application. + - `useEditor` - hook to get the editor that should be used to edit the supplied value. First it looks for applicable editor in EditorsRegistry and if none was found it fallbacks to the default editors. + - `EditorRenderer` - wrapper around EditorRegistry that provides a convenient way to render editors for specific value. + - `useCommittableValue` - custom React hooks that provides a convenient way to add `cancel/commit` actions on `Esc`/`Enter` key click to the editor. + - `Value` - type for all values that are supported by editors. + - `ValueMetadata` - type for additional metadata that can be supplied to editors alongside value itself. It can be extended when implementing custom editors. (E.g. passing available choices and icons to the enum editor that is rendered as button group) + - `createEditorSpec` - an utility function to that provides a convenient way to defined editor spec for typed editors. + - `PropertyRecordEditor` - React component that allows to use existing `PropertyRecord` type with the new editors system. + + #### Rendering editor and registering custom editors + + Defining component that renders value editor: + + ```tsx + function EditingComponent(props: { value: Value; metadata: ValueMetadata }) { + const onChange = (newValue: Value) => { + // handle value change + }; + return ( + + ); + } + ``` + + Defining component that renders value editor with `Commit`/`Cancel` actions: + + ```tsx + function EditingComponent(props: { + initialValue: Value; + metadata: ValueMetadata; + onCommit: (committedValue: Value) => void; + onCancel: () => void; + }) { + // `onKeydown` callback returned by `useCommittableValue` will invoke `onCommit` with current value when `ENTER` is pressed or + // `onCancel` when `ESC` key is pressed. + // Additionally `commit` or `cancel` callback can be invoked to do commit or cancel manually. + const { value, onChange, onKeydown, commit, cancel } = useCommittableValue({ + initialValue, + onCommit, + onCancel, + }); + + return ( + // cancel edit when editor is blurred + + + + ); + } + ``` + + Registering custom editors to be available through out all application: + + ```tsx + import { + CustomBooleanEditorSpec, + CustomNumericEditorSpec, + } from "./customEditors"; + + const rootEditors: EditorSpec[] = [ + CustomBooleanEditorSpec, + CustomNumericEditorSpec, + ]; + + function App() { + return ( + + {/* Render whole application components tree. Components down the tree will be able to use custom editors */} + + ); + } + ``` + + Registering custom editors that should be available only for specific component: + + ```tsx + // setup custom editors for whole application + import { + CustomBooleanEditorSpec, + CustomNumericEditorSpec, + } from "./customEditors"; + + const rootEditors: EditorSpec[] = [ + CustomBooleanEditorSpec, + CustomNumericEditorSpec, + ]; + + function App() { + return ( + + + + ); + } + ``` + + ```tsx + // setup custom editors for specific component + import { SpecialNumericEditorSpec } from "./specialEditors"; + + const customEditors: EditorSpec[] = [SpecialNumericEditorSpec]; + + function EditingComponent(props: EditingComponentProps) { + return ( + + {/* SpecialNumericEditorSpec has higher priority than CustomBooleanEditorSpec and CustomNumericEditorSpec registered at the root of application */} + + + ); + } + ``` + + #### Defining custom editors + + The goal of the new editors system is to remove the need for static editor registration and provide more convenient API for implementing custom editors. Current API has quite a lot optional properties that do not make sense (`propertyRecord` is optional but if it is `undefined` there is no way to figure out what to render): + + ##### Custom editor using old system and react functional components: + + ```tsx + const CustomBooleanEditor = React.forwardRef( + (props, ref) => { + const inputRef = React.useRef(null); + const getCurrentValue = () => { + if ( + props.propertyRecord && + props.propertyRecord.value.valueFormat === + PropertyValueFormat.Primitive + ) { + return props.propertyRecord.value.value as boolean; + } + return false; + }; + const currentValue = getCurrentValue(); + + React.useImperativeHandle( + ref, + () => ({ + getPropertyValue: async () => { + let propertyValue: PropertyValue | undefined; + if ( + props.propertyRecord && + props.propertyRecord.value.valueFormat === + PropertyValueFormat.Primitive + ) { + propertyValue = { + valueFormat: PropertyValueFormat.Primitive, + value: currentValue, + displayValue: "", + }; + } + return propertyValue; + }, + htmlElement: inputRef.current, + hasFocus: document.activeElement === inputRef.current, + }), + [currentValue, props.propertyRecord] + ); + + return ( + { + if (!props.propertyRecord || !props.onCommit) return; + props.onCommit({ + propertyRecord: props.propertyRecord, + newValue: { + valueFormat: PropertyValueFormat.Primitive, + value: e.target.checked, + displayValue: "", + }, + }); + }} + /> + ); + } + ); + + export class CustomBooleanPropertyEditor extends PropertyEditorBase { + public get reactNode(): React.ReactNode { + return ; + } + } + ``` + + ##### Custom boolean editor using new system: + + ```tsx + export const CustomBoolEditorSpec: EditorSpec = createEditorSpec({ + isMetadataSupported: (metadata): metadata is ValueMetadata => + metadata.type === "bool", + isValueSupported: Value.isBoolean, + Editor: CustomBooleanEditor, + }); + + export function CustomBooleanEditor( + props: EditorProps + ) { + const currentValue = props.value?.value ?? false; + const handleChange = (e: React.ChangeEvent) => { + const newValue = { value: e.target.checked }; + props.onChange(newValue); + props.commit && props.commit(); + }; + + return ; + } + ``` + + The new system removes all the code that was associated with class components and accessing values through editor `ref`. It is not clear if that was used/useful so the chosen approach is to add something similar later if that is still needed. Majority of that was used by `EditorContainer` that is replaced by `useCommittableValue` hook in the new system. + +## @itwin/imodel-components-react + +### Additions + +- Added Quantity and Weight property value editor specification using new editors system. [#1166](https://github.com/iTwin/appui/pull/1166) diff --git a/e2e-tests/tests/editors/editors.test.ts b/e2e-tests/tests/editors/editors.test.ts index 7ace638e53f..6c94f806c56 100644 --- a/e2e-tests/tests/editors/editors.test.ts +++ b/e2e-tests/tests/editors/editors.test.ts @@ -6,7 +6,7 @@ import { expect, test } from "@playwright/test"; import { openComponentExamples } from "../Utils"; function editorId(id: string) { - return `#${id.replace(/[^A-Za-z]/g, "")}`; + return `${id.replace(/[^A-Za-z]/g, "")}`; } const testIds = [ @@ -28,15 +28,21 @@ const testIds = [ "Primitive:enum:enum-buttongroup ", "Primitive:enum:enum-buttongroup[UiAbstract-ButtonGroupData] ", ]; -for (const id of testIds) { - test(`Editor ${id} default visual`, async ({ page, baseURL }) => { - await openComponentExamples(page, baseURL); - // Avoid highlighting one of the editors. - await page.keyboard.press("Escape"); - await page.getByRole("button", { name: "Editor", exact: true }).click(); +["Legacy", "New"].forEach((editorSystem) => { + for (const id of testIds) { + test(`Editor ${id} default visual in ${editorSystem} system`, async ({ + page, + baseURL, + }) => { + await openComponentExamples(page, baseURL); - const editors = page.locator(editorId(id)).first(); - await expect(editors).toHaveScreenshot(); - }); -} + // Avoid highlighting one of the editors. + await page.keyboard.press("Escape"); + await page.getByRole("button", { name: "Editor", exact: true }).click(); + + const editors = page.locator(`#${editorSystem}${editorId(id)}`).first(); + await expect(editors).toHaveScreenshot(); + }); + } +}); diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-Default-default-visual-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-Default-default-visual-1-chromium-linux.png deleted file mode 100644 index efe0ca5eb0f..00000000000 Binary files a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-Default-default-visual-1-chromium-linux.png and /dev/null differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-Default-default-visual-in-Legacy-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-Default-default-visual-in-Legacy-system-1-chromium-linux.png new file mode 100644 index 00000000000..d42ca0f0ae0 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-Default-default-visual-in-Legacy-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-Default-default-visual-in-New-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-Default-default-visual-in-New-system-1-chromium-linux.png new file mode 100644 index 00000000000..dfb24ff2a60 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-Default-default-visual-in-New-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-image-check-box-UiAbs-bdd20-ckBoxImages-default-visual-in-Legacy-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-image-check-box-UiAbs-bdd20-ckBoxImages-default-visual-in-Legacy-system-1-chromium-linux.png new file mode 100644 index 00000000000..4e05b434e5a Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-image-check-box-UiAbs-bdd20-ckBoxImages-default-visual-in-Legacy-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-image-check-box-UiAbstract-CheckBoxImages-default-visual-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-image-check-box-UiAbstract-CheckBoxImages-default-visual-1-chromium-linux.png deleted file mode 100644 index 6efd5c5ade4..00000000000 Binary files a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-image-check-box-UiAbstract-CheckBoxImages-default-visual-1-chromium-linux.png and /dev/null differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-image-check-box-UiAbstract-CheckBoxImages-default-visual-in-New-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-image-check-box-UiAbstract-CheckBoxImages-default-visual-in-New-system-1-chromium-linux.png new file mode 100644 index 00000000000..04ad26a931e Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-image-check-box-UiAbstract-CheckBoxImages-default-visual-in-New-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-toggle-default-visual-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-toggle-default-visual-1-chromium-linux.png deleted file mode 100644 index db75e0afb8a..00000000000 Binary files a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-toggle-default-visual-1-chromium-linux.png and /dev/null differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-toggle-default-visual-in-Legacy-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-toggle-default-visual-in-Legacy-system-1-chromium-linux.png new file mode 100644 index 00000000000..e2a40343898 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-toggle-default-visual-in-Legacy-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-toggle-default-visual-in-New-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-toggle-default-visual-in-New-system-1-chromium-linux.png new file mode 100644 index 00000000000..e07cfe1ca06 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-boolean-toggle-default-visual-in-New-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-dateTime-Default-default-visual-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-dateTime-Default-default-visual-1-chromium-linux.png deleted file mode 100644 index f4014086c21..00000000000 Binary files a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-dateTime-Default-default-visual-1-chromium-linux.png and /dev/null differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-dateTime-Default-default-visual-in-Legacy-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-dateTime-Default-default-visual-in-Legacy-system-1-chromium-linux.png new file mode 100644 index 00000000000..d8c1d1df885 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-dateTime-Default-default-visual-in-Legacy-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-dateTime-Default-default-visual-in-New-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-dateTime-Default-default-visual-in-New-system-1-chromium-linux.png new file mode 100644 index 00000000000..b18cf9263c8 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-dateTime-Default-default-visual-in-New-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-Default-default-visual-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-Default-default-visual-1-chromium-linux.png deleted file mode 100644 index a7a6f1a6c83..00000000000 Binary files a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-Default-default-visual-1-chromium-linux.png and /dev/null differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-Default-default-visual-in-Legacy-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-Default-default-visual-in-Legacy-system-1-chromium-linux.png new file mode 100644 index 00000000000..4dbeacd3e96 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-Default-default-visual-in-Legacy-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-Default-default-visual-in-New-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-Default-default-visual-in-New-system-1-chromium-linux.png new file mode 100644 index 00000000000..4dbeacd3e96 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-Default-default-visual-in-New-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-enum-buttongroup-UiAbstr-b48ef-onGroupData-default-visual-in-Legacy-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-enum-buttongroup-UiAbstr-b48ef-onGroupData-default-visual-in-Legacy-system-1-chromium-linux.png new file mode 100644 index 00000000000..fb575a5500f Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-enum-buttongroup-UiAbstr-b48ef-onGroupData-default-visual-in-Legacy-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-enum-buttongroup-UiAbstract-ButtonGroupData-default-visual-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-enum-buttongroup-UiAbstract-ButtonGroupData-default-visual-1-chromium-linux.png deleted file mode 100644 index 3129119305b..00000000000 Binary files a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-enum-buttongroup-UiAbstract-ButtonGroupData-default-visual-1-chromium-linux.png and /dev/null differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-enum-buttongroup-UiAbstract-ButtonGroupData-default-visual-in-New-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-enum-buttongroup-UiAbstract-ButtonGroupData-default-visual-in-New-system-1-chromium-linux.png new file mode 100644 index 00000000000..3796d0e7acb Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-enum-buttongroup-UiAbstract-ButtonGroupData-default-visual-in-New-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-enum-buttongroup-default-visual-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-enum-buttongroup-default-visual-1-chromium-linux.png deleted file mode 100644 index 406d4b84093..00000000000 Binary files a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-enum-buttongroup-default-visual-1-chromium-linux.png and /dev/null differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-enum-buttongroup-default-visual-in-Legacy-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-enum-buttongroup-default-visual-in-Legacy-system-1-chromium-linux.png new file mode 100644 index 00000000000..3047533659f Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-enum-buttongroup-default-visual-in-Legacy-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-enum-buttongroup-default-visual-in-New-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-enum-buttongroup-default-visual-in-New-system-1-chromium-linux.png new file mode 100644 index 00000000000..51d58d7549b Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-enum-enum-buttongroup-default-visual-in-New-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-number-custom-UiAbstra-00bc1-stract-Icon-default-visual-in-Legacy-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-number-custom-UiAbstra-00bc1-stract-Icon-default-visual-in-Legacy-system-1-chromium-linux.png new file mode 100644 index 00000000000..f11e4869bc6 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-number-custom-UiAbstra-00bc1-stract-Icon-default-visual-in-Legacy-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-number-custom-UiAbstra-38c0b-iAbstract-Icon-default-visual-in-New-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-number-custom-UiAbstra-38c0b-iAbstract-Icon-default-visual-in-New-system-1-chromium-linux.png new file mode 100644 index 00000000000..fdd21589810 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-number-custom-UiAbstra-38c0b-iAbstract-Icon-default-visual-in-New-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-number-custom-UiAbstra-5f633-ormattedNumber-default-visual-in-New-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-number-custom-UiAbstra-5f633-ormattedNumber-default-visual-in-New-system-1-chromium-linux.png new file mode 100644 index 00000000000..f858ffc5730 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-number-custom-UiAbstra-5f633-ormattedNumber-default-visual-in-New-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-number-custom-UiAbstra-8f03e-attedNumber-default-visual-in-Legacy-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-number-custom-UiAbstra-8f03e-attedNumber-default-visual-in-Legacy-system-1-chromium-linux.png new file mode 100644 index 00000000000..4030e11da22 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-number-custom-UiAbstra-8f03e-attedNumber-default-visual-in-Legacy-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-number-custom-UiAbstra-caae8-mattedNumber-UiAbstract-Icon-default-visual-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-number-custom-UiAbstra-caae8-mattedNumber-UiAbstract-Icon-default-visual-1-chromium-linux.png deleted file mode 100644 index e1317e8a6e1..00000000000 Binary files a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-number-custom-UiAbstra-caae8-mattedNumber-UiAbstract-Icon-default-visual-1-chromium-linux.png and /dev/null differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-number-custom-UiAbstract-CustomFormattedNumber-default-visual-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-number-custom-UiAbstract-CustomFormattedNumber-default-visual-1-chromium-linux.png deleted file mode 100644 index 7c6e9fbc187..00000000000 Binary files a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-number-custom-UiAbstract-CustomFormattedNumber-default-visual-1-chromium-linux.png and /dev/null differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstra-4e042-tract-Range-default-visual-in-Legacy-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstra-4e042-tract-Range-default-visual-in-Legacy-system-1-chromium-linux.png new file mode 100644 index 00000000000..c9982c72f53 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstra-4e042-tract-Range-default-visual-in-Legacy-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstra-e027f-Abstract-Range-default-visual-in-New-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstra-e027f-Abstract-Range-default-visual-in-New-system-1-chromium-linux.png new file mode 100644 index 00000000000..db950ee48c0 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstra-e027f-Abstract-Range-default-visual-in-New-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstract-InputEditorSize-UiAbstract-Range-default-visual-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstract-InputEditorSize-UiAbstract-Range-default-visual-1-chromium-linux.png deleted file mode 100644 index 53464414008..00000000000 Binary files a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstract-InputEditorSize-UiAbstract-Range-default-visual-1-chromium-linux.png and /dev/null differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstract-InputEditorSize-default-visual-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstract-InputEditorSize-default-visual-1-chromium-linux.png deleted file mode 100644 index 9fee9b97e2e..00000000000 Binary files a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstract-InputEditorSize-default-visual-1-chromium-linux.png and /dev/null differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstract-InputEditorSize-default-visual-in-Legacy-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstract-InputEditorSize-default-visual-in-Legacy-system-1-chromium-linux.png new file mode 100644 index 00000000000..3db7b54af33 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstract-InputEditorSize-default-visual-in-Legacy-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstract-InputEditorSize-default-visual-in-New-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstract-InputEditorSize-default-visual-in-New-system-1-chromium-linux.png new file mode 100644 index 00000000000..21e303ac0f3 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstract-InputEditorSize-default-visual-in-New-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstract-Range-default-visual-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstract-Range-default-visual-1-chromium-linux.png deleted file mode 100644 index c737cf89c70..00000000000 Binary files a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstract-Range-default-visual-1-chromium-linux.png and /dev/null differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstract-Range-default-visual-in-Legacy-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstract-Range-default-visual-in-Legacy-system-1-chromium-linux.png new file mode 100644 index 00000000000..b1a5d009eee Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstract-Range-default-visual-in-Legacy-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstract-Range-default-visual-in-New-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstract-Range-default-visual-in-New-system-1-chromium-linux.png new file mode 100644 index 00000000000..df92300a31d Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-UiAbstract-Range-default-visual-in-New-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-default-visual-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-default-visual-1-chromium-linux.png deleted file mode 100644 index c31b44e57c3..00000000000 Binary files a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-default-visual-1-chromium-linux.png and /dev/null differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-default-visual-in-Legacy-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-default-visual-in-Legacy-system-1-chromium-linux.png new file mode 100644 index 00000000000..3db7b54af33 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-default-visual-in-Legacy-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-default-visual-in-New-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-default-visual-in-New-system-1-chromium-linux.png new file mode 100644 index 00000000000..21e303ac0f3 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-numeric-input-default-visual-in-New-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-slider-UiAbstract-Slider-default-visual-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-slider-UiAbstract-Slider-default-visual-1-chromium-linux.png deleted file mode 100644 index 2038cd9c00d..00000000000 Binary files a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-slider-UiAbstract-Slider-default-visual-1-chromium-linux.png and /dev/null differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-slider-UiAbstract-Slider-default-visual-in-Legacy-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-slider-UiAbstract-Slider-default-visual-in-Legacy-system-1-chromium-linux.png new file mode 100644 index 00000000000..18cc709c2a7 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-slider-UiAbstract-Slider-default-visual-in-Legacy-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-slider-UiAbstract-Slider-default-visual-in-New-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-slider-UiAbstract-Slider-default-visual-in-New-system-1-chromium-linux.png new file mode 100644 index 00000000000..b309cc59897 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-number-slider-UiAbstract-Slider-default-visual-in-New-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-shortdate-Default-default-visual-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-shortdate-Default-default-visual-1-chromium-linux.png deleted file mode 100644 index 151b84d8ed7..00000000000 Binary files a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-shortdate-Default-default-visual-1-chromium-linux.png and /dev/null differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-shortdate-Default-default-visual-in-Legacy-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-shortdate-Default-default-visual-in-Legacy-system-1-chromium-linux.png new file mode 100644 index 00000000000..18306891b74 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-shortdate-Default-default-visual-in-Legacy-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-shortdate-Default-default-visual-in-New-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-shortdate-Default-default-visual-in-New-system-1-chromium-linux.png new file mode 100644 index 00000000000..135e3acdee6 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-shortdate-Default-default-visual-in-New-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-string-Default-default-visual-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-string-Default-default-visual-1-chromium-linux.png deleted file mode 100644 index 8c013d3b461..00000000000 Binary files a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-string-Default-default-visual-1-chromium-linux.png and /dev/null differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-string-Default-default-visual-in-Legacy-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-string-Default-default-visual-in-Legacy-system-1-chromium-linux.png new file mode 100644 index 00000000000..1909472cc0d Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-string-Default-default-visual-in-Legacy-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-string-Default-default-visual-in-New-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-string-Default-default-visual-in-New-system-1-chromium-linux.png new file mode 100644 index 00000000000..9d2f4bd60a2 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-string-Default-default-visual-in-New-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-string-multi-line-UiAbstract-MultilineText-default-visual-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-string-multi-line-UiAbstract-MultilineText-default-visual-1-chromium-linux.png deleted file mode 100644 index 05ee24afc93..00000000000 Binary files a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-string-multi-line-UiAbstract-MultilineText-default-visual-1-chromium-linux.png and /dev/null differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-string-multi-line-UiAbstract-MultilineText-default-visual-in-Legacy-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-string-multi-line-UiAbstract-MultilineText-default-visual-in-Legacy-system-1-chromium-linux.png new file mode 100644 index 00000000000..2cfe2da0b36 Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-string-multi-line-UiAbstract-MultilineText-default-visual-in-Legacy-system-1-chromium-linux.png differ diff --git a/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-string-multi-line-UiAbstract-MultilineText-default-visual-in-New-system-1-chromium-linux.png b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-string-multi-line-UiAbstract-MultilineText-default-visual-in-New-system-1-chromium-linux.png new file mode 100644 index 00000000000..ec0881b47ba Binary files /dev/null and b/e2e-tests/tests/editors/editors.test.ts-snapshots/Editor-Primitive-string-multi-line-UiAbstract-MultilineText-default-visual-in-New-system-1-chromium-linux.png differ diff --git a/ui/components-react/src/components-react.ts b/ui/components-react/src/components-react.ts index 73c1df8719f..2a5d0ad028e 100644 --- a/ui/components-react/src/components-react.ts +++ b/ui/components-react/src/components-react.ts @@ -92,6 +92,32 @@ export { } from "./components-react/datepicker/DatePickerPopupButton.js"; export { IntlFormatter } from "./components-react/datepicker/IntlFormatter.js"; +export { + BooleanValue, + DateValue, + EnumValue, + InstanceKeyValue, + NumericValue, + TextValue, + Value, +} from "./components-react/new-editors/values/Values.js"; +export { + EnumChoice, + EnumValueMetadata, + ValueMetadata, + ValueType, +} from "./components-react/new-editors/values/Metadata.js"; +export { EditorsRegistryProvider } from "./components-react/new-editors/editors-registry/EditorsRegistryProvider.js"; +export { + EditorProps, + EditorSpec, + createEditorSpec, +} from "./components-react/new-editors/Types.js"; +export { EditorRenderer } from "./components-react/new-editors/EditorRenderer.js"; +export { useCommittableValue } from "./components-react/new-editors/UseCommittableValue.js"; +export { FormattedNumericInput } from "./components-react/new-editors/editors/FormattedNumericInput.js"; +export { PropertyRecordEditor } from "./components-react/new-editors/interop/PropertyRecordEditor.js"; + export { BooleanEditor, BooleanPropertyEditor, diff --git a/ui/components-react/src/components-react/new-editors/EditorRenderer.tsx b/ui/components-react/src/components-react/new-editors/EditorRenderer.tsx new file mode 100644 index 00000000000..0711642dbee --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/EditorRenderer.tsx @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +/** @packageDocumentation + * @module PropertyEditors + */ + +import * as React from "react"; +import type { EditorProps } from "./Types.js"; +import { useEditor } from "./editors-registry/UseEditor.js"; + +/** + * Editor component that renders an editor based on the metadata and value. + * @beta + */ +export function EditorRenderer(props: EditorProps) { + const { metadata, value } = props; + const TypeEditor = useEditor(metadata, value); + + if (!TypeEditor) { + return null; + } + + return ; +} diff --git a/ui/components-react/src/components-react/new-editors/Types.ts b/ui/components-react/src/components-react/new-editors/Types.ts new file mode 100644 index 00000000000..70deef4df00 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/Types.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +/** @packageDocumentation + * @module PropertyEditors + */ + +import type { ValueMetadata } from "./values/Metadata.js"; +import type { Value } from "./values/Values.js"; + +/** + * An editor specification defining single editor with a predicate that determines if the editor can be used for a given value. + * @beta + */ +export interface EditorSpec { + applies: (metaData: ValueMetadata, value: Value | undefined) => boolean; + Editor: React.ComponentType; +} + +/** + * Generic editor props that are supplied to the editor for rendering. + * @beta + */ +export interface EditorProps { + metadata: TMetadata; + value?: TValue; + onChange: (value?: TValue) => void; + /** + * Callback that allows editor implementation to indicate that editing is finished. This is useful if editor + * is rendered inside a container that waits for committing/cancelling action (like `ENTER` or `ESC` key press) but editor implementation + * has additional actions that should cause commit (closing popup, commit button, etc.). + */ + commit?: () => void; + /** + * Callback that allows editor implementation to indicate that editing is cancelled. This is useful if editor + * is rendered inside a container that waits for committing/cancelling action (like `ENTER` or `ESC` key press) but editor implementation + * has additional actions that should cause cancellation (cancel button, etc). + */ + cancel?: () => void; + disabled?: boolean; + size?: "small" | "large"; +} + +/** + * Utility function to create an editor spec for editor with concrete metadata and value types. + * @beta + */ +export function createEditorSpec< + TMetadata extends ValueMetadata, + TValue extends Value +>({ + Editor, + isMetadataSupported, + isValueSupported, +}: { + isMetadataSupported: (metadata: ValueMetadata) => metadata is TMetadata; + isValueSupported: (value: Value) => value is TValue; + Editor: React.ComponentType>; +}): EditorSpec { + return { + applies: (metadata: ValueMetadata, value?: Value) => + isMetadataSupported(metadata) && + (value === undefined || isValueSupported(value)), + // typeguards in `applies` function will take care of casting + Editor: Editor as unknown as React.ComponentType, + }; +} diff --git a/ui/components-react/src/components-react/new-editors/UseCommittableValue.tsx b/ui/components-react/src/components-react/new-editors/UseCommittableValue.tsx new file mode 100644 index 00000000000..da02a989b18 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/UseCommittableValue.tsx @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +/** @packageDocumentation + * @module PropertyEditors + */ + +import * as React from "react"; +import type { Value } from "./values/Values.js"; + +interface UseCommittableValueProps { + initialValue?: Value; + onCommit: (value?: Value) => void; + onCancel?: () => void; +} + +/** + * Custom React hook that provides to commit or cancel value changes by editor using + * `Enter` or `Escape` keys. + * + * Example usage: + * + * ```tsx + * function MyValueEditor({ initialValue, ...editorProps }: Props) { + * const { value, onChange, onKeyDown, commit, cancel } = useCommittableValue({ + * initialValue, + * onCommit: (newValue) => { + * // commit new value + * } + * onCancel: () => { + * // restore to initial value or close editor + * } + * }) + * + * return
+ * + *
+ * } + * ``` + * + * @beta + */ +export function useCommittableValue({ + initialValue, + onCancel, + onCommit, +}: UseCommittableValueProps) { + const [currentValue, setCurrentValue] = React.useState( + initialValue + ); + const currentValueRef = React.useRef<{ + state: "changed" | "cancelled" | "initial"; + value?: Value; + }>({ + state: "initial", + value: initialValue, + }); + + const handleChange = (newValue?: Value) => { + currentValueRef.current = { state: "changed", value: newValue }; + setCurrentValue(newValue); + }; + + const handleCommit = () => { + if (currentValueRef.current.state === "changed") { + onCommit(currentValueRef.current.value); + return; + } + if (currentValueRef.current.state === "cancelled") { + return; + } + onCancel?.(); + }; + + const handleCancel = () => { + currentValueRef.current = { state: "cancelled" }; + onCancel?.(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + e.stopPropagation(); + + if (e.key === "Enter" || e.key === "Tab") { + handleCommit(); + } else if (e.key === "Escape") { + handleCancel(); + } + }; + + return { + onChange: handleChange, + onKeydown: handleKeyDown, + commit: handleCommit, + cancel: handleCancel, + value: currentValue, + }; +} diff --git a/ui/components-react/src/components-react/new-editors/editors-registry/DefaultEditors.tsx b/ui/components-react/src/components-react/new-editors/editors-registry/DefaultEditors.tsx new file mode 100644 index 00000000000..604dedcb801 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/editors-registry/DefaultEditors.tsx @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import { StandardEditorNames } from "@itwin/appui-abstract"; +import { BooleanEditor } from "../editors/BooleanEditor.js"; +import { DateTimeEditor } from "../editors/DateTimeEditor.js"; +import { EnumEditor } from "../editors/EnumEditor.js"; +import { NumericEditor } from "../editors/NumericEditor.js"; +import { TextEditor } from "../editors/TextEditor.js"; + +import { ColorEditorSpec as InteropColorEditorSpec } from "../interop/old-editors/Color.js"; +import { CustomNumberEditorSpec as InteropCustomNumberEditorSpec } from "../interop/old-editors/CustomNumber.js"; +import { EnumEditorSpec as InteropEnumEditorSpec } from "../interop/old-editors/Enum.js"; +import { EnumButtonGroupEditorSpec as InteropEnumButtonGroupEditorSpec } from "../interop/old-editors/EnumButtonGroup.js"; +import { MultilineEditorSpec as InteropMultilineEditorSpec } from "../interop/old-editors/TextArea.js"; +import { SliderEditorSpec as InteropSliderEditorSpec } from "../interop/old-editors/Slider.js"; +import { NumericInputEditorSpec as InteropNumericInputEditorSpec } from "../interop/old-editors/NumericInput.js"; + +import { createEditorSpec, type EditorSpec } from "../Types.js"; +import type { EnumValueMetadata, ValueMetadata } from "../values/Metadata.js"; +import { Value } from "../values/Values.js"; +import { ToggleEditor } from "../editors/ToggleEditor.js"; +import { DateEditor } from "../editors/DateEditor.js"; + +/** v8 ignore start */ + +/** + * Specification for default text editor. It applies for values whose type is "string". + * @internal + */ +export const TextEditorSpec: EditorSpec = createEditorSpec({ + isMetadataSupported: (metadata): metadata is ValueMetadata => + metadata.type === "string", + isValueSupported: Value.isText, + Editor: TextEditor, +}); + +/** + * Specification for default date editor. It applies for values whose type is "date". + * @internal + */ +export const DateEditorSpec: EditorSpec = createEditorSpec({ + isMetadataSupported: (metadata): metadata is ValueMetadata => + metadata.type === "date", + isValueSupported: Value.isDate, + Editor: DateEditor, +}); + +/** + * Specification for default date time editor. It applies for values whose type is "dateTime". + * @internal + */ +export const DateTimeEditorSpec: EditorSpec = createEditorSpec({ + isMetadataSupported: (metadata): metadata is ValueMetadata => + metadata.type === "dateTime", + isValueSupported: Value.isDate, + Editor: DateTimeEditor, +}); + +/** + * Specification for default boolean editor. It applies for values whose type is "bool". + * @internal + */ +export const BoolEditorSpec: EditorSpec = createEditorSpec({ + isMetadataSupported: (metadata): metadata is ValueMetadata => + metadata.type === "bool", + isValueSupported: Value.isBoolean, + Editor: BooleanEditor, +}); + +/** + * Specification for default numeric editor. It applies for values whose type is "number". + * @internal + */ +export const NumericEditorSpec: EditorSpec = createEditorSpec({ + isMetadataSupported: (metadata): metadata is ValueMetadata => + metadata.type === "number", + isValueSupported: Value.isNumeric, + Editor: NumericEditor, +}); + +/** + * Specification for default toggle editor. It applies for values whose type is "bool" when preferred editor set to `toggle`. + * @internal + */ +export const EnumEditorSpec: EditorSpec = createEditorSpec({ + isMetadataSupported: (metadata): metadata is EnumValueMetadata => + metadata.type === "enum", + isValueSupported: Value.isEnum, + Editor: EnumEditor, +}); + +/** + * Specification for default enum editor. It applies for values whose type is "enum". + * @internal + */ +export const ToggleEditorSpec: EditorSpec = createEditorSpec({ + isMetadataSupported: (metadata): metadata is ValueMetadata => + metadata.type === "bool" && + metadata.preferredEditor === StandardEditorNames.Toggle, + isValueSupported: Value.isBoolean, + Editor: ToggleEditor, +}); + +/** + * List of default editor specifications that are used as fallback if EditorRegistry does not provide a custom editor. + * @internal + */ +export const defaultEditorSpecs: EditorSpec[] = [ + // list editors with preferred editor check first + ToggleEditorSpec, + + // list editors that checks only value type + TextEditorSpec, + BoolEditorSpec, + NumericEditorSpec, + DateEditorSpec, + DateTimeEditorSpec, + EnumEditorSpec, +]; + +/** + * List of editor specifications that are rewritten based on the old version that accepts editor params from `PropertyRecord`. Needed to support + * editing customizations used through `PropertyRecord`. + * @internal + */ +export const interopEditorSpecs: EditorSpec[] = [ + InteropSliderEditorSpec, + InteropEnumButtonGroupEditorSpec, + InteropNumericInputEditorSpec, + InteropCustomNumberEditorSpec, + InteropColorEditorSpec, + InteropMultilineEditorSpec, + + InteropEnumEditorSpec, +]; + +/** v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/editors-registry/EditorsRegistryContext.ts b/ui/components-react/src/components-react/new-editors/editors-registry/EditorsRegistryContext.ts new file mode 100644 index 00000000000..de9954bfbfe --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/editors-registry/EditorsRegistryContext.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import * as React from "react"; +import type { EditorSpec } from "../Types.js"; + +interface EditorsRegistry { + editors: EditorSpec[]; +} + +/** + * Context for storing registered editors. + * @internal + */ +export const EditorsRegistryContext = React.createContext({ + editors: [], +}); +EditorsRegistryContext.displayName = "uifw:EditorsRegistryContext"; diff --git a/ui/components-react/src/components-react/new-editors/editors-registry/EditorsRegistryProvider.tsx b/ui/components-react/src/components-react/new-editors/editors-registry/EditorsRegistryProvider.tsx new file mode 100644 index 00000000000..636c518947f --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/editors-registry/EditorsRegistryProvider.tsx @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +/** @packageDocumentation + * @module PropertyEditors + */ + +import * as React from "react"; +import { useContext, useMemo } from "react"; +import type { EditorSpec } from "../Types.js"; +import { EditorsRegistryContext } from "./EditorsRegistryContext.js"; + +/** + * Provider that adds supplied editors into `EditorsRegistry`. Multiple providers can be nested together. + * Editors added through the lowest level provider will have the highest priority. + * @beta + */ +export function EditorsRegistryProvider({ + children, + editors, +}: { + children: React.ReactNode; + editors: EditorSpec[]; +}) { + const parentContext = useContext(EditorsRegistryContext); + + const value = useMemo(() => { + return { + editors: [...editors, ...parentContext.editors], + }; + }, [parentContext, editors]); + + return ( + + {children} + + ); +} diff --git a/ui/components-react/src/components-react/new-editors/editors-registry/UseEditor.ts b/ui/components-react/src/components-react/new-editors/editors-registry/UseEditor.ts new file mode 100644 index 00000000000..bbb0dc1608c --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/editors-registry/UseEditor.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +/** @packageDocumentation + * @module PropertyEditors + */ + +import * as React from "react"; +import type { EditorSpec } from "../Types.js"; +import type { ValueMetadata } from "../values/Metadata.js"; +import type { Value } from "../values/Values.js"; +import { defaultEditorSpecs, interopEditorSpecs } from "./DefaultEditors.js"; +import { EditorsRegistryContext } from "./EditorsRegistryContext.js"; +import { FallbackEditor } from "../editors/FallbackEditor.js"; + +/** v8 ignore start */ + +/** + * Custom React hook that returns editor for specified metadata and value. It uses `EditorsRegistry` context to find registered editors. + * If no registered editor is found, it will use applicable default editor. + * + * @beta + */ +export function useEditor( + metadata: ValueMetadata, + value: Value | undefined +): EditorSpec["Editor"] | undefined { + const { editors } = React.useContext(EditorsRegistryContext); + + const registeredEditor = editors.find((editor) => + editor.applies(metadata, value) + )?.Editor; + if (registeredEditor) { + return registeredEditor; + } + + const oldEditor = interopEditorSpecs.find((editor) => + editor.applies(metadata, value) + )?.Editor; + if (oldEditor) { + return oldEditor; + } + + const defaultEditor = defaultEditorSpecs.find((editor) => + editor.applies(metadata, value) + )?.Editor; + if (defaultEditor) { + return defaultEditor; + } + + return FallbackEditor; +} + +/** v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/editors/BooleanEditor.tsx b/ui/components-react/src/components-react/new-editors/editors/BooleanEditor.tsx new file mode 100644 index 00000000000..b97ecc29cd8 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/editors/BooleanEditor.tsx @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import * as React from "react"; +import { Checkbox } from "@itwin/itwinui-react"; +import type { EditorProps } from "../Types.js"; +import type { ValueMetadata } from "../values/Metadata.js"; +import type { BooleanValue } from "../values/Values.js"; + +/* v8 ignore start */ + +/** + * Boolean value editor that renders `Checkbox` component for changing value. + * @internal + */ +export function BooleanEditor({ + value, + onChange, + commit, + disabled, +}: EditorProps) { + const currentValue = value?.value ?? false; + const handleChange = (e: React.ChangeEvent) => { + const newValue = { value: e.target.checked }; + onChange(newValue); + commit?.(); + }; + + return ( + + ); +} + +/* v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/editors/DateEditor.tsx b/ui/components-react/src/components-react/new-editors/editors/DateEditor.tsx new file mode 100644 index 00000000000..7160aa59ad6 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/editors/DateEditor.tsx @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import * as React from "react"; +import type { EditorProps } from "../Types.js"; +import type { ValueMetadata } from "../values/Metadata.js"; +import type { DateValue } from "../values/Values.js"; +import { DateInput } from "./DateInput.js"; + +/* v8 ignore start */ + +/** + * Date value editor that render `DatePicker` component for changing value. + * @internal + */ +export function DateEditor({ + value, + onChange, + commit, + size, + disabled, +}: EditorProps) { + return ( + { + onChange({ value: newValue }); + }} + onClose={commit} + size={size} + disabled={disabled} + /> + ); +} + +/* v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/editors/DateInput.tsx b/ui/components-react/src/components-react/new-editors/editors/DateInput.tsx new file mode 100644 index 00000000000..db02219e692 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/editors/DateInput.tsx @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import * as React from "react"; +import { Button, DatePicker, Popover } from "@itwin/itwinui-react"; + +/* v8 ignore start */ + +interface DateInputProps { + value?: Date; + onChange: (value: Date) => void; + onClose?: () => void; + size?: "small" | "large"; + disabled?: boolean; + showTimePicker?: boolean; +} + +/** + * @internal + */ +export function DateInput({ + value, + onChange, + onClose, + size, + showTimePicker, + disabled, +}: DateInputProps) { + const currentValue = value ?? new Date(); + const dateStr = showTimePicker + ? currentValue.toLocaleString() + : currentValue.toLocaleDateString(); + + return ( + + } + onVisibleChange={(visible) => { + if (!visible) { + onClose?.(); + } + }} + > + + + ); +} + +/* v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/editors/DateTimeEditor.tsx b/ui/components-react/src/components-react/new-editors/editors/DateTimeEditor.tsx new file mode 100644 index 00000000000..328340c7bd0 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/editors/DateTimeEditor.tsx @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import * as React from "react"; +import type { EditorProps } from "../Types.js"; +import type { ValueMetadata } from "../values/Metadata.js"; +import type { DateValue } from "../values/Values.js"; +import { DateInput } from "./DateInput.js"; + +/* v8 ignore start */ + +/** + * Date time value editor that render `DatePicker` component with time selector for changing value. + * @internal + */ +export function DateTimeEditor({ + value, + onChange, + commit, + size, + disabled, +}: EditorProps) { + return ( + { + onChange({ value: newValue }); + }} + onClose={commit} + size={size} + disabled={disabled} + showTimePicker={true} + /> + ); +} + +/* v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/editors/EnumEditor.tsx b/ui/components-react/src/components-react/new-editors/editors/EnumEditor.tsx new file mode 100644 index 00000000000..01aa7ea8df8 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/editors/EnumEditor.tsx @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import * as React from "react"; +import { Select } from "@itwin/itwinui-react"; +import type { EditorProps } from "../Types.js"; +import type { EnumChoice, EnumValueMetadata } from "../values/Metadata.js"; +import type { EnumValue } from "../values/Values.js"; + +/* v8 ignore start */ + +/** + * Enum value editor that renders `Select` component for changing value. + * @internal + */ +export function EnumEditor({ + metadata, + value, + onChange, + commit, + size, + disabled, +}: EditorProps) { + const choices = metadata.choices; + const currentValue = getEnumValue(value, choices); + + const handleChange = (newChoice: number | string) => { + const choice = choices.find((c) => c.value === newChoice); + const newValue = { choice: newChoice, label: choice?.label ?? "" }; + onChange(newValue); + commit?.(); + }; + + return ( + ; +} + +function getTextValue(value?: Value) { + if (value === undefined) { + return value; + } + if (Value.isBoolean(value)) { + return value.value ? "true" : "false"; + } + if (Value.isDate(value)) { + return value.value.toDateString(); + } + if (Value.isEnum(value)) { + return value.choice; + } + if (Value.isNumeric(value)) { + return value.displayValue; + } + if (Value.isText(value)) { + return value.value; + } + if (Value.isInstanceKey(value)) { + return value.label; + } + return ""; +} + +/* v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/editors/FormattedNumericInput.tsx b/ui/components-react/src/components-react/new-editors/editors/FormattedNumericInput.tsx new file mode 100644 index 00000000000..28266891c30 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/editors/FormattedNumericInput.tsx @@ -0,0 +1,124 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +/** @packageDocumentation + * @module PropertyEditors + */ + +import * as React from "react"; +import { Input } from "@itwin/itwinui-react"; +import type { NumericValue } from "../values/Values.js"; + +/* v8 ignore start */ + +/** + * Props for FormattedNumericInput component. + * @beta + */ +interface FormattedNumericInputProps { + value: NumericValue; + onChange: (value: NumericValue) => void; + parseValue: (value: string) => number | undefined; + formatValue: (num: number) => string; + disabled?: boolean; + size?: "small" | "large"; +} + +/** + * A numeric input that allows to pass custom parsing/formatting logic to handle values with units etc. + * @beta + */ +export function FormattedNumericInput({ + onChange, + value, + parseValue, + formatValue, + disabled, + size, +}: FormattedNumericInputProps) { + const { currentValue, inputProps } = useParsedNumberInput({ + initialValue: value.rawValue, + parseValue, + formatValue, + }); + const onChangeRef = React.useRef(onChange); + React.useEffect(() => { + onChangeRef.current = onChange; + }, [onChange]); + + React.useEffect(() => { + onChangeRef.current(currentValue); + }, [currentValue]); + + return ; +} + +function useParsedNumberInput({ + initialValue, + formatValue, + parseValue, +}: { + initialValue: number | undefined; + parseValue: (value: string) => number | undefined; + formatValue: (num: number) => string; +}) { + interface State { + value: NumericValue; + placeholder: string; + } + + const [state, setState] = React.useState(() => { + return { + value: { + rawValue: initialValue, + displayValue: + initialValue !== undefined ? formatValue(initialValue) : "", + }, + placeholder: formatValue(initialValue ?? 123.45), + }; + }); + + React.useEffect(() => { + setState((prevState) => { + return { + ...prevState, + value: { + ...prevState.value, + displayValue: + prevState.value.rawValue !== undefined + ? formatValue(prevState.value.rawValue) + : "", + }, + placeholder: formatValue(123.45), + }; + }); + }, [formatValue]); + + const handleChange = (e: React.ChangeEvent) => { + const newValue = e.target.value; + const rawValue = parseValue(newValue); + + setState((prevState) => { + return { + ...prevState, + value: { + ...prevState.value, + rawValue, + displayValue: newValue, + }, + }; + }); + }; + + return { + currentValue: state.value, + inputProps: { + value: state.value.displayValue, + placeholder: state.placeholder, + onChange: handleChange, + }, + }; +} + +/* v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/editors/NumericEditor.tsx b/ui/components-react/src/components-react/new-editors/editors/NumericEditor.tsx new file mode 100644 index 00000000000..5a0e2186140 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/editors/NumericEditor.tsx @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import * as React from "react"; +import { Input } from "@itwin/itwinui-react"; +import type { EditorProps } from "../Types.js"; +import type { ValueMetadata } from "../values/Metadata.js"; +import type { NumericValue } from "../values/Values.js"; + +/* v8 ignore start */ + +/** + * Numeric value editor that renders `Input` components for changing value. + * @internal + */ +export function NumericEditor({ + value, + onChange, + size, + disabled, +}: EditorProps) { + const currentValue = getNumericValue(value); + return ( + + onChange({ + rawValue: parseFloat(e.target.value), + displayValue: e.target.value, + }) + } + size={size} + disabled={disabled} + /> + ); +} + +function getNumericValue(value: NumericValue | undefined): NumericValue { + return value ? value : { rawValue: undefined, displayValue: "" }; +} + +/* v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/editors/TextEditor.tsx b/ui/components-react/src/components-react/new-editors/editors/TextEditor.tsx new file mode 100644 index 00000000000..77f2d67a8e7 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/editors/TextEditor.tsx @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import * as React from "react"; +import { Input } from "@itwin/itwinui-react"; +import type { EditorProps } from "../Types.js"; +import type { ValueMetadata } from "../values/Metadata.js"; +import type { TextValue } from "../values/Values.js"; + +/* v8 ignore start */ + +/** + * Text value editor that renders `Input` component for changing value. + * @internal + */ +export function TextEditor({ + value, + onChange, + size, + disabled, +}: EditorProps) { + const currentValue = value ? value : { value: "" }; + + return ( + onChange({ value: e.target.value })} + size={size} + disabled={disabled} + /> + ); +} + +/* v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/editors/ToggleEditor.tsx b/ui/components-react/src/components-react/new-editors/editors/ToggleEditor.tsx new file mode 100644 index 00000000000..462b45cc2a4 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/editors/ToggleEditor.tsx @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import * as React from "react"; +import type { ValueMetadata } from "../values/Metadata.js"; +import type { BooleanValue } from "../values/Values.js"; +import type { EditorProps } from "../Types.js"; +import { ToggleSwitch } from "@itwin/itwinui-react"; + +/* v8 ignore start */ + +/** + * Boolean value editor that renders `ToggleSwitch` component for changing value. + * @internal + */ +export function ToggleEditor({ + value, + onChange, + commit, + disabled, + size, +}: EditorProps) { + const currentValue = value ?? { value: false }; + const handleChange = (e: React.ChangeEvent) => { + const newValue = { value: e.target.checked }; + onChange(newValue); + commit?.(); + }; + + return ( + + ); +} + +/* v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/interop/EditorInterop.ts b/ui/components-react/src/components-react/new-editors/interop/EditorInterop.ts new file mode 100644 index 00000000000..6ff474b0cce --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/interop/EditorInterop.ts @@ -0,0 +1,169 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import type { + Primitives, + PrimitiveValue, + PropertyRecord, +} from "@itwin/appui-abstract"; +import { PropertyValueFormat } from "@itwin/appui-abstract"; +import type { + BooleanValue, + DateValue, + EnumValue, + InstanceKeyValue, + NumericValue, + TextValue, +} from "../values/Values.js"; +import { Value as NewEditorValue } from "../values/Values.js"; +import type { OldEditorMetadata } from "./Metadata.js"; + +/** + * Interop utilities for converting between old and new editor values. + * @internal + */ +export namespace EditorInterop { + /** Attempts to convert `PropertyRecord` into `ValueMetadata` and `Value`. */ + export function getMetadataAndValue(propertyRecord: PropertyRecord): { + metadata: OldEditorMetadata | undefined; + value: NewEditorValue | undefined; + } { + const baseMetadata: Omit = { + preferredEditor: propertyRecord.property.editor?.name, + params: propertyRecord.property.editor?.params, + extendedData: propertyRecord.extendedData, + enum: propertyRecord.property.enum, + typename: propertyRecord.property.typename, + }; + + const primitiveValue = propertyRecord.value as PrimitiveValue; + switch (propertyRecord.property.typename) { + case "text": + case "string": + return { + metadata: { + ...baseMetadata, + type: "string", + }, + value: { + value: (primitiveValue.value as string) ?? "", + } satisfies TextValue, + }; + case "dateTime": + case "shortdate": + return { + metadata: { + ...baseMetadata, + type: + propertyRecord.property.typename === "shortdate" + ? "date" + : "dateTime", + }, + value: { + value: (primitiveValue.value as Date) ?? new Date(), + } satisfies DateValue, + }; + case "boolean": + case "bool": + return { + metadata: { + ...baseMetadata, + type: "bool", + }, + value: { + value: (primitiveValue.value as boolean) ?? false, + } satisfies BooleanValue, + }; + case "float": + case "double": + case "int": + case "integer": + case "number": + return { + metadata: { + ...baseMetadata, + type: "number", + }, + value: { + rawValue: primitiveValue.value as number, + displayValue: primitiveValue.displayValue + ? primitiveValue.displayValue + : primitiveValue.value !== undefined + ? `${primitiveValue.value as number}` + : "", + } satisfies NumericValue, + }; + case "enum": + return { + metadata: { + ...baseMetadata, + type: "enum", + }, + value: { + choice: primitiveValue.value as number | string, + } satisfies EnumValue, + }; + case "navigation": + return { + metadata: { + ...baseMetadata, + type: "instanceKey", + }, + value: { + key: primitiveValue.value as Primitives.InstanceKey, + label: primitiveValue.displayValue ?? "", + } satisfies InstanceKeyValue, + }; + } + + return { + metadata: undefined, + value: undefined, + }; + } + + /** Converts new editors system `Value` into old `PrimitiveValue` */ + export function convertToPrimitiveValue( + newValue: NewEditorValue + ): PrimitiveValue { + if (NewEditorValue.isText(newValue)) { + return { + valueFormat: PropertyValueFormat.Primitive, + value: newValue.value, + displayValue: newValue.value, + }; + } + if (NewEditorValue.isNumeric(newValue)) { + return { + valueFormat: PropertyValueFormat.Primitive, + value: newValue.rawValue, + displayValue: newValue.displayValue, + }; + } + if (NewEditorValue.isBoolean(newValue)) { + return { + valueFormat: PropertyValueFormat.Primitive, + value: newValue.value, + displayValue: newValue.value.toString(), + }; + } + if (NewEditorValue.isDate(newValue)) { + return { + valueFormat: PropertyValueFormat.Primitive, + value: newValue.value, + displayValue: newValue.value.toString(), + }; + } + + if (NewEditorValue.isEnum(newValue)) { + return { + valueFormat: PropertyValueFormat.Primitive, + value: newValue.choice, + }; + } + + throw new Error("Invalid value type"); + } +} diff --git a/ui/components-react/src/components-react/new-editors/interop/IconsRegistry.tsx b/ui/components-react/src/components-react/new-editors/interop/IconsRegistry.tsx new file mode 100644 index 00000000000..59ad4859550 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/interop/IconsRegistry.tsx @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import { SvgPlaceholder } from "@itwin/itwinui-icons-react"; +import * as React from "react"; + +/* v8 ignore start */ + +/** + * @internal + */ +export function findIcon(iconName?: string) { + if (!iconName) { + return ; + } + + const icon = webfontIconsMap[iconName]; + return icon ? icon : ; +} + +const webfontIconsMap: { + [key: string]: React.JSX.Element; +} = { + "icon-select-single": ( + + + + ), + "icon-select-line": ( + + + + ), + "icon-select-box": ( + + + + ), + "icon-replace": ( + + + + ), + "icon-select-plus": ( + + + + ), + "icon-select-minus": ( + + + + ), + "icon-app-1": ( + + + + + ), + "icon-app-2": ( + + + + + ), + "icon-apps-itwin": ( + + + + ), +}; + +/* v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/interop/Metadata.ts b/ui/components-react/src/components-react/new-editors/interop/Metadata.ts new file mode 100644 index 00000000000..37433289ac2 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/interop/Metadata.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import type { + EnumerationChoicesInfo, + PropertyEditorParams, +} from "@itwin/appui-abstract"; +import type { ValueMetadata } from "../values/Metadata.js"; + +/* v8 ignore start */ + +/** + * Metadata that is created by mapping `PropertyRecord` used to render old editor into the new editor metadata. + * @internal + */ +export interface OldEditorMetadata extends ValueMetadata { + params?: PropertyEditorParams[]; + extendedData?: { [key: string]: unknown }; + enum?: EnumerationChoicesInfo; + quantityType?: string; + typename: string; +} + +/** + * Type guard for `OldEditorMetadata`. + * @internal + */ +export function isOldEditorMetadata( + metadata: ValueMetadata +): metadata is OldEditorMetadata { + return (metadata as OldEditorMetadata).typename !== undefined; +} + +/* v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/interop/PropertyRecordEditor.tsx b/ui/components-react/src/components-react/new-editors/interop/PropertyRecordEditor.tsx new file mode 100644 index 00000000000..515274957d6 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/interop/PropertyRecordEditor.tsx @@ -0,0 +1,122 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +/** @packageDocumentation + * @module PropertyEditors + */ + +import * as React from "react"; +import { + type PropertyRecord, + PropertyValueFormat, +} from "@itwin/appui-abstract"; +import { EditorInterop } from "./EditorInterop.js"; +import { + EditorContainer, + type PropertyUpdatedArgs, +} from "../../editors/EditorContainer.js"; +import { useCommittableValue } from "../UseCommittableValue.js"; +import { EditorRenderer } from "../EditorRenderer.js"; +import type { ValueMetadata } from "../values/Metadata.js"; +import type { Value } from "../values/Values.js"; + +interface PropertyRecordEditorProps { + propertyRecord: PropertyRecord; + onCommit: (args: PropertyUpdatedArgs) => void; + onCancel: () => void; + onClick?: () => void; + setFocus?: boolean; + size?: "small" | "large"; + editorSystem?: "legacy" | "new"; +} + +/** + * Editor component for editing property values represented by `PropertyRecord`. + * @beta + */ +export function PropertyRecordEditor({ + propertyRecord, + onCommit, + onCancel, + onClick, + setFocus, + size, + editorSystem, +}: PropertyRecordEditorProps) { + const { metadata, value } = EditorInterop.getMetadataAndValue(propertyRecord); + if (editorSystem === "new" && metadata && value) { + return ( + { + onCommit({ + propertyRecord, + newValue: + newValue === undefined + ? { valueFormat: PropertyValueFormat.Primitive } + : EditorInterop.convertToPrimitiveValue(newValue), + }); + }} + onCancel={onCancel} + onClick={onClick} + disabled={propertyRecord.isDisabled || propertyRecord.isReadonly} + size={size} + /> + ); + } + + return ( + + ); +} + +function CommittingEditor({ + metadata, + initialValue, + onCancel, + onCommit, + onClick, + disabled, + size, +}: { + metadata: ValueMetadata; + initialValue?: Value; + onCommit: (value?: Value) => void; + onCancel: () => void; + onClick?: () => void; + disabled?: boolean; + size?: "small" | "large"; +}) { + const { value, onChange, onKeydown, commit, cancel } = useCommittableValue({ + initialValue, + onCommit, + onCancel, + }); + + return ( +
+ +
+ ); +} diff --git a/ui/components-react/src/components-react/new-editors/interop/old-editors/Color.tsx b/ui/components-react/src/components-react/new-editors/interop/old-editors/Color.tsx new file mode 100644 index 00000000000..9e1e4302f84 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/interop/old-editors/Color.tsx @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import * as React from "react"; +import { + PropertyEditorParamTypes, + StandardEditorNames, +} from "@itwin/appui-abstract"; +import { + ColorPalette, + ColorPicker, + ColorSwatch, + ColorValue, + IconButton, + Popover, +} from "@itwin/itwinui-react"; +import type { EditorProps, EditorSpec } from "../../Types.js"; +import { createEditorSpec } from "../../Types.js"; +import type { OldEditorMetadata } from "../Metadata.js"; +import { isOldEditorMetadata } from "../Metadata.js"; +import type { NumericValue } from "../../values/Values.js"; +import { Value } from "../../values/Values.js"; +import { useColorEditorParams } from "./UseEditorParams.js"; + +/* v8 ignore start */ + +/** + * Editor specification for color editor. + * @internal + */ +export const ColorEditorSpec: EditorSpec = createEditorSpec({ + isMetadataSupported: (metadata): metadata is OldEditorMetadata => + isOldEditorMetadata(metadata) && + metadata.type === "number" && + !!metadata.params?.find( + (param) => param.type === PropertyEditorParamTypes.ColorData.valueOf() + ) && + metadata.preferredEditor === StandardEditorNames.ColorPicker, + isValueSupported: Value.isNumeric, + Editor: ColorEditor, +}); + +function ColorEditor({ + value, + metadata, + onChange, + commit, + size, +}: EditorProps) { + const colorParams = useColorEditorParams(metadata); + const colors = colorParams?.colorValues ?? []; + + const currentValue = value + ? value + : { rawValue: colors[0], displayValue: "" }; + const colorsList = colors.map((color) => ColorValue.fromTbgr(color)); + const activeColor = + currentValue.rawValue !== undefined + ? ColorValue.fromTbgr(currentValue.rawValue) + : colorsList[0]; + + const onColorChanged = (color: ColorValue) => { + onChange({ rawValue: color.toTbgr(), displayValue: "" }); + }; + + return ( + + + + } + onVisibleChange={(visible) => { + if (!visible) { + commit?.(); + } + }} + > + + + + + ); +} + +/* v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/interop/old-editors/CustomNumber.tsx b/ui/components-react/src/components-react/new-editors/interop/old-editors/CustomNumber.tsx new file mode 100644 index 00000000000..db6582f11b7 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/interop/old-editors/CustomNumber.tsx @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import * as React from "react"; +import { + PropertyEditorParamTypes, + StandardEditorNames, +} from "@itwin/appui-abstract"; +import { InputWithDecorations } from "@itwin/itwinui-react"; +import type { EditorProps } from "../../Types.js"; +import { createEditorSpec } from "../../Types.js"; +import type { OldEditorMetadata } from "../Metadata.js"; +import { isOldEditorMetadata } from "../Metadata.js"; +import type { NumericValue } from "../../values/Values.js"; +import { Value } from "../../values/Values.js"; +import { + useCustomFormattedNumberParams, + useIconEditorParams, + useInputEditorSizeParams, +} from "./UseEditorParams.js"; +import { findIcon } from "../IconsRegistry.js"; + +/* v8 ignore start */ + +/** @internal */ +export const CustomNumberEditorSpec = createEditorSpec({ + isMetadataSupported: (metadata): metadata is OldEditorMetadata => + isOldEditorMetadata(metadata) && + metadata.type === "number" && + !!metadata.params?.find( + (param) => + param.type === PropertyEditorParamTypes.CustomFormattedNumber.valueOf() + ) && + metadata.preferredEditor === StandardEditorNames.NumberCustom, + isValueSupported: Value.isNumeric, + Editor: CustomNumberEditor, +}); + +function CustomNumberEditor({ + metadata, + value, + onChange, + size, + disabled, +}: EditorProps) { + const formatParams = useCustomFormattedNumberParams(metadata); + const sizeParams = useInputEditorSizeParams(metadata); + const iconParams = useIconEditorParams(metadata); + const inputRef = React.useRef(null); + const [inputValue, setInputValue] = React.useState(() => { + return value?.rawValue !== undefined && formatParams + ? formatParams.formatFunction(value.rawValue) + : ""; + }); + + if (!formatParams) { + return null; + } + + const style: React.CSSProperties | undefined = + sizeParams?.size !== undefined + ? { + minWidth: `${sizeParams?.size * 0.75}em`, + } + : undefined; + const icon = + iconParams?.definition?.iconSpec !== undefined + ? findIcon(iconParams.definition.iconSpec) + : undefined; + + const handleChange = (e: React.ChangeEvent) => { + const currentValue = e.target.value; + const result = formatParams.parseFunction(currentValue); + const parsedValue = + typeof result.value === "number" ? result.value : undefined; + + setInputValue(currentValue); + + onChange({ + rawValue: parsedValue, + displayValue: currentValue, + }); + }; + + return ( + + {icon !== undefined ? ( + {icon} + ) : null} + + + ); +} + +/* v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/interop/old-editors/Enum.tsx b/ui/components-react/src/components-react/new-editors/interop/old-editors/Enum.tsx new file mode 100644 index 00000000000..09fa2284ea9 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/interop/old-editors/Enum.tsx @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import * as React from "react"; +import { + createEditorSpec, + type EditorProps, + type EditorSpec, +} from "../../Types.js"; +import { EnumEditor as NewEnumEditor } from "../../editors/EnumEditor.js"; +import type { OldEditorMetadata } from "../Metadata.js"; +import { isOldEditorMetadata } from "../Metadata.js"; +import type { EnumValue } from "../../values/Values.js"; +import { Value } from "../../values/Values.js"; +import { useEnumMetadata } from "./UseEnumMetadata.js"; + +/* v8 ignore start */ + +/** @internal */ +export const EnumEditorSpec: EditorSpec = createEditorSpec({ + isMetadataSupported: (metadata): metadata is OldEditorMetadata => + isOldEditorMetadata(metadata) && metadata.type === "enum", + isValueSupported: Value.isEnum, + Editor: EnumEditor, +}); + +function EnumEditor(props: EditorProps) { + const newMetadata = useEnumMetadata(props.metadata); + return ; +} + +/* v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/interop/old-editors/EnumButtonGroup.tsx b/ui/components-react/src/components-react/new-editors/interop/old-editors/EnumButtonGroup.tsx new file mode 100644 index 00000000000..5bc6ab0c254 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/interop/old-editors/EnumButtonGroup.tsx @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import * as React from "react"; +import type { ButtonGroupEditorParams } from "@itwin/appui-abstract"; +import { + type EnumerationChoice, + StandardEditorNames, +} from "@itwin/appui-abstract"; +import type { EditorProps, EditorSpec } from "../../Types.js"; +import { createEditorSpec } from "../../Types.js"; +import type { OldEditorMetadata } from "../Metadata.js"; +import { isOldEditorMetadata } from "../Metadata.js"; +import type { EnumValue } from "../../values/Values.js"; +import { Value } from "../../values/Values.js"; +import { useEnumMetadata } from "./UseEnumMetadata.js"; +import { useButtonGroupEditorParams } from "./UseEditorParams.js"; +import { ButtonGroup, IconButton } from "@itwin/itwinui-react"; +import { findIcon } from "../IconsRegistry.js"; + +/* v8 ignore start */ + +/** @internal */ +export const EnumButtonGroupEditorSpec: EditorSpec = createEditorSpec({ + isMetadataSupported: (metadata): metadata is OldEditorMetadata => + isOldEditorMetadata(metadata) && + metadata.type === "enum" && + metadata.preferredEditor === StandardEditorNames.EnumButtonGroup, + isValueSupported: Value.isEnum, + Editor: EnumButtonGroupEditor, +}); + +function EnumButtonGroupEditor({ + value, + onChange, + commit, + size, + disabled, + metadata, +}: EditorProps) { + const enumMetadata = useEnumMetadata(metadata); + const buttonGroupParams = useButtonGroupEditorParams(metadata); + + const enumIcons = React.useMemo(() => { + return buttonGroupParams + ? createIconsMap(enumMetadata.choices, buttonGroupParams) + : undefined; + }, [enumMetadata, buttonGroupParams]); + + const firstChoice = enumMetadata.choices[0] as EnumerationChoice | undefined; + const currentValue = value ? value : { choice: firstChoice?.value ?? "" }; + + return ( + + {enumMetadata.choices.map((choice) => { + const icon = findIcon(enumIcons?.get(choice.value)); + return ( + { + onChange({ choice: choice.value }); + commit?.(); + }} + label={choice.label} + isActive={choice.value === currentValue.choice} + size={size} + disabled={disabled} + > + {icon} + + ); + })} + + ); +} + +function createIconsMap( + choices: EnumerationChoice[], + params: ButtonGroupEditorParams +) { + const icons = new Map(); + for (let i = 0; i < choices.length; i++) { + const iconDef = params.buttons[i]; + icons.set(choices[i].value, iconDef.iconSpec); + } + return icons; +} + +/* v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/interop/old-editors/NumericInput.tsx b/ui/components-react/src/components-react/new-editors/interop/old-editors/NumericInput.tsx new file mode 100644 index 00000000000..d97e60cdb5e --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/interop/old-editors/NumericInput.tsx @@ -0,0 +1,133 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import * as React from "react"; +import { StandardEditorNames } from "@itwin/appui-abstract"; +import type { EditorProps, EditorSpec } from "../../Types.js"; +import { createEditorSpec } from "../../Types.js"; +import type { OldEditorMetadata } from "../Metadata.js"; +import { isOldEditorMetadata } from "../Metadata.js"; +import type { NumericValue } from "../../values/Values.js"; +import { Value } from "../../values/Values.js"; +import { + useInputEditorSizeParams, + useRangeEditorParams, +} from "./UseEditorParams.js"; +import { Input } from "@itwin/itwinui-react"; + +/* v8 ignore start */ + +/** @internal */ +export const NumericInputEditorSpec: EditorSpec = createEditorSpec({ + isMetadataSupported: (metadata): metadata is OldEditorMetadata => + isOldEditorMetadata(metadata) && + metadata.type === "number" && + metadata.preferredEditor === StandardEditorNames.NumericInput, + isValueSupported: Value.isNumeric, + Editor: NumericInputEditor, +}); + +function NumericInputEditor({ + metadata, + value, + onChange, + size, + disabled, +}: EditorProps) { + const sizeParams = useInputEditorSizeParams(metadata); + const rangeParams = useRangeEditorParams(metadata); + const handleChange = (newValue: string) => { + onChange({ + displayValue: newValue, + rawValue: parseFloat(newValue), + }); + }; + + const style: React.CSSProperties | undefined = + sizeParams?.size !== undefined + ? { + minWidth: `${sizeParams?.size * 0.75}em`, + } + : undefined; + + const inputProps = useNumericInput({ + min: rangeParams?.minimum, + max: rangeParams?.maximum, + precision: rangeParams?.precision, + step: rangeParams?.step, + maxLength: sizeParams?.maxLength, + onChange: handleChange, + value: value?.displayValue ?? "", + }); + + return ( + + ); +} + +interface UseNumericInputProps { + min?: number; + max?: number; + step?: number; + precision?: number; + maxLength?: number; + value: string; + onChange: (newValue: string) => void; +} + +function useNumericInput({ + min, + max, + step, + precision, + maxLength, + value, + onChange, +}: UseNumericInputProps) { + const formatValue = (val: number) => { + const appliedMin = min ?? Number.MIN_VALUE; + const appliedMax = max ?? Number.MAX_VALUE; + const valueInRange = Math.min(appliedMax, Math.max(appliedMin, val)); + + return precision + ? valueInRange.toFixed(precision) + : valueInRange.toString(); + }; + + const [formattedValue, setFormattedValue] = React.useState(() => { + return value.length !== 0 ? formatValue(Number(value)) : value; + }); + + const handleChange = (event: React.ChangeEvent) => { + const currentValue = event.target.value; + if (maxLength && currentValue.length > maxLength) { + return; + } + setFormattedValue(currentValue); + onChange(currentValue); + }; + + const handleBlur = () => { + if (precision) { + const newFormattedValue = formatValue(Number(formattedValue)); + if (newFormattedValue !== formattedValue) { + setFormattedValue(newFormattedValue); + onChange(newFormattedValue); + } + } + }; + + return { + type: "number", + min, + max, + step, + value: formattedValue, + onChange: handleChange, + onBlur: handleBlur, + }; +} + +/* v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/interop/old-editors/Slider.tsx b/ui/components-react/src/components-react/new-editors/interop/old-editors/Slider.tsx new file mode 100644 index 00000000000..fbeeccec9e4 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/interop/old-editors/Slider.tsx @@ -0,0 +1,128 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import * as React from "react"; +import type { EditorProps } from "../../Types.js"; +import { createEditorSpec } from "../../Types.js"; +import type { OldEditorMetadata } from "../Metadata.js"; +import { isOldEditorMetadata } from "../Metadata.js"; +import type { NumericValue } from "../../values/Values.js"; +import { Value } from "../../values/Values.js"; +import type { SliderEditorParams } from "@itwin/appui-abstract"; +import { + PropertyEditorParamTypes, + StandardEditorNames, +} from "@itwin/appui-abstract"; +import { Button, Icon, Popover, Slider } from "@itwin/itwinui-react"; +import { useSliderEditorParams } from "./UseEditorParams.js"; +import { findIcon } from "../IconsRegistry.js"; + +/* v8 ignore start */ + +/** @internal */ +export const SliderEditorSpec = createEditorSpec({ + isMetadataSupported: (metadata): metadata is OldEditorMetadata => + isOldEditorMetadata(metadata) && + metadata.type === "number" && + !!metadata.params?.find( + (param) => param.type === PropertyEditorParamTypes.Slider.valueOf() + ) && + metadata.preferredEditor === StandardEditorNames.Slider, + isValueSupported: Value.isNumeric, + Editor: SliderEditor, +}); + +function SliderEditor({ + metadata, + value, + disabled, + onChange, + commit, + size, +}: EditorProps) { + const sliderParams = useSliderEditorParams(metadata); + if (!sliderParams) { + return null; + } + + const minLabel = + sliderParams.showMinMax && sliderParams.minIconSpec ? ( + {findIcon(sliderParams.minIconSpec)} + ) : undefined; + const maxLabel = + sliderParams.showMinMax && sliderParams.maxIconSpec ? ( + {findIcon(sliderParams.maxIconSpec)} + ) : undefined; + + const handleChange = (values: ReadonlyArray): void => { + const newValue = values[0]; + onChange({ rawValue: newValue, displayValue: `${newValue}` }); + }; + + const currentValue = value?.rawValue ?? sliderParams.minimum; + const slider = ( + { + return { + placement: sliderParams.tooltipBelow ? "bottom" : "top", + content: formatTickLabel(tooltipValue, sliderParams), + visible: sliderParams.showTooltip, + }; + }} + tickLabels={getTickLabels(sliderParams)} + onChange={handleChange} + disabled={disabled} + /> + ); + return ( + { + if (!visible) { + commit?.(); + } + }} + applyBackground={true} + > + + + ); +} + +function getTickLabels(sliderParams: SliderEditorParams) { + if (!sliderParams.showTicks || !sliderParams.showTickLabels) { + return undefined; + } + const count = sliderParams.getTickCount ? sliderParams.getTickCount() : 0; + if (count > 0) { + const increment = (sliderParams.maximum - sliderParams.minimum) / count; + return Array.from({ length: count + 1 }, (_, i) => + formatTickLabel(i * increment + sliderParams.minimum, sliderParams) + ); + } + return sliderParams + .getTickValues?.() + .map((val) => formatTickLabel(val, sliderParams)); +} + +function formatTickLabel(value: number, params: SliderEditorParams) { + return params.formatTooltip ? params.formatTooltip(value) : `${value}`; +} + +/* v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/interop/old-editors/TextArea.tsx b/ui/components-react/src/components-react/new-editors/interop/old-editors/TextArea.tsx new file mode 100644 index 00000000000..dd8516f6e9a --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/interop/old-editors/TextArea.tsx @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import * as React from "react"; +import { Button, Popover, Textarea } from "@itwin/itwinui-react"; +import { createEditorSpec, type EditorProps } from "../../Types.js"; +import { isOldEditorMetadata, type OldEditorMetadata } from "../Metadata.js"; +import { type TextValue, Value } from "../../values/Values.js"; +import { StandardEditorNames } from "@itwin/appui-abstract"; + +/* v8 ignore start */ + +/** @internal */ +export const MultilineEditorSpec = createEditorSpec({ + isMetadataSupported: (metadata): metadata is OldEditorMetadata => + isOldEditorMetadata(metadata) && + metadata.type === "string" && + metadata.preferredEditor === StandardEditorNames.MultiLine, + isValueSupported: Value.isText, + Editor: TextAreaEditor, +}); + +function TextAreaEditor({ + value, + onChange, + commit, + size, + disabled, +}: EditorProps) { + const currentValue = value ?? { value: "" }; + + return ( + { + const newValue = e.currentTarget.value; + onChange({ value: newValue }); + }} + /> + } + onVisibleChange={(visible) => { + if (!visible) { + commit?.(); + } + }} + > + + + ); +} + +/* v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/interop/old-editors/UseEditorParams.ts b/ui/components-react/src/components-react/new-editors/interop/old-editors/UseEditorParams.ts new file mode 100644 index 00000000000..5643cec91ee --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/interop/old-editors/UseEditorParams.ts @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import * as React from "react"; +import type { OldEditorMetadata } from "../Metadata.js"; +import type { + ButtonGroupEditorParams, + ColorEditorParams, + CustomFormattedNumberParams, + IconEditorParams, + InputEditorSizeParams, + RangeEditorParams, + SliderEditorParams, +} from "@itwin/appui-abstract"; +import { PropertyEditorParamTypes } from "@itwin/appui-abstract"; + +/* v8 ignore start */ + +/** + * @internal + */ +export function useCustomFormattedNumberParams( + metadata: OldEditorMetadata +): CustomFormattedNumberParams | undefined { + return React.useMemo( + () => + metadata.params?.find( + (param) => + param.type === + PropertyEditorParamTypes.CustomFormattedNumber.valueOf() + ) as CustomFormattedNumberParams, + [metadata] + ); +} + +/** + * @internal + */ +export function useInputEditorSizeParams( + metadata: OldEditorMetadata +): InputEditorSizeParams | undefined { + return React.useMemo( + () => + metadata.params?.find( + (param) => + param.type === PropertyEditorParamTypes.InputEditorSize.valueOf() + ) as InputEditorSizeParams, + [metadata] + ); +} + +/** + * @internal + */ +export function useIconEditorParams( + metadata: OldEditorMetadata +): IconEditorParams | undefined { + return React.useMemo( + () => + metadata.params?.find( + (param) => param.type === PropertyEditorParamTypes.Icon.valueOf() + ) as IconEditorParams, + [metadata] + ); +} + +/** + * @internal + */ +export function useButtonGroupEditorParams( + metadata: OldEditorMetadata +): ButtonGroupEditorParams | undefined { + return React.useMemo( + () => + metadata.params?.find( + (param) => + param.type === PropertyEditorParamTypes.ButtonGroupData.valueOf() + ) as ButtonGroupEditorParams, + [metadata] + ); +} + +/** + * @internal + */ +export function useColorEditorParams( + metadata: OldEditorMetadata +): ColorEditorParams | undefined { + return React.useMemo( + () => + metadata.params?.find( + (param) => param.type === PropertyEditorParamTypes.ColorData.valueOf() + ) as ColorEditorParams, + [metadata] + ); +} + +/** + * @internal + */ +export function useRangeEditorParams( + metadata: OldEditorMetadata +): RangeEditorParams | undefined { + return React.useMemo( + () => + metadata.params?.find( + (param) => param.type === PropertyEditorParamTypes.Range.valueOf() + ) as RangeEditorParams, + [metadata] + ); +} + +/** + * @internal + */ +export function useSliderEditorParams( + metadata: OldEditorMetadata +): SliderEditorParams | undefined { + return React.useMemo( + () => + metadata.params?.find( + (param) => param.type === PropertyEditorParamTypes.Slider.valueOf() + ) as SliderEditorParams, + [metadata] + ); +} + +/* v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/interop/old-editors/UseEnumMetadata.tsx b/ui/components-react/src/components-react/new-editors/interop/old-editors/UseEnumMetadata.tsx new file mode 100644 index 00000000000..9fe35a3a4b2 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/interop/old-editors/UseEnumMetadata.tsx @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import * as React from "react"; +import type { OldEditorMetadata } from "../Metadata.js"; +import type { EnumValueMetadata } from "../../values/Metadata.js"; + +/* v8 ignore start */ + +/** + * Converts old enum metadata definition into the new one. It takes care of lazy loaded choices. + * @internal + */ +export function useEnumMetadata( + oldMetadata: OldEditorMetadata +): EnumValueMetadata { + const [metadata, setMetadata] = React.useState(() => ({ + type: "enum", + choices: [], + isStrict: false, + })); + + React.useEffect(() => { + let disposed = false; + const loadChoices = async () => { + const loadedChoices = + oldMetadata.enum === undefined + ? [] + : oldMetadata.enum.choices instanceof Promise + ? [...(await oldMetadata.enum.choices)] + : [...oldMetadata.enum.choices]; + + if (!disposed) { + setMetadata({ + type: "enum", + choices: loadedChoices, + isStrict: oldMetadata.enum?.isStrict ?? false, + }); + } + }; + + void loadChoices(); + + return () => { + disposed = true; + }; + }, [oldMetadata]); + + return metadata; +} + +/* v8 ignore stop */ diff --git a/ui/components-react/src/components-react/new-editors/values/Metadata.ts b/ui/components-react/src/components-react/new-editors/values/Metadata.ts new file mode 100644 index 00000000000..febeaaae4a6 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/values/Metadata.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +/** @packageDocumentation + * @module PropertyEditors + */ + +/** + * Value types supported by editor system. + * @beta + */ +export type ValueType = + | "string" + | "number" + | "bool" + | "date" + | "dateTime" + | "enum" + | "instanceKey"; + +/** + * Additional metadata that is used along side value. + * @beta + */ +export interface ValueMetadata { + type: ValueType; + preferredEditor?: string; + isNullable?: boolean; +} + +/** + * Type definition for available enum choice that can be supplied for editing enum values. + * @beta + */ +export interface EnumChoice { + value: number | string; + label: string; +} + +/** + * Additional metadata that is used along side enum value to determine applicable editor. + * @beta + */ +export interface EnumValueMetadata extends ValueMetadata { + type: "enum"; + choices: EnumChoice[]; + isStrict: boolean; +} diff --git a/ui/components-react/src/components-react/new-editors/values/Values.ts b/ui/components-react/src/components-react/new-editors/values/Values.ts new file mode 100644 index 00000000000..8efb496a810 --- /dev/null +++ b/ui/components-react/src/components-react/new-editors/values/Values.ts @@ -0,0 +1,124 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +/** @packageDocumentation + * @module PropertyEditors + */ + +import type { Id64String } from "@itwin/core-bentley"; + +/** + * Type definition for numeric value that can be handled by editor. + * @beta + */ +export interface NumericValue { + rawValue: number | undefined; + displayValue: string; + roundingError?: number; +} + +/** + * Type definition for instance key value that can be handled by editor. + * @beta + */ +export interface InstanceKeyValue { + key: { id: Id64String; className: string }; + label: string; +} + +/** + * Type definition for text value that can be handled by editor. + * @beta + */ +export interface TextValue { + value: string; +} + +/** + * Type definition for boolean value that can be handled by editor. + * @beta + */ +export interface BooleanValue { + value: boolean; +} + +/** + * Type definition for date value that can be handled by editor. + * @beta + */ +export interface DateValue { + value: Date; +} + +/** + * Type definition for enum value that can be handled by editor. + * @beta + */ +export interface EnumValue { + choice: number | string; +} + +/** + * Type definition for a value. + * @beta + */ +export type Value = + | NumericValue + | InstanceKeyValue + | TextValue + | BooleanValue + | DateValue + | EnumValue; + +/** @beta */ +// eslint-disable-next-line @typescript-eslint/no-redeclare +export namespace Value { + /** + * Type guard for text value. + * @beta + */ + export function isText(value: Value): value is TextValue { + return "value" in value && typeof value.value === "string"; + } + + /** + * Type guard for numeric value. + * @beta + */ + export function isNumeric(value: Value): value is NumericValue { + return "rawValue" in value && "displayValue" in value; + } + + /** + * Type guard for boolean value. + * @beta + */ + export function isBoolean(value: Value): value is BooleanValue { + return "value" in value && typeof value.value === "boolean"; + } + + /** + * Type guard for date value. + * @beta + */ + export function isDate(value: Value): value is DateValue { + return "value" in value && value.value instanceof Date; + } + + /** + * Type guard for enum value. + * @beta + */ + export function isEnum(value: Value): value is EnumValue { + return "choice" in value; + } + + /** + * Type guard for instance key value. + * @beta + */ + export function isInstanceKey(value: Value): value is InstanceKeyValue { + return "key" in value && "id" in value.key && "className" in value.key; + } +} 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 76b95ce6605..587f99cd797 100644 --- a/ui/components-react/src/components-react/propertygrid/component/VirtualizedPropertyGrid.tsx +++ b/ui/components-react/src/components-react/propertygrid/component/VirtualizedPropertyGrid.tsx @@ -69,6 +69,12 @@ export interface VirtualizedPropertyGridProps extends CommonPropertyGridProps { width: number; /** Height of the property grid component. */ height: number; + /** + * Specifies which editors system should be used: legacy or the new one. + * @default "legacy" + * @beta + */ + editorSystem?: "legacy" | "new"; } /** State of [[VirtualizedPropertyGrid]] React component @@ -125,6 +131,7 @@ export interface VirtualizedPropertyGridContext { category: PropertyCategory ) => void; onEditCancel?: () => void; + editorSystem: "legacy" | "new"; eventHandler: IPropertyGridEventHandler; dataProvider: IPropertyDataProvider; @@ -385,6 +392,7 @@ export class VirtualizedPropertyGrid extends React.Component< editingPropertyKey: selectionContext.editingPropertyKey, onEditCommit: selectionContext.onEditCommit, onEditCancel: selectionContext.onEditCancel, + editorSystem: this.props.editorSystem ?? "legacy", eventHandler: this.props.eventHandler, dataProvider: this.props.dataProvider, @@ -609,6 +617,7 @@ const FlatGridItemNode = React.memo( alwaysShowEditor={gridContext.alwaysShowEditor} onEditCommit={gridContext.onEditCommit} onEditCancel={gridContext.onEditCancel} + editorSystem={gridContext.editorSystem} isExpanded={node.isExpanded} onExpansionToggled={onExpansionToggled} onHeightChanged={onHeightChanged} diff --git a/ui/components-react/src/components-react/propertygrid/component/VirtualizedPropertyGridWithDataProvider.tsx b/ui/components-react/src/components-react/propertygrid/component/VirtualizedPropertyGridWithDataProvider.tsx index c4e866b8038..4de0a21dda4 100644 --- a/ui/components-react/src/components-react/propertygrid/component/VirtualizedPropertyGridWithDataProvider.tsx +++ b/ui/components-react/src/components-react/propertygrid/component/VirtualizedPropertyGridWithDataProvider.tsx @@ -38,6 +38,12 @@ export interface VirtualizedPropertyGridWithDataProviderProps width: number; /** Height of the property grid component. */ height: number; + /** + * Specifies which editors system should be used: legacy or the new one. + * @default "legacy" + * @beta + */ + editorSystem?: "legacy" | "new"; } /** @@ -63,6 +69,7 @@ export function VirtualizedPropertyGridWithDataProvider( {...props} model={model} eventHandler={eventHandler} + editorSystem={props.editorSystem ?? "legacy"} /> )} 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 35ab2acfd73..490aaa5071e 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 @@ -11,7 +11,6 @@ import type { PropertyRecord } from "@itwin/appui-abstract"; import { PropertyValueFormat } from "@itwin/appui-abstract"; import type { HighlightingComponentProps } from "../../../common/HighlightingComponentProps.js"; import type { PropertyUpdatedArgs } from "../../../editors/EditorContainer.js"; -import { EditorContainer } from "../../../editors/EditorContainer.js"; import { CommonPropertyRenderer } from "../../../properties/renderers/CommonPropertyRenderer.js"; import type { PrimitiveRendererProps } from "../../../properties/renderers/PrimitivePropertyRenderer.js"; import type { SharedRendererProps } from "../../../properties/renderers/PropertyRenderer.js"; @@ -20,6 +19,7 @@ import type { PropertyCategory } from "../../PropertyDataProvider.js"; import { FlatNonPrimitivePropertyRenderer } from "./FlatNonPrimitivePropertyRenderer.js"; import { CustomizablePropertyRenderer } from "../../../properties/renderers/CustomizablePropertyRenderer.js"; import { Orientation } from "../../../common/Orientation.js"; +import { PropertyRecordEditor } from "../../../new-editors/interop/PropertyRecordEditor.js"; /** Properties of [[FlatPropertyRenderer]] React component * @internal @@ -41,6 +41,8 @@ export interface FlatPropertyRendererProps extends SharedRendererProps { ) => void; /** Called when property edit is cancelled. */ onEditCancel?: () => void; + /** Used to switch between new and legacy editing system. */ + editorSystem?: "legacy" | "new"; /** Whether property value is displayed in expanded state. */ isExpanded: boolean; /** Called when toggling between expanded and collapsed property value display state. */ @@ -65,7 +67,9 @@ export const FlatPropertyRenderer: React.FC = ( const { propertyValueRendererManager, highlight, ...passthroughProps } = props; - const valueElementRenderer = () => ; + const valueElementRenderer = () => ( + + ); const primitiveRendererProps: PrimitiveRendererProps = { ...passthroughProps, @@ -140,6 +144,7 @@ interface DisplayValueProps { onClick?: (property: PropertyRecord, key?: string) => void; uniqueKey?: string; category?: PropertyCategory; + editorSystem: "legacy" | "new"; onEditCancel?: () => void; onEditCommit?: ( args: PropertyUpdatedArgs, @@ -172,12 +177,14 @@ const DisplayValue: React.FC = (props) => { }; return ( - {})} - setFocus={props.isEditing} onClick={() => props.onClick?.(props.propertyRecord, props.uniqueKey)} + setFocus={props.isEditing} + editorSystem={props.editorSystem} + size="small" /> ); } diff --git a/ui/components-react/src/test/new-editors/EditorsRegistryProvider.test.tsx b/ui/components-react/src/test/new-editors/EditorsRegistryProvider.test.tsx new file mode 100644 index 00000000000..e78427eb79d --- /dev/null +++ b/ui/components-react/src/test/new-editors/EditorsRegistryProvider.test.tsx @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import * as React from "react"; +import { describe, it } from "vitest"; +import { render, waitFor } from "@testing-library/react"; +import { createEditorSpec } from "../../components-react/new-editors/Types.js"; +import type { ValueMetadata } from "../../components-react/new-editors/values/Metadata.js"; +import type { + TextValue, + Value, +} from "../../components-react/new-editors/values/Values.js"; +import { EditorsRegistryProvider } from "../../components-react/new-editors/editors-registry/EditorsRegistryProvider.js"; +import { EditorRenderer } from "../../components-react/new-editors/EditorRenderer.js"; + +describe("EditorsRegistryProvider", () => { + const testMetadata: ValueMetadata = { + type: "string", + }; + const value: TextValue = { + value: "test", + }; + + it("should render using custom editor editor", async () => { + const editorsSpec = createEditorSpec({ + isMetadataSupported: (_metadata): _metadata is ValueMetadata => true, + isValueSupported: (_value): _value is Value => true, + Editor: () =>
Custom Editor
, + }); + + const rendered = render( + + {}} + /> + + ); + + await waitFor(() => rendered.getByText("Custom Editor")); + }); + + it("should render using higher priority custom editor editor", async () => { + const lowEditorsSpec = createEditorSpec({ + isMetadataSupported: (_metadata): _metadata is ValueMetadata => true, + isValueSupported: (_value): _value is Value => true, + Editor: () =>
Low priority Custom Editor
, + }); + + const highEditorsSpec = createEditorSpec({ + isMetadataSupported: (_metadata): _metadata is ValueMetadata => true, + isValueSupported: (_value): _value is Value => true, + Editor: () =>
High priority Custom Editor
, + }); + + const rendered = render( + + + {}} + /> + + + ); + + await waitFor(() => rendered.getByText("High priority Custom Editor")); + }); + + it("should render lower priority custom editor editor if no higher priority editors match", async () => { + const lowEditorsSpec = createEditorSpec({ + isMetadataSupported: (_metadata): _metadata is ValueMetadata => true, + isValueSupported: (_value): _value is Value => true, + Editor: () =>
Low priority Custom Editor
, + }); + + const highEditorsSpec = createEditorSpec({ + isMetadataSupported: (metadata): metadata is ValueMetadata => + metadata.type === "number", + isValueSupported: (_value): _value is Value => true, + Editor: () =>
High priority Custom Editor
, + }); + + const rendered = render( + + + {}} + /> + + + ); + + await waitFor(() => rendered.getByText("Low priority Custom Editor")); + }); +}); diff --git a/ui/components-react/src/test/new-editors/Values.test.ts b/ui/components-react/src/test/new-editors/Values.test.ts new file mode 100644 index 00000000000..bfe3b49d177 --- /dev/null +++ b/ui/components-react/src/test/new-editors/Values.test.ts @@ -0,0 +1,81 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import { describe, it } from "vitest"; +import type { + BooleanValue, + DateValue, + EnumValue, + InstanceKeyValue, + NumericValue, + TextValue, +} from "../../components-react/new-editors/values/Values.js"; +import { Value } from "../../components-react/new-editors/values/Values.js"; + +describe("Value", () => { + const textValue: TextValue = { value: "test" }; + const numericValue: NumericValue = { rawValue: 1, displayValue: "1" }; + const booleanValue: BooleanValue = { value: true }; + const dateValue: DateValue = { value: new Date() }; + const enumValue: EnumValue = { choice: 1 }; + const instanceKeyValue: InstanceKeyValue = { + key: { id: "0x1", className: "Schema.Class" }, + label: "Instance Label", + }; + + it("isText returns correct result", () => { + expect(Value.isText(textValue)).toBe(true); + expect(Value.isText(numericValue)).toBe(false); + expect(Value.isText(booleanValue)).toBe(false); + expect(Value.isText(dateValue)).toBe(false); + expect(Value.isText(enumValue)).toBe(false); + expect(Value.isText(instanceKeyValue)).toBe(false); + }); + + it("isNumeric returns correct result", () => { + expect(Value.isNumeric(textValue)).toBe(false); + expect(Value.isNumeric(numericValue)).toBe(true); + expect(Value.isNumeric(booleanValue)).toBe(false); + expect(Value.isNumeric(dateValue)).toBe(false); + expect(Value.isNumeric(enumValue)).toBe(false); + expect(Value.isNumeric(instanceKeyValue)).toBe(false); + }); + + it("isBoolean returns correct result", () => { + expect(Value.isBoolean(textValue)).toBe(false); + expect(Value.isBoolean(numericValue)).toBe(false); + expect(Value.isBoolean(booleanValue)).toBe(true); + expect(Value.isBoolean(dateValue)).toBe(false); + expect(Value.isBoolean(enumValue)).toBe(false); + expect(Value.isBoolean(instanceKeyValue)).toBe(false); + }); + + it("isDate returns correct result", () => { + expect(Value.isDate(textValue)).toBe(false); + expect(Value.isDate(numericValue)).toBe(false); + expect(Value.isDate(booleanValue)).toBe(false); + expect(Value.isDate(dateValue)).toBe(true); + expect(Value.isDate(enumValue)).toBe(false); + expect(Value.isDate(instanceKeyValue)).toBe(false); + }); + + it("isEnum returns correct result", () => { + expect(Value.isEnum(textValue)).toBe(false); + expect(Value.isEnum(numericValue)).toBe(false); + expect(Value.isEnum(booleanValue)).toBe(false); + expect(Value.isEnum(dateValue)).toBe(false); + expect(Value.isEnum(enumValue)).toBe(true); + expect(Value.isEnum(instanceKeyValue)).toBe(false); + }); + + it("isInstanceKey returns correct result", () => { + expect(Value.isInstanceKey(textValue)).toBe(false); + expect(Value.isInstanceKey(numericValue)).toBe(false); + expect(Value.isInstanceKey(booleanValue)).toBe(false); + expect(Value.isInstanceKey(dateValue)).toBe(false); + expect(Value.isInstanceKey(enumValue)).toBe(false); + expect(Value.isInstanceKey(instanceKeyValue)).toBe(true); + }); +}); diff --git a/ui/components-react/src/test/new-editors/interop/EditorInterop.test.ts b/ui/components-react/src/test/new-editors/interop/EditorInterop.test.ts new file mode 100644 index 00000000000..b339a9652af --- /dev/null +++ b/ui/components-react/src/test/new-editors/interop/EditorInterop.test.ts @@ -0,0 +1,399 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import { PropertyRecord, PropertyValueFormat } from "@itwin/appui-abstract"; +import { describe, it } from "vitest"; +import { EditorInterop } from "../../../components-react/new-editors/interop/EditorInterop.js"; +import type { OldEditorMetadata } from "../../../components-react/new-editors/interop/Metadata.js"; +import type { + BooleanValue, + DateValue, + EnumValue, + InstanceKeyValue, + NumericValue, + TextValue, +} from "../../../components-react/new-editors/values/Values.js"; + +describe("EditorInterop", () => { + describe("converts PropertyRecord to ValueMetadata and Value of type", () => { + it("string", () => { + const record = new PropertyRecord( + { + valueFormat: PropertyValueFormat.Primitive, + value: "test", + }, + { + name: "TestProp", + typename: "string", + displayLabel: "Test Property", + } + ); + + const { metadata, value } = EditorInterop.getMetadataAndValue(record); + expect(metadata).toMatchObject({ + type: "string", + typename: "string", + } satisfies OldEditorMetadata); + + expect(value).toMatchObject({ + value: "test", + } satisfies TextValue); + }); + + it("text", () => { + const record = new PropertyRecord( + { + valueFormat: PropertyValueFormat.Primitive, + value: "test", + }, + { + name: "TestProp", + typename: "text", + displayLabel: "Test Property", + } + ); + + const { metadata, value } = EditorInterop.getMetadataAndValue(record); + expect(metadata).toMatchObject({ + type: "string", + typename: "text", + } satisfies OldEditorMetadata); + + expect(value).toMatchObject({ + value: "test", + } satisfies TextValue); + }); + + it("number", () => { + const record = new PropertyRecord( + { + valueFormat: PropertyValueFormat.Primitive, + value: 1, + displayValue: "1", + }, + { + name: "TestProp", + typename: "number", + displayLabel: "Test Property", + } + ); + + const { metadata, value } = EditorInterop.getMetadataAndValue(record); + expect(metadata).toMatchObject({ + type: "number", + typename: "number", + } satisfies OldEditorMetadata); + + expect(value).toMatchObject({ + rawValue: 1, + displayValue: "1", + } satisfies NumericValue); + }); + + it("float", () => { + const record = new PropertyRecord( + { + valueFormat: PropertyValueFormat.Primitive, + value: 1.23, + displayValue: "1.23", + }, + { + name: "TestProp", + typename: "float", + displayLabel: "Test Property", + } + ); + + const { metadata, value } = EditorInterop.getMetadataAndValue(record); + expect(metadata).toMatchObject({ + type: "number", + typename: "float", + } satisfies OldEditorMetadata); + + expect(value).toMatchObject({ + rawValue: 1.23, + displayValue: "1.23", + } satisfies NumericValue); + }); + + it("double", () => { + const record = new PropertyRecord( + { + valueFormat: PropertyValueFormat.Primitive, + value: 2.34, + displayValue: "2.34", + }, + { + name: "TestProp", + typename: "double", + displayLabel: "Test Property", + } + ); + + const { metadata, value } = EditorInterop.getMetadataAndValue(record); + expect(metadata).toMatchObject({ + type: "number", + typename: "double", + } satisfies OldEditorMetadata); + + expect(value).toMatchObject({ + rawValue: 2.34, + displayValue: "2.34", + } satisfies NumericValue); + }); + + it("int", () => { + const record = new PropertyRecord( + { + valueFormat: PropertyValueFormat.Primitive, + value: 123, + displayValue: "123", + }, + { + name: "TestProp", + typename: "int", + displayLabel: "Test Property", + } + ); + + const { metadata, value } = EditorInterop.getMetadataAndValue(record); + expect(metadata).toMatchObject({ + type: "number", + typename: "int", + } satisfies OldEditorMetadata); + + expect(value).toMatchObject({ + rawValue: 123, + displayValue: "123", + } satisfies NumericValue); + }); + + it("integer", () => { + const record = new PropertyRecord( + { + valueFormat: PropertyValueFormat.Primitive, + value: 456, + displayValue: "456", + }, + { + name: "TestProp", + typename: "integer", + displayLabel: "Test Property", + } + ); + + const { metadata, value } = EditorInterop.getMetadataAndValue(record); + expect(metadata).toMatchObject({ + type: "number", + typename: "integer", + } satisfies OldEditorMetadata); + + expect(value).toMatchObject({ + rawValue: 456, + displayValue: "456", + } satisfies NumericValue); + }); + + it("boolean", () => { + const record = new PropertyRecord( + { + valueFormat: PropertyValueFormat.Primitive, + value: true, + }, + { + name: "TestProp", + typename: "boolean", + displayLabel: "Test Property", + } + ); + + const { metadata, value } = EditorInterop.getMetadataAndValue(record); + expect(metadata).toMatchObject({ + type: "bool", + typename: "boolean", + } satisfies OldEditorMetadata); + + expect(value).toMatchObject({ + value: true, + } satisfies BooleanValue); + }); + + it("bool", () => { + const record = new PropertyRecord( + { + valueFormat: PropertyValueFormat.Primitive, + value: true, + }, + { + name: "TestProp", + typename: "bool", + displayLabel: "Test Property", + } + ); + + const { metadata, value } = EditorInterop.getMetadataAndValue(record); + expect(metadata).toMatchObject({ + type: "bool", + typename: "bool", + } satisfies OldEditorMetadata); + + expect(value).toMatchObject({ + value: true, + } satisfies BooleanValue); + }); + + it("dateTime", () => { + const date = new Date(); + const record = new PropertyRecord( + { + valueFormat: PropertyValueFormat.Primitive, + value: date, + }, + { + name: "TestProp", + typename: "dateTime", + displayLabel: "Test Property", + } + ); + + const { metadata, value } = EditorInterop.getMetadataAndValue(record); + expect(metadata).toMatchObject({ + type: "dateTime", + typename: "dateTime", + } satisfies OldEditorMetadata); + + expect(value).toMatchObject({ + value: date, + } satisfies DateValue); + }); + + it("shortdate", () => { + const date = new Date(); + const record = new PropertyRecord( + { + valueFormat: PropertyValueFormat.Primitive, + value: date, + }, + { + name: "TestProp", + typename: "shortdate", + displayLabel: "Test Property", + } + ); + + const { metadata, value } = EditorInterop.getMetadataAndValue(record); + expect(metadata).toMatchObject({ + type: "date", + typename: "shortdate", + } satisfies OldEditorMetadata); + + expect(value).toMatchObject({ + value: date, + } satisfies DateValue); + }); + + it("enum", () => { + const record = new PropertyRecord( + { + valueFormat: PropertyValueFormat.Primitive, + value: 1, + }, + { + name: "TestProp", + typename: "enum", + displayLabel: "Test Property", + } + ); + + const { metadata, value } = EditorInterop.getMetadataAndValue(record); + expect(metadata).toMatchObject({ + type: "enum", + typename: "enum", + } satisfies OldEditorMetadata); + + expect(value).toMatchObject({ + choice: 1, + } satisfies EnumValue); + }); + + it("navigation", () => { + const record = new PropertyRecord( + { + valueFormat: PropertyValueFormat.Primitive, + value: { id: "1", className: "TestClass" }, + displayValue: "Test Navigation", + }, + { + name: "TestProp", + typename: "navigation", + displayLabel: "Test Property", + } + ); + + const { metadata, value } = EditorInterop.getMetadataAndValue(record); + expect(metadata).toMatchObject({ + type: "instanceKey", + typename: "navigation", + } satisfies OldEditorMetadata); + + expect(value).toMatchObject({ + key: { id: "1", className: "TestClass" }, + label: "Test Navigation", + } satisfies InstanceKeyValue); + }); + }); + + describe("convertToPrimitiveValue converts Value to PrimitiveValue for type", () => { + it("string", () => { + const value = { value: "test" } satisfies TextValue; + const primitiveValue = EditorInterop.convertToPrimitiveValue(value); + expect(primitiveValue).toMatchObject({ + valueFormat: PropertyValueFormat.Primitive, + value: "test", + displayValue: "test", + }); + }); + + it("number", () => { + const value = { rawValue: 1, displayValue: "1" } satisfies NumericValue; + const primitiveValue = EditorInterop.convertToPrimitiveValue(value); + expect(primitiveValue).toMatchObject({ + valueFormat: PropertyValueFormat.Primitive, + value: 1, + displayValue: "1", + }); + }); + + it("boolean", () => { + const value = { value: true } satisfies BooleanValue; + const primitiveValue = EditorInterop.convertToPrimitiveValue(value); + expect(primitiveValue).toMatchObject({ + valueFormat: PropertyValueFormat.Primitive, + value: true, + displayValue: "true", + }); + }); + + it("date", () => { + const date = new Date(); + const value = { value: date } satisfies DateValue; + const primitiveValue = EditorInterop.convertToPrimitiveValue(value); + expect(primitiveValue).toMatchObject({ + valueFormat: PropertyValueFormat.Primitive, + value: date, + displayValue: date.toString(), + }); + }); + + it("enum", () => { + const value = { choice: 1 } satisfies EnumValue; + const primitiveValue = EditorInterop.convertToPrimitiveValue(value); + expect(primitiveValue).toMatchObject({ + valueFormat: PropertyValueFormat.Primitive, + value: 1, + }); + }); + }); +}); diff --git a/ui/components-react/src/test/new-editors/interop/PropertyRecordEditor.test.tsx b/ui/components-react/src/test/new-editors/interop/PropertyRecordEditor.test.tsx new file mode 100644 index 00000000000..0632c997c46 --- /dev/null +++ b/ui/components-react/src/test/new-editors/interop/PropertyRecordEditor.test.tsx @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import * as React from "react"; +import { describe, it } from "vitest"; +import { render, waitFor } from "@testing-library/react"; +import { PropertyRecordEditor } from "../../../components-react.js"; +import { PropertyRecord } from "@itwin/appui-abstract"; + +describe("PropertyRecordEditor", () => { + const propertyRecord = PropertyRecord.fromString("test"); + + it("should render using old editor system by default", async () => { + const rendered = render( + {}} + onCancel={() => {}} + /> + ); + + await waitFor(() => rendered.getByDisplayValue("test")); + expect( + rendered.container.querySelector(".components-editor-container") + ).not.toBeNull(); + }); + + it("should render using new editor system", async () => { + const rendered = render( + {}} + onCancel={() => {}} + editorSystem="new" + /> + ); + + await waitFor(() => rendered.getByDisplayValue("test")); + expect( + rendered.container.querySelector(".components-editor-container") + ).toBeNull(); + }); +}); diff --git a/ui/imodel-components-react/src/imodel-components-react.ts b/ui/imodel-components-react/src/imodel-components-react.ts index 527e16d0c9b..431bdb9fba3 100644 --- a/ui/imodel-components-react/src/imodel-components-react.ts +++ b/ui/imodel-components-react/src/imodel-components-react.ts @@ -43,6 +43,7 @@ export { WeightEditor, WeightPropertyEditor, } from "./imodel-components-react/editors/WeightEditor.js"; +export { WeightEditorSpec } from "./imodel-components-react/editors/NewWeightEditor.js"; export { QuantityInput, @@ -53,6 +54,7 @@ export { QuantityNumberInputProps, StepFunctionProp, } from "./imodel-components-react/inputs/QuantityNumberInput.js"; +export { QuantityEditorSpec } from "./imodel-components-react/inputs/new-editors/QuantityEditor.js"; export { LineWeightSwatch, diff --git a/ui/imodel-components-react/src/imodel-components-react/editors/NewWeightEditor.tsx b/ui/imodel-components-react/src/imodel-components-react/editors/NewWeightEditor.tsx new file mode 100644 index 00000000000..36f2e61cc1d --- /dev/null +++ b/ui/imodel-components-react/src/imodel-components-react/editors/NewWeightEditor.tsx @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +/** @packageDocumentation + * @module PropertyEditors + */ + +import * as React from "react"; +import { StandardEditorNames } from "@itwin/appui-abstract"; +import { WeightPickerButton } from "../lineweight/WeightPickerButton.js"; +import type { ValueMetadata } from "@itwin/components-react"; +import { + createEditorSpec, + type EditorProps, + type EditorSpec, + type NumericValue, + Value, +} from "@itwin/components-react"; + +/* v8 ignore start */ + +/** + * Editor specification for weight editor. + * @beta + */ +export const WeightEditorSpec: EditorSpec = createEditorSpec({ + isMetadataSupported: (metadata): metadata is ValueMetadata => + metadata.type === "number" && + metadata.preferredEditor === StandardEditorNames.WeightPicker, + isValueSupported: Value.isNumeric, + Editor: WeightEditor, +}); + +function WeightEditor({ + value, + onChange, + commit, +}: EditorProps) { + const currentValue = value ? value : { rawValue: 0, displayValue: "" }; + + return ( + { + const newValue = { rawValue: newWeight, displayValue: "" }; + onChange(newValue); + commit?.(); + }} + /> + ); +} + +/* v8 ignore stop */ diff --git a/ui/imodel-components-react/src/imodel-components-react/inputs/new-editors/QuantityEditor.tsx b/ui/imodel-components-react/src/imodel-components-react/inputs/new-editors/QuantityEditor.tsx new file mode 100644 index 00000000000..509953e412f --- /dev/null +++ b/ui/imodel-components-react/src/imodel-components-react/inputs/new-editors/QuantityEditor.tsx @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +/** @packageDocumentation + * @module PropertyEditors + */ + +import * as React from "react"; +import { type QuantityTypeArg } from "@itwin/core-frontend"; +import { useQuantityInfo } from "./UseQuantityInfo.js"; +import { + createEditorSpec, + type EditorProps, + type EditorSpec, + type NumericValue, + Value, + type ValueMetadata, +} from "@itwin/components-react"; +import { QuantityInput } from "./QuantityInput.js"; + +/* v8 ignore start */ + +/** + * Editor specification for quantity values based on `IModelApp.quantityFormatter`. + * @beta + */ +export const QuantityEditorSpec: EditorSpec = createEditorSpec({ + isMetadataSupported: (metadata): metadata is QuantityValueMetadata => + metadata.type === "number" && + "quantityType" in metadata && + metadata.quantityType !== undefined, + isValueSupported: Value.isNumeric, + Editor: QuantityEditor, +}); + +/** + * Metadata for quantity values. + * @beta + */ +export interface QuantityValueMetadata extends ValueMetadata { + type: "number"; + quantityType: QuantityTypeArg; +} + +function QuantityEditor({ + metadata, + value, + onChange, + size, +}: EditorProps) { + const { formatter, parser } = useQuantityInfo({ + type: metadata.quantityType, + }); + const currentValue = value + ? value + : { rawValue: undefined, displayValue: "" }; + + return ( + + ); +} + +/* v8 ignore stop */ diff --git a/ui/imodel-components-react/src/imodel-components-react/inputs/new-editors/QuantityInput.tsx b/ui/imodel-components-react/src/imodel-components-react/inputs/new-editors/QuantityInput.tsx new file mode 100644 index 00000000000..7db8e40e404 --- /dev/null +++ b/ui/imodel-components-react/src/imodel-components-react/inputs/new-editors/QuantityInput.tsx @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import * as React from "react"; +import { FormattedNumericInput } from "@itwin/components-react"; +import type { FormatterSpec, ParserSpec } from "@itwin/core-quantity"; + +/* v8 ignore start */ + +type FormattedNumericInputProps = React.ComponentPropsWithoutRef< + typeof FormattedNumericInput +>; + +interface QuantityInputProps + extends Omit< + FormattedNumericInputProps, + "parseValue" | "formatValue" | "disabled" + > { + formatter?: FormatterSpec; + parser?: ParserSpec; +} + +/** + * A component that wraps `FormattedNumericInput` and takes a formatter and parser for quantity values. + * @internal + */ +export function QuantityInput({ + formatter, + parser, + ...props +}: QuantityInputProps) { + const initialValue = React.useRef(props.value); + const formatValue = React.useCallback( + (currValue: number) => { + if (!formatter) { + return initialValue.current.displayValue; + } + return formatter.applyFormatting(currValue); + }, + [formatter] + ); + + const parseString = React.useCallback( + (userInput: string) => { + if (!parser) { + return undefined; + } + const parseResult = parser.parseToQuantityValue(userInput); + return parseResult.ok ? parseResult.value : undefined; + }, + [parser] + ); + + return ( + + ); +} + +/* v8 ignore stop */ diff --git a/ui/imodel-components-react/src/imodel-components-react/inputs/new-editors/UseQuantityInfo.ts b/ui/imodel-components-react/src/imodel-components-react/inputs/new-editors/UseQuantityInfo.ts new file mode 100644 index 00000000000..3c3c8df67e6 --- /dev/null +++ b/ui/imodel-components-react/src/imodel-components-react/inputs/new-editors/UseQuantityInfo.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import type { QuantityTypeArg } from "@itwin/core-frontend"; +import { IModelApp } from "@itwin/core-frontend"; +import type { FormatterSpec, ParserSpec } from "@itwin/core-quantity"; +import * as React from "react"; + +/* v8 ignore start */ + +interface UseQuantityInfoProps { + type: QuantityTypeArg | undefined; +} + +/** + * Hook that finds the formatter and parser in `IModelApp.quantityFormatter` for a given quantity type. + * @internal + */ +export function useQuantityInfo({ type }: UseQuantityInfoProps) { + const [{ formatter, parser }, setState] = React.useState<{ + formatter: FormatterSpec | undefined; + parser: ParserSpec | undefined; + }>(() => ({ + formatter: undefined, + parser: undefined, + })); + + React.useEffect(() => { + if (type === undefined) { + setState({ + formatter: undefined, + parser: undefined, + }); + return; + } + + const loadFormatterParser = () => { + const formatterSpec = + IModelApp.quantityFormatter.findFormatterSpecByQuantityType(type); + const parserSpec = + IModelApp.quantityFormatter.findParserSpecByQuantityType(type); + + setState({ formatter: formatterSpec, parser: parserSpec }); + }; + + loadFormatterParser(); + const removeListeners = [ + IModelApp.quantityFormatter.onActiveFormattingUnitSystemChanged.addListener( + loadFormatterParser + ), + IModelApp.quantityFormatter.onQuantityFormatsChanged.addListener( + ({ quantityType }) => { + if (quantityType === type) { + loadFormatterParser(); + } + } + ), + ]; + + return () => { + removeListeners.forEach((remove) => { + remove(); + }); + }; + }, [type]); + + return { formatter, parser }; +} + +/* v8 ignore stop */