diff --git a/.eslintrc.js b/.eslintrc.js index 26ba9a7756c..1a1cefdf55d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -416,6 +416,16 @@ const rules = { match: true, }, }, + { + // Exception for hooks which start with 'use' + selector: "variable", + format: ["strictCamelCase"], + modifiers: ["global"], + filter: { + regex: "^use", + match: true, + }, + }, { selector: "variable", format: ["PascalCase"], diff --git a/package-lock.json b/package-lock.json index 2987f51a1bf..e329b5bdcc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28224,12 +28224,16 @@ "@babel/core": "^7.17.9", "@dev/core": "^1.0.0", "@dev/gui": "^1.0.0", + "@fluentui/react-components": "^9.62.0", + "@fluentui/react-icons": "^2.0.271", "@fortawesome/fontawesome-svg-core": "^6.1.0", "@fortawesome/free-solid-svg-icons": "^6.0.0", "@fortawesome/react-fontawesome": "^0.2.0", "sass": "^1.62.1" }, "peerDependencies": { + "@fluentui/react-components": "^9.62.0", + "@fluentui/react-icons": "^2.0.271", "@fortawesome/fontawesome-svg-core": "^6.1.0", "@fortawesome/free-solid-svg-icons": "^6.1.0", "@fortawesome/react-fontawesome": "^0.2.0", diff --git a/packages/dev/buildTools/src/generateDeclaration.ts b/packages/dev/buildTools/src/generateDeclaration.ts index f825e9e803a..4d17d0a3849 100644 --- a/packages/dev/buildTools/src/generateDeclaration.ts +++ b/packages/dev/buildTools/src/generateDeclaration.ts @@ -112,7 +112,7 @@ function GetModuleDeclaration( // not a dev dependency // TODO - make a list of external dependencies per package // for now - we support react - if (match[1] !== "react") { + if (match[1] !== "react" /* && !match[1].startsWith("@fluentui")*/) { // check what the line imports line = ""; } @@ -163,9 +163,9 @@ function GetModuleDeclaration( // TODO - make a list of dependencies that are accepted by each package if (!devPackageName) { if (externalName) { - if (externalName === "@fortawesome" || externalName === "react-contextmenu") { + if (externalName === "@fortawesome" || externalName === "react-contextmenu" || externalName === "@fluentui") { // replace with any - const matchRegex = new RegExp(`([ <])(${alias}[^;\n ]*)([^\\w])`, "g"); + const matchRegex = new RegExp(`([ <])(${alias}[^,;\n> ]*)([^\\w])`, "g"); processedLines = processedLines.replace(matchRegex, `$1any$3`); return; } @@ -399,9 +399,9 @@ function GetPackageDeclaration( // TODO - make a list of dependencies that are accepted by each package if (!localDevPackageMap) { if (externalName) { - if (externalName === "@fortawesome" || externalName === "react-contextmenu") { + if (externalName === "@fortawesome" || externalName === "react-contextmenu" || externalName === "@fluentui") { // replace with any - const matchRegex = new RegExp(`([ <])(${alias}[^;\n ]*)([^\\w])`, "g"); + const matchRegex = new RegExp(`([ <])(${alias}[^,;\n> ]*)([^\\w])`, "g"); processedSource = processedSource.replace(matchRegex, `$1any$3`); return; } else if (externalName === "react") { diff --git a/packages/dev/inspector/src/components/actionTabs/tabs/propertyGridTabComponent.tsx b/packages/dev/inspector/src/components/actionTabs/tabs/propertyGridTabComponent.tsx index 527d7d5338b..505ae89cb26 100644 --- a/packages/dev/inspector/src/components/actionTabs/tabs/propertyGridTabComponent.tsx +++ b/packages/dev/inspector/src/components/actionTabs/tabs/propertyGridTabComponent.tsx @@ -116,6 +116,7 @@ import { SkyMaterialPropertyGridComponent } from "./propertyGrids/materials/skyM import { Tags } from "core/Misc/tags"; import { LineContainerComponent } from "shared-ui-components/lines/lineContainerComponent"; import type { RectAreaLight } from "core/Lights/rectAreaLight"; +import { FluentToolWrapper } from "shared-ui-components/fluent/hoc/fluentToolWrapper"; export class PropertyGridTabComponent extends PaneComponent { private _timerIntervalId: number; @@ -774,15 +775,17 @@ export class PropertyGridTabComponent extends PaneComponent { const entity = this.props.selectedEntity || {}; const entityHasMetadataProp = Object.prototype.hasOwnProperty.call(entity, "metadata"); return ( -
- {this.renderContent()} - {Tags.HasTags(entity) && ( - -
{this.renderTags()}
-
- )} - {entityHasMetadataProp && } -
+ +
+ {this.renderContent()} + {Tags.HasTags(entity) && ( + +
{this.renderTags()}
+
+ )} + {entityHasMetadataProp && } +
+
); } } diff --git a/packages/dev/sharedUiComponents/package.json b/packages/dev/sharedUiComponents/package.json index 8aac42ca749..05a6eef119b 100644 --- a/packages/dev/sharedUiComponents/package.json +++ b/packages/dev/sharedUiComponents/package.json @@ -24,6 +24,8 @@ "@fortawesome/fontawesome-svg-core": "^6.1.0", "@fortawesome/free-solid-svg-icons": "^6.1.0", "@fortawesome/react-fontawesome": "^0.2.0", + "@fluentui/react-components": "^9.62.0", + "@fluentui/react-icons": "^2.0.271", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "react": "^18.2.0", @@ -38,6 +40,8 @@ "@fortawesome/fontawesome-svg-core": "^6.1.0", "@fortawesome/free-solid-svg-icons": "^6.0.0", "@fortawesome/react-fontawesome": "^0.2.0", + "@fluentui/react-components": "^9.62.0", + "@fluentui/react-icons": "^2.0.271", "sass": "^1.62.1" }, "sideEffects": false, diff --git a/packages/dev/sharedUiComponents/src/fluent/hoc/colorPropertyLine.tsx b/packages/dev/sharedUiComponents/src/fluent/hoc/colorPropertyLine.tsx new file mode 100644 index 00000000000..7ca55e2a646 --- /dev/null +++ b/packages/dev/sharedUiComponents/src/fluent/hoc/colorPropertyLine.tsx @@ -0,0 +1,40 @@ +import type { FunctionComponent } from "react"; + +import type { PropertyLineProps } from "./propertyLine"; +import { PropertyLine } from "./propertyLine"; +import { SyncedSliderLine } from "./syncedSliderLine"; + +import type { Color3 } from "core/Maths/math.color"; +import { Color4 } from "core/Maths/math.color"; + +type ColorSliderProps = { + color: Color3 | Color4; +}; + +const ColorSliders: FunctionComponent = (props) => { + return ( + <> + + + + {props.color instanceof Color4 && } + + ); +}; + +/** + * Reusable component which renders a color property line containing a label, colorPicker popout, and expandable RGBA values + * The expandable RGBA values are synced sliders that allow the user to modify the color's RGBA values directly + * @param props - PropertyLine props, replacing children with a color object so that we can properly display the color + * @returns Component wrapping a colorPicker component (coming soon) with a property line + */ +export const ColorPropertyLine: FunctionComponent = (props) => { + return ( + }> + { + props.color.toString() + // Will replace with colorPicker in future PR + } + + ); +}; diff --git a/packages/dev/sharedUiComponents/src/fluent/hoc/fluentToolWrapper.tsx b/packages/dev/sharedUiComponents/src/fluent/hoc/fluentToolWrapper.tsx new file mode 100644 index 00000000000..afdc02a07c9 --- /dev/null +++ b/packages/dev/sharedUiComponents/src/fluent/hoc/fluentToolWrapper.tsx @@ -0,0 +1,32 @@ +import type { PropsWithChildren, FunctionComponent } from "react"; +import { createContext } from "react"; +import type { Theme } from "@fluentui/react-components"; +import { FluentProvider, webDarkTheme } from "@fluentui/react-components"; + +export type ToolHostProps = { + /** + * Allows host to pass in a theme + */ + customTheme?: Theme; +}; + +export const ToolContext = createContext({ useFluent: false as boolean } as const); + +/** + * For tools which are ready to move over the fluent, wrap the root of the tool (or the panel which you want fluentized) with this component + * Today we will only enable fluent if the URL has the `newUX` query parameter is truthy + * @param props + * @returns + */ +export const FluentToolWrapper: FunctionComponent> = (props) => { + const url = new URL(window.location.href); + const enableFluent = url.searchParams.has("newUX") || url.hash.includes("newUX"); + + return enableFluent ? ( + + {props.children} + + ) : ( + props.children + ); +}; diff --git a/packages/dev/sharedUiComponents/src/fluent/hoc/propertyLine.tsx b/packages/dev/sharedUiComponents/src/fluent/hoc/propertyLine.tsx new file mode 100644 index 00000000000..c2860696e5d --- /dev/null +++ b/packages/dev/sharedUiComponents/src/fluent/hoc/propertyLine.tsx @@ -0,0 +1,108 @@ +import { Body1Strong, Button, InfoLabel, makeStyles, tokens } from "@fluentui/react-components"; +import { Add24Filled, Copy24Regular, Subtract24Filled } from "@fluentui/react-icons"; +import type { FunctionComponent, PropsWithChildren } from "react"; +import { useState } from "react"; +import { copyCommandToClipboard } from "../../copyCommandToClipboard"; + +const usePropertyLineStyles = makeStyles({ + container: { + width: "100%", + display: "flex", + flexDirection: "column", // Stack line + expanded content + borderBottom: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, + }, + line: { + display: "flex", + alignItems: "center", + justifyContent: "flex-start", + padding: `${tokens.spacingVerticalXS} 0px`, + width: "100%", + }, + label: { + width: "33%", + textAlign: "left", + }, + rightContent: { + width: "67%", + display: "flex", + alignItems: "center", + justifyContent: "flex-end", + }, + button: { + marginLeft: tokens.spacingHorizontalXXS, + width: "100px", + }, + fillRestOfRightContentWidth: { + flex: 1, + display: "flex", + justifyContent: "flex-end", + alignItems: "center", + }, + expandedContent: { + backgroundColor: tokens.colorNeutralBackground1, + }, +}); + +export type PropertyLineProps = { + /** + * The name of the property to display in the property line. + */ + label: string; + /** + * Optional description for the property, shown on hover of the info icon + */ + description?: string; + /** + * Optional function returning a string to copy to clipboard. + */ + onCopy?: () => string; + /** + * If supplied, an 'expand' icon will be shown which, when clicked, renders this component within the property line. + */ + expandedContent?: JSX.Element; +}; + +/** + * A reusable component that renders a property line with a label and child content, and an optional description, copy button, and expandable section. + * + * @param props - The properties for the PropertyLine component. + * @returns A React element representing the property line. + * + */ +export const PropertyLine: FunctionComponent> = (props) => { + const classes = usePropertyLineStyles(); + const [expanded, setExpanded] = useState(false); + + const { label, description, onCopy, expandedContent, children } = props; + + return ( +
+
+ + {label} + +
+
{children}
+ + {expandedContent && ( +
+
+ + {expanded && expandedContent &&
{expandedContent}
} +
+ ); +}; diff --git a/packages/dev/sharedUiComponents/src/fluent/hoc/syncedSliderLine.tsx b/packages/dev/sharedUiComponents/src/fluent/hoc/syncedSliderLine.tsx new file mode 100644 index 00000000000..66b466a462c --- /dev/null +++ b/packages/dev/sharedUiComponents/src/fluent/hoc/syncedSliderLine.tsx @@ -0,0 +1,45 @@ +import { PropertyLine } from "./propertyLine"; +import type { PropertyLineProps } from "./propertyLine"; +import { SyncedSliderInput } from "../primitives/syncedSlider"; +import type { SyncedSliderProps } from "../primitives/syncedSlider"; + +export type SyncedSliderLineProps = PropertyLineProps & + Omit & { + /** + * String key + */ + propertyKey: K; + /** + * target where O[K] is a number + */ + target: O; + /** + * Callback when either the slider or input value changes + */ + onChange?: (value: number) => void; + }; + +/** + * Renders a SyncedSlider within a PropertyLine for a given key/value pair, where the value is number (ex: can be used for a color's RGBA values, a vector's XYZ values, etc) + * When value changes, updates the object with the new value and calls the onChange callback. + * + * Example usage looks like + * \ + * \ + * @param props + * @returns + */ +export const SyncedSliderLine = , K extends PropertyKey>(props: SyncedSliderLineProps): React.ReactElement => { + return ( + + { + props.target[props.propertyKey] = val as O[K]; + props.onChange?.(val); + }} + /> + + ); +}; diff --git a/packages/dev/sharedUiComponents/src/fluent/hoc/vectorPropertyLine.tsx b/packages/dev/sharedUiComponents/src/fluent/hoc/vectorPropertyLine.tsx new file mode 100644 index 00000000000..9019e95baea --- /dev/null +++ b/packages/dev/sharedUiComponents/src/fluent/hoc/vectorPropertyLine.tsx @@ -0,0 +1,43 @@ +import type { FunctionComponent } from "react"; + +import { Body1 } from "@fluentui/react-components"; +import { PropertyLine } from "./propertyLine"; +import type { PropertyLineProps } from "./propertyLine"; + +import { SyncedSliderLine } from "./syncedSliderLine"; + +import { Vector4 } from "core/Maths/math.vector"; +import type { Vector3 } from "core/Maths/math.vector"; + +type VectorSliderProps = { + vector: Vector3 | Vector4; + min?: number; + max?: number; + step?: number; +}; + +const VectorSliders: FunctionComponent = (props) => { + const { vector, ...sliderProps } = props; + return ( + <> + + + + {vector instanceof Vector4 && } + + ); +}; + +/** + * Reusable component which renders a vector property line containing a label, vector value, and expandable XYZW values + * The expanded section contains a slider/input box for each component of the vector (x, y, z, w) + * @param props + * @returns + */ +export const VectorPropertyLine: FunctionComponent = (props) => { + return ( + }> + {props.vector.toString()} + + ); +}; diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/dropdown.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/dropdown.tsx new file mode 100644 index 00000000000..b631b8790bf --- /dev/null +++ b/packages/dev/sharedUiComponents/src/fluent/primitives/dropdown.tsx @@ -0,0 +1,48 @@ +import { Dropdown as FluentDropdown, makeStyles, Option } from "@fluentui/react-components"; +import type { FunctionComponent } from "react"; + +const useDropdownStyles = makeStyles({ + dropdownOption: { + textAlign: "right", + minWidth: "40px", + }, + optionsLine: {}, +}); + +export type DropdownOption = { + /** + * Defines the visible part of the option + */ + label: string; + /** + * Defines the value part of the option (returned through the callback) + */ + value: string | number; +}; + +type DropdownProps = { options: readonly DropdownOption[]; onSelect: (o: string) => void; defaultValue?: DropdownOption }; + +/** + * Renders a fluent UI dropdown with a calback for selection and a required default value + * @param props + * @returns dropdown component + */ +export const Dropdown: FunctionComponent = (props) => { + const classes = useDropdownStyles(); + return ( + { + data.optionValue != undefined && props.onSelect(data.optionValue); + }} + defaultValue={props.defaultValue?.label} + defaultSelectedOptions={props.defaultValue && [props.defaultValue.value.toString()]} + > + {props.options.map((option: DropdownOption) => ( + + ))} + + ); +}; diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/input.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/input.tsx new file mode 100644 index 00000000000..39c1448d191 --- /dev/null +++ b/packages/dev/sharedUiComponents/src/fluent/primitives/input.tsx @@ -0,0 +1,47 @@ +import type { FunctionComponent, KeyboardEvent, ChangeEvent } from "react"; +import { useEffect, useState } from "react"; + +import type { InputProps as FluentInputProps } from "@fluentui/react-components"; +import { Input as FluentInput, makeStyles } from "@fluentui/react-components"; + +const useInputStyles = makeStyles({ + text: { + height: "auto", + }, + float: { + height: "auto", + width: "80px", // Fixed width for number input + flexShrink: 0, + }, +}); + +/** + * 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 + */ +export const Input: FunctionComponent = (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, data: any) => { + event.stopPropagation(); // Prevent event propagation + if (props.onChange) { + props.onChange(event, data); // Call the original onChange handler passed as prop + } + setValue(event.target.value); // Update local state with the new value + }; + + const handleKeyDown = (event: KeyboardEvent) => { + event.stopPropagation(); // Prevent event propagation + if (props.onKeyDown) { + props.onKeyDown(event); // Call the original onKeyDown handler passed as prop + } + }; + + return ; +}; diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/link.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/link.tsx new file mode 100644 index 00000000000..0df5e9f8709 --- /dev/null +++ b/packages/dev/sharedUiComponents/src/fluent/primitives/link.tsx @@ -0,0 +1,3 @@ +// No special styling / functionality yet! + +export { Link } from "@fluentui/react-components"; diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/switch.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/switch.tsx new file mode 100644 index 00000000000..00ffbfb38bb --- /dev/null +++ b/packages/dev/sharedUiComponents/src/fluent/primitives/switch.tsx @@ -0,0 +1,33 @@ +// eslint-disable-next-line import/no-internal-modules + +import type { SwitchOnChangeData, SwitchProps as FluentSwitchProps } from "@fluentui/react-components"; +import type { ChangeEvent, FunctionComponent } from "react"; + +import { makeStyles, Switch as FluentSwitch } from "@fluentui/react-components"; +import { useState } from "react"; + +const useSwitchStyles = makeStyles({ + switch: { + marginLeft: "auto", + }, + indicator: { + marginRight: 0, // Remove the default right margin so the switch aligns well on the right side inside panels like the properties pane. + }, +}); + +/** + * This is a primitive fluent boolean switch component whose only knowledge is the shared styling across all tools + * @param props + * @returns Switch component + */ +export const Switch: FunctionComponent = (props) => { + const classes = useSwitchStyles(); + const [checked, setChecked] = useState(() => props.checked ?? false); + + const onChange = (event: ChangeEvent, data: SwitchOnChangeData) => { + props.onChange && props.onChange(event, data); + setChecked(event.target.checked); + }; + + return ; +}; diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/syncedSlider.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/syncedSlider.tsx new file mode 100644 index 00000000000..b976814f61a --- /dev/null +++ b/packages/dev/sharedUiComponents/src/fluent/primitives/syncedSlider.tsx @@ -0,0 +1,67 @@ +import type { InputProps, SliderOnChangeData, SliderProps } from "@fluentui/react-components"; +import { makeStyles, Slider } from "@fluentui/react-components"; +import { Input } from "./input"; +import type { ChangeEvent, FunctionComponent } from "react"; +import { useEffect, useState } from "react"; + +const useSyncedSliderStyles = makeStyles({ + syncedSlider: { + display: "flex", + alignItems: "center", + gap: "1rem", + width: "100%", // Only fill available space + }, + slider: { + flexGrow: 1, // Let slider grow + minWidth: 0, // Allow shrink if needed + }, + input: { + width: "80px", // Fixed width for number input + flexShrink: 0, + }, +}); + +export type SyncedSliderProps = Omit & { + /** + * Callback to notify parent of value change, override both of the slider/input handlers + */ + onChange: (value: number) => void; + /** + * Controlled value for the slider and input + */ + value: number; +}; + +/** + * Component which synchronizes a slider and an input field, allowing the user to change a value using either control + * @param props + * @returns SyncedSlider component + */ +export const SyncedSliderInput: FunctionComponent = (props) => { + const classes = useSyncedSliderStyles(); + const [value, setValue] = useState(props.value); + + useEffect(() => { + setValue(props.value ?? ""); // Update local state when props.value changes + }, [props.value]); + + const handleSliderChange = (_: ChangeEvent, data: SliderOnChangeData) => { + setValue(data.value); + props.onChange(data.value); // Notify parent + }; + + const handleInputChange = (e: ChangeEvent) => { + const newValue = Number(e.target.value); + if (!isNaN(newValue)) { + setValue(newValue); + props.onChange(newValue); // Notify parent + } + }; + + return ( +
+ + +
+ ); +}; diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/text.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/text.tsx new file mode 100644 index 00000000000..5812e2cf140 --- /dev/null +++ b/packages/dev/sharedUiComponents/src/fluent/primitives/text.tsx @@ -0,0 +1,3 @@ +export { Text } from "@fluentui/react-components"; + +// No special styling / functionality yet! diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/textarea.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/textarea.tsx new file mode 100644 index 00000000000..f53a70c1fab --- /dev/null +++ b/packages/dev/sharedUiComponents/src/fluent/primitives/textarea.tsx @@ -0,0 +1,31 @@ +import type { TextareaProps as FluentTextareaProps } from "@fluentui/react-components"; +import { Textarea as FluentTextarea, makeStyles } from "@fluentui/react-components"; +import type { FunctionComponent, KeyboardEvent, ChangeEvent } from "react"; + +const useInputStyles = makeStyles({ + textarea: {}, +}); + +/** + * This is a texarea box that stops propagation of change/keydown events + * @param props + * @returns + */ +export const Textarea: FunctionComponent = (props) => { + const classes = useInputStyles(); + const handleChange = (event: ChangeEvent, data: any) => { + event.stopPropagation(); // Prevent event propagation + if (props.onChange) { + props.onChange(event, data); // Call the original onChange handler passed as prop + } + }; + + const handleKeyDown = (event: KeyboardEvent) => { + event.stopPropagation(); // Prevent event propagation + if (props.onKeyDown) { + props.onKeyDown(event); // Call the original onKeyDown handler passed as prop + } + }; + + return ; +}; diff --git a/packages/dev/sharedUiComponents/src/fluent/readme.md b/packages/dev/sharedUiComponents/src/fluent/readme.md new file mode 100644 index 00000000000..02e4da93ed0 --- /dev/null +++ b/packages/dev/sharedUiComponents/src/fluent/readme.md @@ -0,0 +1,19 @@ +# Fluent! + +We are embarking on an effort to replace our common controls with modernized fluent UX controls, used by the existing tools and the in-progress inspectorV2. + +This work is split into several distinct chunks + +1. **Fluent primitives** + These are lightweight wrappers around existing fluent components, with some minor styling applied. Depending on the primitive at hand, the wrapper may do very little (ex: apply some minor styling) or more complex logic (ex: SyncedSlider.tsx coordinates changes between a slider component and an input component). + These primitive can be used standalone or within a pre-styled component such as PropertyLine.tsx, see #2 +2. **Fluent higher order components (hoc)** + These are wrappers around the above primitives to handle styling for common UX patterns, ex: PropertyLine.tsx which renders a label, fluent primtive child component (to modify said propetry), and optional copy button. This is a common UX pattern used in the property pane of all of our tools. +3. **Render fluent components directly from inspectorV2** + InspectorV2 will be using the new fluent components directly, rather than using any existing shared-ui-components.You can see examples of this in various files within packages\dev\inspector-v2 (ex: meshGeneralProperties.tsx, commonGeneralProperties.tsx, outlineOverlayProperties.tsx). +4. **Conditionally render fluent components within existing shared-ui-components** + We are using a contextConsumer to read context.useFluent from within each shared component and conditionally render the fluent version of each shared component. This means that existing tools do not have to update every callsite of a shared component, instead the tool just needs to ensure the contextProvider is properly created at the tool root, see below +5. **Incrementally move each tool over to fluent** + 1 by 1 we will wrap each tool's root in a , which both creates a fluent provider and ensures useFluent context is set to true (so shared components render their fluent versions). This will allow for modular testing of the new components within each tool before lighting it up. Note that we also check for a 'newUX' Query String Parameter in the URL so that we can check-in the new logic without default enabling it for all users (to enable, add ?newUX=true to the URL) +6. **Incrementally refactor existing tools** + After each tool is fluentized, we can incrementally refactor the way the shared components are being initialized (ex: see PropertyTabComponent) to extract common tool paradigms into our shared tooling folder and reduce duplication of logic across our tools. diff --git a/packages/dev/sharedUiComponents/src/lines/optionsLineComponent.tsx b/packages/dev/sharedUiComponents/src/lines/optionsLineComponent.tsx index b48ea1b36cf..9f3c8a10cd3 100644 --- a/packages/dev/sharedUiComponents/src/lines/optionsLineComponent.tsx +++ b/packages/dev/sharedUiComponents/src/lines/optionsLineComponent.tsx @@ -4,6 +4,9 @@ import type { PropertyChangedEvent } from "../propertyChangedEvent"; import { copyCommandToClipboard, getClassNameWithNamespace } from "../copyCommandToClipboard"; import type { IInspectableOptions } from "core/Misc/iInspectable"; import copyIcon from "../imgs/copy.svg"; +import { PropertyLine } from "../fluent/hoc/propertyLine"; +import { Dropdown } from "../fluent/primitives/dropdown"; +import { ToolContext } from "../fluent/hoc/fluentToolWrapper"; // eslint-disable-next-line @typescript-eslint/naming-convention export const Null_Value = Number.MAX_SAFE_INTEGER; @@ -103,20 +106,34 @@ export class OptionsLine extends React.Component this.onCopyClickStr()}> + { + val !== undefined && this.updateValue(val); + }} + defaultValue={this.props.options.find((o) => o.value === this.state.value || o.selected)} + /> + + ); + } + + private _renderOriginal() { return (
{this.props.icon && {this.props.iconLabel}} @@ -134,10 +151,13 @@ export class OptionsLine extends React.Component
-
this.onCopyClick()} title="Copy to clipboard"> +
copyCommandToClipboard(this.onCopyClickStr())} title="Copy to clipboard"> Copy
); } + override render() { + return {({ useFluent }) => (useFluent ? this._renderFluent() : this._renderOriginal())}; + } } diff --git a/packages/dev/sharedUiComponents/src/lines/sliderLineComponent.tsx b/packages/dev/sharedUiComponents/src/lines/sliderLineComponent.tsx index 5b22d578ab5..4f7743f71fe 100644 --- a/packages/dev/sharedUiComponents/src/lines/sliderLineComponent.tsx +++ b/packages/dev/sharedUiComponents/src/lines/sliderLineComponent.tsx @@ -6,6 +6,8 @@ import { Tools } from "core/Misc/tools"; import { FloatLineComponent } from "./floatLineComponent"; import type { LockObject } from "../tabs/propertyGrids/lockObject"; import copyIcon from "../imgs/copy.svg"; +import { ToolContext } from "../fluent/hoc/fluentToolWrapper"; +import { SyncedSliderLine } from "../fluent/hoc/syncedSliderLine"; interface ISliderLineComponentProps { label: string; @@ -140,7 +142,21 @@ export class SliderLineComponent extends React.Component this.onChange(val)} + step={this.props.step} + min={this.props.minimum} + max={this.props.maximum} + /> + ); + } + + renderOriginal() { return (
{this.props.icon && {this.props.iconLabel}} @@ -188,4 +204,7 @@ export class SliderLineComponent extends React.Component ); } + override render() { + return {({ useFluent }) => (useFluent ? this.renderFluent() : this.renderOriginal())}; + } } diff --git a/packages/dev/sharedUiComponents/src/lines/textInputLineComponent.tsx b/packages/dev/sharedUiComponents/src/lines/textInputLineComponent.tsx index 9e862ba1773..f54f4859bc9 100644 --- a/packages/dev/sharedUiComponents/src/lines/textInputLineComponent.tsx +++ b/packages/dev/sharedUiComponents/src/lines/textInputLineComponent.tsx @@ -1,9 +1,14 @@ -import * as React from "react"; +import type { ReactNode, KeyboardEvent } from "react"; +import { Component } from "react"; import type { Observable } from "core/Misc/observable"; 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/propertyLine"; +import { Textarea } from "../fluent/primitives/textarea"; +import { Input } from "../fluent/primitives/input"; +import { ToolContext } from "../fluent/hoc/fluentToolWrapper"; export interface ITextInputLineComponentProps { label?: string; @@ -26,7 +31,7 @@ export interface ITextInputLineComponentProps { min?: number; max?: number; placeholder?: string; - unit?: React.ReactNode; + unit?: ReactNode; validator?: (value: string) => boolean; multilines?: boolean; throttlePropertyChangedNotification?: boolean; @@ -36,7 +41,7 @@ export interface ITextInputLineComponentProps { let ThrottleTimerId = -1; -export class TextInputLineComponent extends React.Component { +export class TextInputLineComponent extends Component { private _localChange = false; constructor(props: ITextInputLineComponentProps) { @@ -178,7 +183,7 @@ export class TextInputLineComponent extends React.Component + {this.props.multilines ? ( +