{})}
- 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 */