diff --git a/packages/dev/inspector-v2/src/components/properties/animation/animationGroupProperties.tsx b/packages/dev/inspector-v2/src/components/properties/animation/animationGroupProperties.tsx index 37a7829b7eb..2783098cdc3 100644 --- a/packages/dev/inspector-v2/src/components/properties/animation/animationGroupProperties.tsx +++ b/packages/dev/inspector-v2/src/components/properties/animation/animationGroupProperties.tsx @@ -78,8 +78,7 @@ export const AnimationGroupControlProperties: FunctionComponent<{ animationGroup /> - - {/* TODO: Hey georgie : should be integer (even when typing)*/} + diff --git a/packages/dev/sharedUiComponents/src/fluent/hoc/propertyLines/inputPropertyLine.tsx b/packages/dev/sharedUiComponents/src/fluent/hoc/propertyLines/inputPropertyLine.tsx index 6c92b3e30af..ea5e86d9248 100644 --- a/packages/dev/sharedUiComponents/src/fluent/hoc/propertyLines/inputPropertyLine.tsx +++ b/packages/dev/sharedUiComponents/src/fluent/hoc/propertyLines/inputPropertyLine.tsx @@ -1,15 +1,16 @@ import { PropertyLine } from "./propertyLine"; import type { PropertyLineProps } from "./propertyLine"; import type { FunctionComponent } from "react"; -import { NumberInput, TextInput } from "../../primitives/input"; -import type { InputProps } from "../../primitives/input"; - +import type { TextInputProps } from "../../primitives/textInput"; +import { TextInput } from "../../primitives/textInput"; +import type { SpinButtonProps } from "../../primitives/spinButton"; +import { SpinButton } from "../../primitives/spinButton"; /** * Wraps a text input in a property line * @param props - PropertyLineProps and InputProps * @returns property-line wrapped input component */ -export const TextInputPropertyLine: FunctionComponent & PropertyLineProps> = (props) => ( +export const TextInputPropertyLine: FunctionComponent> = (props) => ( @@ -17,11 +18,14 @@ export const TextInputPropertyLine: FunctionComponent & Prope /** * Wraps a number input in a property line + * To force integer values, use forceInt param (this is distinct from the 'step' param, which will still allow submitting an integer value. forceInt will not) * @param props - PropertyLineProps and InputProps * @returns property-line wrapped input component */ -export const NumberInputPropertyLine: FunctionComponent & PropertyLineProps> = (props) => ( - - - -); +export const NumberInputPropertyLine: FunctionComponent & { forceInt?: boolean }> = (props) => { + return ( + + + + ); +}; diff --git a/packages/dev/sharedUiComponents/src/fluent/hoc/propertyLines/vectorPropertyLine.tsx b/packages/dev/sharedUiComponents/src/fluent/hoc/propertyLines/vectorPropertyLine.tsx index 74fad661b87..9d4e8e989aa 100644 --- a/packages/dev/sharedUiComponents/src/fluent/hoc/propertyLines/vectorPropertyLine.tsx +++ b/packages/dev/sharedUiComponents/src/fluent/hoc/propertyLines/vectorPropertyLine.tsx @@ -21,6 +21,10 @@ export type TensorPropertyLineProps - onChange(val, "x")} /> - onChange(val, "y")} /> - {HasZ(vector) && onChange(val, "z")} />} - {HasW(vector) && onChange(val, "w")} />} + onChange(val, "x")} unit={props.unit} /> + onChange(val, "y")} unit={props.unit} /> + {HasZ(vector) && ( + onChange(val, "z")} unit={props.unit} /> + )} + {HasW(vector) && ( + onChange(val, "w")} unit={props.unit} /> + )} } > @@ -89,7 +97,7 @@ const ToDegreesConverter = { from: Tools.ToDegrees, to: Tools.ToRadians }; export const RotationVectorPropertyLine: FunctionComponent = (props) => { const min = props.useDegrees ? 0 : undefined; const max = props.useDegrees ? 360 : undefined; - return ; + return ; }; type QuaternionPropertyLineProps = TensorPropertyLineProps & { @@ -118,7 +126,7 @@ export const QuaternionPropertyLine: FunctionComponent ) : ( - + ); }; export const Vector2PropertyLine = TensorPropertyLine as FunctionComponent>; diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/colorPicker.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/colorPicker.tsx index 103796eb6eb..9932841e8c8 100644 --- a/packages/dev/sharedUiComponents/src/fluent/primitives/colorPicker.tsx +++ b/packages/dev/sharedUiComponents/src/fluent/primitives/colorPicker.tsx @@ -1,17 +1,12 @@ /* eslint-disable jsdoc/require-returns */ /* eslint-disable @typescript-eslint/naming-convention */ import { useState, useEffect, useCallback } from "react"; -import type { FunctionComponent, ChangeEvent } from "react"; +import type { FunctionComponent } from "react"; import { - Input, - Label, - SpinButton, - useId, - ColorPicker, + ColorPicker as FluentColorPicker, ColorSlider, ColorArea, AlphaSlider, - InfoLabel, Link, makeStyles, Popover, @@ -21,9 +16,11 @@ import { Body1Strong, ColorSwatch, } from "@fluentui/react-components"; -import type { SpinButtonChangeEvent, SpinButtonOnChangeData, ColorPickerProps as FluentColorPickerProps, InputOnChangeData } from "@fluentui/react-components"; +import type { ColorPickerProps as FluentColorPickerProps } from "@fluentui/react-components"; import { Color3, Color4 } from "core/Maths/math.color"; import type { PrimitiveProps } from "./primitive"; +import { SpinButton } from "./spinButton"; +import { TextInput } from "./textInput"; const useColorPickerStyles = makeStyles({ colorPickerContainer: { @@ -57,7 +54,7 @@ const useColorPickerStyles = makeStyles({ width: "80px", }, spinButton: { - minWidth: "60px", + width: "50px", }, container: { display: "flex", @@ -110,32 +107,32 @@ export const ColorPickerPopup: FunctionComponent
- + {color instanceof Color4 && } - +
{/* Top Row: Preview, Gamma Hex, Linear Hex */}
- - + +
{/* Middle Row: Red, Green, Blue, Alpha */}
- - - + + +
{/* Bottom Row: Hue, Saturation, Value */}
- - - + + +
@@ -144,9 +141,9 @@ export const ColorPickerPopup: FunctionComponent & { - label?: string; linearHex?: boolean; isLinearMode?: boolean; }; @@ -159,102 +156,93 @@ export type InputHexProps = PrimitiveProps & { * @returns */ export const InputHexField: FunctionComponent = (props) => { - const id = useId("hex-input"); const styles = useColorPickerStyles(); - const { label, value, onChange, linearHex, isLinearMode } = props; + const { title, value, onChange, linearHex, isLinearMode } = props; - const handleChange = (e: ChangeEvent, _: InputOnChangeData) => { - // If linearHint (aka PBR material, ensure the other values are displayed in gamma even if linear hex changes) - const value = e.target.value; - if (value != "" && /^[0-9A-Fa-f]+$/g.test(value) == false) { - return; - } - onChange(Color3.FromHexString(value).toGammaSpace()); - }; return (
- {props.linearHex ? ( - This color picker is attached to an entity whose color is stored in gamma space, so we are showing linear hex in disabled view - ) : ( - <> - This color picker is attached to an entity whose color is stored in linear space (ex: PBR Material), and Babylon converts the color to gamma space - before rendering on screen because the human eye is best at processing colors in gamma space. We thus also want to display the color picker in gamma - space so that the color chosen here will match the color seen in your entity. -
- If you want to copy/paste the HEX into your code, you can either use - Color3.FromHexString(LINEAR_HEX) -
- or -
- Color3.FromHexString(GAMMA_HEX).toLinearSpace() -
-
- Read more in our docs! - - ) - } - > - {label} -
- ) : ( - - )} - val != "" && HEX_REGEX.test(val)} + onChange={(val) => (linearHex ? onChange(Color3.FromHexString(val).toGammaSpace()) : onChange(Color3.FromHexString(val)))} + infoLabel={ + title + ? { + label: title, + // If not representing a linearHex, no info is needed. + info: !props.linearHex ? undefined : !isLinearMode ? ( // If representing a linear hex but we are in gammaMode, simple message explaining why linearHex is disabled + <> This color picker is attached to an entity whose color is stored in gamma space, so we are showing linear hex in disabled view + ) : ( + // If representing a linear hex and we are in linearMode, give information about how to use these hex values + <> + This color picker is attached to an entity whose color is stored in linear space (ex: PBR Material), and Babylon converts the color to gamma + space before rendering on screen because the human eye is best at processing colors in gamma space. We thus also want to display the color + picker in gamma space so that the color chosen here will match the color seen in your entity. +
+ If you want to copy/paste the HEX into your code, you can either use + Color3.FromHexString(LINEAR_HEX) +
+ or +
+ Color3.FromHexString(GAMMA_HEX).toLinearSpace() +
+
+ Read more in our docs! + + ), + } + : undefined + } />
); }; type RgbKey = "r" | "g" | "b"; -type InputRgbFieldProps = { - color: Color3 | Color4; - label: string; +type InputRgbFieldProps = PrimitiveProps & { rgbKey: RgbKey; - onChange: (color: Color3 | Color4) => void; }; const InputRgbField: FunctionComponent = (props) => { - const { color, onChange, label, rgbKey } = props; - const id = useId(`${label.toLowerCase()}-input`); + const { value, onChange, title, rgbKey } = props; const classes = useColorPickerStyles(); const handleChange = useCallback( - (_: SpinButtonChangeEvent, data: SpinButtonOnChangeData) => { - const val = data.value ?? parseFloat(data.displayValue ?? ""); - - if (val === null || Number.isNaN(val) || !NUMBER_REGEX.test(val.toString())) { - return; - } - - const newColor = color.clone(); + (val: number) => { + const newColor = value.clone(); newColor[rgbKey] = val / 255.0; // Convert to 0-1 range onChange(newColor); }, - [color] + [value, onChange, rgbKey] ); return (
- - +
); }; -type InputHsvFieldProps = { - color: Color3 | Color4; - label: string; +function rgbaToHsv(color: { r: number; g: number; b: number; a?: number }): { h: number; s: number; v: number; a?: number } { + const c = new Color3(color.r, color.g, color.b); + const hsv = c.toHSV(); + return { h: hsv.r, s: hsv.g, v: hsv.b, a: color.a }; +} + +type HsvKey = "h" | "s" | "v"; +type InputHsvFieldProps = PrimitiveProps & { hsvKey: HsvKey; - onChange: (color: Color3 | Color4) => void; max: number; scale?: number; }; @@ -266,35 +254,36 @@ type InputHsvFieldProps = { * @param props - The properties for the InputHsvField component. */ export const InputHsvField: FunctionComponent = (props) => { - const { color, label, hsvKey, max, scale = 1 } = props; + const { value, title, hsvKey, max, onChange, scale = 1 } = props; - const id = useId(`${label.toLowerCase()}-input`); const classes = useColorPickerStyles(); const handleChange = useCallback( - (_: SpinButtonChangeEvent, data: SpinButtonOnChangeData) => { - const val = data.value ?? parseFloat(data.displayValue ?? ""); - - if (val === null || Number.isNaN(val) || !NUMBER_REGEX.test(val.toString())) { - return; - } - + (val: number) => { // Convert current color to HSV, update the new hsv value, then call onChange prop - const hsv = rgbaToHsv(color); + const hsv = rgbaToHsv(value); hsv[hsvKey] = val / scale; let newColor: Color3 | Color4 = Color3.FromHSV(hsv.h, hsv.s, hsv.v); - if (color instanceof Color4) { - newColor = Color4.FromColor3(newColor, color.a ?? 1); + if (value instanceof Color4) { + newColor = Color4.FromColor3(newColor, value.a ?? 1); } props.onChange(newColor); }, - [color] + [value, onChange, hsvKey, scale] ); return (
- - +
); }; @@ -311,41 +300,27 @@ type InputAlphaProps = { */ const InputAlphaField: FunctionComponent = (props) => { const classes = useColorPickerStyles(); - const id = useId("alpha-input"); - const { color } = props; + const { color, onChange } = props; - const onChange = (_: SpinButtonChangeEvent, data: SpinButtonOnChangeData) => { - const value = data.value ?? parseFloat(data.displayValue ?? ""); - - if (Number.isNaN(value) || value < 0 || value > 1) { - return; - } + const handleChange = useCallback( + (value: number) => { + if (Number.isNaN(value) || value < 0 || value > 1) { + return; + } - if (color instanceof Color4) { - const newColor = color.clone(); - newColor.a = value; - return newColor; - } else { - return Color4.FromColor3(color, value); - } - }; + if (color instanceof Color4) { + const newColor = color.clone(); + newColor.a = value; + return newColor; + } else { + return Color4.FromColor3(color, value); + } + }, + [onChange] + ); return (
-
- - {color instanceof Color3 && ( - - Because this color picker is representing a Color3, we do not permit modifying alpha from the color picker. You can however modify the material's - alpha property directly, either in code via material.alpha OR via inspector's transparency section. - - } - > - )} -
= (props) => { className={classes.spinButton} value={color instanceof Color3 ? 1 : color.a} step={0.01} - onChange={onChange} - id={id} + onChange={handleChange} + infoLabel={{ + label: "Alpha", + info: + color instanceof Color3 ? ( + <> + Because this color picker is representing a Color3, we do not permit modifying alpha from the color picker. You can however modify the entity's + alpha property directly, either in code via entity.alpha OR via inspector's transparency section. + + ) : undefined, + }} />
); }; - -const NUMBER_REGEX = /^\d+$/; - -function rgbaToHsv(color: { r: number; g: number; b: number; a?: number }): { h: number; s: number; v: number; a?: number } { - const c = new Color3(color.r, color.g, color.b); - const hsv = c.toHSV(); - return { h: hsv.r, s: hsv.g, v: hsv.b, a: color.a }; -} diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/infoLabel.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/infoLabel.tsx new file mode 100644 index 00000000000..457120054cd --- /dev/null +++ b/packages/dev/sharedUiComponents/src/fluent/primitives/infoLabel.tsx @@ -0,0 +1,22 @@ +import type { FunctionComponent } from "react"; +import { InfoLabel as FluentInfoLabel } from "@fluentui/react-components"; + +export type InfoLabelProps = { + htmlFor: string; // required ID of the element whose label we are applying + info?: JSX.Element; + label: string; +}; +export type InfoLabelParentProps = Omit; + +/** + * Renders a label with an optional popup containing more info + * @param props + * @returns + */ +export const InfoLabel: FunctionComponent = (props) => { + return ( + + {props.label} + + ); +}; diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/input.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/input.tsx deleted file mode 100644 index 5eccd89cdde..00000000000 --- a/packages/dev/sharedUiComponents/src/fluent/primitives/input.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import type { FunctionComponent, KeyboardEvent, ChangeEvent } from "react"; -import { useEffect, useState } from "react"; - -import { Input as FluentInput, makeStyles } from "@fluentui/react-components"; -import type { PrimitiveProps } from "./primitive"; - -const useInputStyles = makeStyles({ - text: { - height: "auto", - textAlign: "right", - minWidth: "100px", // Min width for text input - }, - number: { - height: "auto", - minWidth: "40px", // Min width for number input - }, -}); - -export type InputProps = PrimitiveProps & { - step?: number; - placeholder?: string; - min?: number; - max?: number; -}; -/** - * This is an input text box that stops propagation of change events and sets its width based on the type of input (text or number) - * @param props - * @returns - */ -const Input: FunctionComponent & { type: "text" | "number" }> = (props) => { - const classes = useInputStyles(); - const [value, setValue] = useState(props.value ?? ""); - - useEffect(() => { - setValue(props.value ?? ""); // Update local state when props.value changes - }, [props.value]); - - const handleChange = (event: ChangeEvent, _: unknown) => { - event.stopPropagation(); // Prevent event propagation - const value = props.type === "number" ? Number(event.target.value) : String(event.target.value); - props.onChange(value); // Call the original onChange handler passed as prop - setValue(value); // Update local state with the new value - }; - - const handleKeyDown = (event: KeyboardEvent) => { - event.stopPropagation(); // Prevent event propagation - }; - - return ( - - ); -}; - -const NumberInputCast = Input as FunctionComponent & { type: "number" }>; -const TextInputCast = Input as FunctionComponent & { type: "text" }>; - -export const NumberInput: FunctionComponent> = (props) => ; -export const TextInput: FunctionComponent> = (props) => ; diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/primitive.ts b/packages/dev/sharedUiComponents/src/fluent/primitives/primitive.ts index 29ac8bb876b..dd0dbaec144 100644 --- a/packages/dev/sharedUiComponents/src/fluent/primitives/primitive.ts +++ b/packages/dev/sharedUiComponents/src/fluent/primitives/primitive.ts @@ -1,3 +1,5 @@ +import type { InfoLabelParentProps } from "./infoLabel"; + export type ImmutablePrimitiveProps = { /** * The value of the property to be displayed and modified. @@ -17,6 +19,11 @@ export type ImmutablePrimitiveProps = { * Optional title for the component, used for tooltips or accessibility. */ title?: string; + + /** + * Optional information to display as an infoLabel popup aside the component. + */ + infoLabel?: InfoLabelParentProps; }; export type PrimitiveProps = ImmutablePrimitiveProps & { diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/spinButton.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/spinButton.tsx index 1151fabf419..e985654794f 100644 --- a/packages/dev/sharedUiComponents/src/fluent/primitives/spinButton.tsx +++ b/packages/dev/sharedUiComponents/src/fluent/primitives/spinButton.tsx @@ -1,48 +1,111 @@ -import { makeStyles, SpinButton as FluentSpinButton } from "@fluentui/react-components"; +import { makeStyles, SpinButton as FluentSpinButton, useId } from "@fluentui/react-components"; import type { SpinButtonOnChangeData, SpinButtonChangeEvent } from "@fluentui/react-components"; -import type { FunctionComponent } from "react"; -import { useCallback, useState } from "react"; +import type { FunctionComponent, KeyboardEvent, FocusEvent } from "react"; +import { useEffect, useState, useRef } from "react"; import type { PrimitiveProps } from "./primitive"; +import { InfoLabel } from "./infoLabel"; const useSpinStyles = makeStyles({ base: { display: "flex", flexDirection: "column", + minWidth: "55px", }, }); export type SpinButtonProps = PrimitiveProps & { - precision?: number; // Optional precision for the spin button - step?: number; // Optional step value for the spin button min?: number; max?: number; + step?: number; + unit?: string; + forceInt?: boolean; + validator?: (value: number) => boolean; }; export const SpinButton: FunctionComponent = (props) => { const classes = useSpinStyles(); + const { min, max } = props; - const [spinButtonValue, setSpinButtonValue] = useState(props.value); - - const onSpinButtonChange = useCallback( - (_ev: SpinButtonChangeEvent, data: SpinButtonOnChangeData) => { - // Stop propagation of the event to prevent it from bubbling up - _ev.stopPropagation(); - - if (data.value != null) { - setSpinButtonValue(data.value); - } else if (data.displayValue !== undefined) { - const newValue = parseFloat(data.displayValue); - if (!Number.isNaN(newValue)) { - setSpinButtonValue(newValue); - } - } - }, - [setSpinButtonValue] - ); + const [value, setValue] = useState(props.value); + const lastCommittedValue = useRef(props.value); + // step and forceInt are not mutually exclusive since there could be cases where you want to forceInt but have spinButton jump >1 int per spin + const step = props.step != undefined ? props.step : props.forceInt ? 1 : 2; + + useEffect(() => { + if (props.value != lastCommittedValue.current) { + lastCommittedValue.current = props.value; + setValue(props.value); // Update local state when props.value changes + } + }, [props.value]); + + const validateValue = (numericValue: number): boolean => { + const outOfBounds = (min !== undefined && numericValue < min) || (max !== undefined && numericValue > max); + const failsValidator = props.validator && !props.validator(numericValue); + const failsIntCheck = props.forceInt ? !Number.isInteger(numericValue) : false; + const invalid = !!outOfBounds || !!failsValidator || isNaN(numericValue) || !!failsIntCheck; + return !invalid; + }; + + const tryCommitValue = (currVal: number) => { + // Only commit if valid and different from last committed value + if (validateValue(currVal) && currVal !== lastCommittedValue.current) { + lastCommittedValue.current = currVal; + props.onChange(currVal); + } + }; + + const handleChange = (event: SpinButtonChangeEvent, data: SpinButtonOnChangeData) => { + event.stopPropagation(); // Prevent event propagation + if (data.value != null && !Number.isNaN(data.value)) { + setValue(data.value); // Update local state. Do not notify parent + tryCommitValue(data.value); + } + }; + + const handleKeyUp = (event: KeyboardEvent) => { + event.stopPropagation(); // Prevent event propagation + + if (event.key !== "Enter") { + // Update local state and try to commit the value if valid, applying styling if not + const currVal = parseFloat((event.target as any).value); + setValue(currVal); + tryCommitValue(currVal); + } + }; + + const handleKeyDown = (event: KeyboardEvent) => { + event.stopPropagation(); // Prevent event propagation + + // Prevent Enter key from causing form submission or value reversion + if (event.key === "Enter") { + event.preventDefault(); + } + }; + + const handleOnBlur = (event: FocusEvent) => { + event.stopPropagation(); + event.preventDefault(); + }; + + const invalidStyle = !validateValue(value) ? { backgroundColor: "#fdeaea" } : {}; + const id = useId("spin-button"); return (
- + {props.infoLabel && } +
); }; diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/syncedSlider.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/syncedSlider.tsx index 94f3fc818e1..259af3425ae 100644 --- a/packages/dev/sharedUiComponents/src/fluent/primitives/syncedSlider.tsx +++ b/packages/dev/sharedUiComponents/src/fluent/primitives/syncedSlider.tsx @@ -1,6 +1,6 @@ import type { SliderOnChangeData } from "@fluentui/react-components"; import { makeStyles, Slider, tokens } from "@fluentui/react-components"; -import { NumberInput } from "./input"; +import { SpinButton } from "./spinButton"; import type { ChangeEvent, FunctionComponent } from "react"; import { useEffect, useState, useRef } from "react"; import type { PrimitiveProps } from "./primitive"; @@ -16,9 +16,8 @@ const useSyncedSliderStyles = makeStyles({ flexGrow: 1, // Let slider grow minWidth: "40px", // Minimum width for slider to remain usable }, - input: { - width: "40px", // Fixed width for input - always 40px - flexShrink: 0, + spinButton: { + width: "60px", }, }); @@ -29,6 +28,8 @@ export type SyncedSliderProps = PrimitiveProps & { max?: number; /** Step size for the slider */ step?: number; + /** Displayed in the ux to indicate unit of measurement */ + unit?: string; /** When true, onChange is only called when the user releases the slider, not during drag */ notifyOnlyOnRelease?: boolean; }; @@ -80,12 +81,9 @@ export const SyncedSliderInput: FunctionComponent = (props) = isDraggingRef.current = false; }; - const handleInputChange = (value: string | number) => { - const newValue = Number(value); - if (!isNaN(newValue)) { - setValue(newValue); - props.onChange(newValue); // Input always updates immediately - } + const handleInputChange = (value: number) => { + setValue(value); + props.onChange(value); // Input always updates immediately }; return ( @@ -104,7 +102,7 @@ export const SyncedSliderInput: FunctionComponent = (props) = onPointerUp={handleSliderPointerUp} /> )} - +
); }; diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/textInput.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/textInput.tsx new file mode 100644 index 00000000000..fd3d06bf9a6 --- /dev/null +++ b/packages/dev/sharedUiComponents/src/fluent/primitives/textInput.tsx @@ -0,0 +1,94 @@ +import type { FunctionComponent, KeyboardEvent, ChangeEvent, FocusEvent } from "react"; +import { useEffect, useRef, useState } from "react"; +import type { InputOnChangeData } from "@fluentui/react-components"; +import { Input as FluentInput, makeStyles, useId } from "@fluentui/react-components"; +import type { PrimitiveProps } from "./primitive"; +import { InfoLabel } from "./infoLabel"; + +const useInputStyles = makeStyles({ + base: { + display: "flex", + flexDirection: "column", + width: "100px", + }, +}); + +export type TextInputProps = PrimitiveProps & { + validator?: (value: string) => boolean; +}; + +export const TextInput: FunctionComponent = (props) => { + const classes = useInputStyles(); + + const [value, setValue] = useState(props.value); + const lastCommittedValue = useRef(props.value); + + useEffect(() => { + if (props.value != lastCommittedValue.current) { + setValue(props.value); // Update local state when props.value changes + lastCommittedValue.current = props.value; + } + }, [props.value]); + + const validateValue = (val: string): boolean => { + const failsValidator = props.validator && !props.validator(val); + return !failsValidator; + }; + + const tryCommitValue = (currVal: string) => { + // Only commit if valid and different from last committed value + if (validateValue(currVal) && currVal !== lastCommittedValue.current) { + lastCommittedValue.current = currVal; + props.onChange(currVal); + } + }; + + const handleChange = (event: ChangeEvent, data: InputOnChangeData) => { + event.stopPropagation(); // Prevent event propagation + setValue(data.value); // Update local state. Do not notify parent + tryCommitValue(data.value); + }; + + const handleKeyUp = (event: KeyboardEvent) => { + event.stopPropagation(); // Prevent event propagation + + if (event.key !== "Enter") { + // Update local state and try to commit the value if valid, applying styling if not + setValue((event.target as any).value); + tryCommitValue((event.target as any).value); + } + }; + + const handleKeyDown = (event: KeyboardEvent) => { + event.stopPropagation(); // Prevent event propagation + + // Prevent Enter key from causing form submission or value reversion + if (event.key === "Enter") { + event.preventDefault(); + } + }; + + const handleOnBlur = (event: FocusEvent) => { + event.stopPropagation(); + event.preventDefault(); + }; + const invalidStyle = !validateValue(value) ? { backgroundColor: "#fdeaea" } : {}; + + const id = useId("input-button"); + return ( +
+ {props.infoLabel && } + +
+ ); +}; diff --git a/packages/dev/sharedUiComponents/src/lines/textInputLineComponent.tsx b/packages/dev/sharedUiComponents/src/lines/textInputLineComponent.tsx index 789a3a5a16e..b0779b65172 100644 --- a/packages/dev/sharedUiComponents/src/lines/textInputLineComponent.tsx +++ b/packages/dev/sharedUiComponents/src/lines/textInputLineComponent.tsx @@ -5,9 +5,8 @@ import type { PropertyChangedEvent } from "../propertyChangedEvent"; import type { LockObject } from "../tabs/propertyGrids/lockObject"; import { conflictingValuesPlaceholder } from "./targetsProxy"; import { InputArrowsComponent } from "./inputArrowsComponent"; -import { PropertyLine } from "../fluent/hoc/propertyLines/propertyLine"; -import { Textarea } from "../fluent/primitives/textarea"; -import { TextInput, NumberInput } from "../fluent/primitives/input"; +import { TextInputPropertyLine, NumberInputPropertyLine } from "../fluent/hoc/propertyLines/inputPropertyLine"; +import { TextAreaPropertyLine } from "../fluent/hoc/propertyLines/textAreaPropertyLine"; import { ToolContext } from "../fluent/hoc/fluentToolWrapper"; export interface ITextInputLineComponentProps { @@ -197,16 +196,18 @@ export class TextInputLineComponent extends Component - {this.props.multilines ? ( -