Skip to content

Commit fd3d76f

Browse files
georginahalpernGeorginaRaananWryantrem
authored
Introduce new fluent shared components; conditionally load them from existing shared components if useFluent context is true (#16709)
View shared-ui-components/src/fluent/**readme.md** for a complete overview of these changes and the plan for rolling them out! The changes in this PR can be broken down into the below buckets, with several examples for each. Future PRs will be more incremental, can bring 1 control at a time. - shared-ui-components/src/**fluent**: contains new primitives and HOCs (including FluentToolWrapper, which wraps children in FluentProvider and ContextProvider<{useFluent=true}> if the 'newUX' string is present in the QSP or hash) - shared-ui-components/src/**lines**: existing shared components, updated to conditionally render above fluent components if anywhere upstream is wrapped in a FluentToolWrapper - examples **enabling fluent in the below tools** - packages/tools/nodeEditor/src/graphEditor.tsx (NME) - packages/dev/inspector/src/components/actionTabs/tabs/propertyGridTabComponent.tsx (inspectorV1 property panel) - shared-ui-components/src/fluent/tempInspectorV2 (temp folder to show usage before ryan's changes are merged - u can view them in PR history but won't be checked in with the final merge) Miscellaneous: - eslint change to allow useFoo to be camelcased (since that is the standard w fluent) - generateDeclaration.ts change to cast fluent types to any in the d.ts files so that consumers don't need to declare fluent dependency (same thing we do for fontawesome) NOTE that introducing fluent introduced some build issues in this PR, which were diagnosed/ resolved in this [test PR](#16730) and then checked in separately in [this PR](#16759). --------- Co-authored-by: Georgina <[email protected]> Co-authored-by: Raanan Weber <[email protected]> Co-authored-by: Ryan Tremblay <[email protected]>
1 parent caa1d12 commit fd3d76f

23 files changed

+724
-86
lines changed

.eslintrc.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,16 @@ const rules = {
416416
match: true,
417417
},
418418
},
419+
{
420+
// Exception for hooks which start with 'use'
421+
selector: "variable",
422+
format: ["strictCamelCase"],
423+
modifiers: ["global"],
424+
filter: {
425+
regex: "^use",
426+
match: true,
427+
},
428+
},
419429
{
420430
selector: "variable",
421431
format: ["PascalCase"],

package-lock.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/dev/buildTools/src/generateDeclaration.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ function GetModuleDeclaration(
112112
// not a dev dependency
113113
// TODO - make a list of external dependencies per package
114114
// for now - we support react
115-
if (match[1] !== "react") {
115+
if (match[1] !== "react" /* && !match[1].startsWith("@fluentui")*/) {
116116
// check what the line imports
117117
line = "";
118118
}
@@ -163,9 +163,9 @@ function GetModuleDeclaration(
163163
// TODO - make a list of dependencies that are accepted by each package
164164
if (!devPackageName) {
165165
if (externalName) {
166-
if (externalName === "@fortawesome" || externalName === "react-contextmenu") {
166+
if (externalName === "@fortawesome" || externalName === "react-contextmenu" || externalName === "@fluentui") {
167167
// replace with any
168-
const matchRegex = new RegExp(`([ <])(${alias}[^;\n ]*)([^\\w])`, "g");
168+
const matchRegex = new RegExp(`([ <])(${alias}[^,;\n> ]*)([^\\w])`, "g");
169169
processedLines = processedLines.replace(matchRegex, `$1any$3`);
170170
return;
171171
}
@@ -399,9 +399,9 @@ function GetPackageDeclaration(
399399
// TODO - make a list of dependencies that are accepted by each package
400400
if (!localDevPackageMap) {
401401
if (externalName) {
402-
if (externalName === "@fortawesome" || externalName === "react-contextmenu") {
402+
if (externalName === "@fortawesome" || externalName === "react-contextmenu" || externalName === "@fluentui") {
403403
// replace with any
404-
const matchRegex = new RegExp(`([ <])(${alias}[^;\n ]*)([^\\w])`, "g");
404+
const matchRegex = new RegExp(`([ <])(${alias}[^,;\n> ]*)([^\\w])`, "g");
405405
processedSource = processedSource.replace(matchRegex, `$1any$3`);
406406
return;
407407
} else if (externalName === "react") {

packages/dev/inspector/src/components/actionTabs/tabs/propertyGridTabComponent.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ import { SkyMaterialPropertyGridComponent } from "./propertyGrids/materials/skyM
116116
import { Tags } from "core/Misc/tags";
117117
import { LineContainerComponent } from "shared-ui-components/lines/lineContainerComponent";
118118
import type { RectAreaLight } from "core/Lights/rectAreaLight";
119+
import { FluentToolWrapper } from "shared-ui-components/fluent/hoc/fluentToolWrapper";
119120

120121
export class PropertyGridTabComponent extends PaneComponent {
121122
private _timerIntervalId: number;
@@ -774,15 +775,17 @@ export class PropertyGridTabComponent extends PaneComponent {
774775
const entity = this.props.selectedEntity || {};
775776
const entityHasMetadataProp = Object.prototype.hasOwnProperty.call(entity, "metadata");
776777
return (
777-
<div className="pane">
778-
{this.renderContent()}
779-
{Tags.HasTags(entity) && (
780-
<LineContainerComponent title="TAGS" selection={this.props.globalState}>
781-
<div className="tagContainer">{this.renderTags()}</div>
782-
</LineContainerComponent>
783-
)}
784-
{entityHasMetadataProp && <MetadataGridComponent globalState={this.props.globalState} entity={entity} />}
785-
</div>
778+
<FluentToolWrapper>
779+
<div className="pane">
780+
{this.renderContent()}
781+
{Tags.HasTags(entity) && (
782+
<LineContainerComponent title="TAGS" selection={this.props.globalState}>
783+
<div className="tagContainer">{this.renderTags()}</div>
784+
</LineContainerComponent>
785+
)}
786+
{entityHasMetadataProp && <MetadataGridComponent globalState={this.props.globalState} entity={entity} />}
787+
</div>
788+
</FluentToolWrapper>
786789
);
787790
}
788791
}

packages/dev/sharedUiComponents/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
"@fortawesome/fontawesome-svg-core": "^6.1.0",
2525
"@fortawesome/free-solid-svg-icons": "^6.1.0",
2626
"@fortawesome/react-fontawesome": "^0.2.0",
27+
"@fluentui/react-components": "^9.62.0",
28+
"@fluentui/react-icons": "^2.0.271",
2729
"@types/react": "^18.0.0",
2830
"@types/react-dom": "^18.0.0",
2931
"react": "^18.2.0",
@@ -38,6 +40,8 @@
3840
"@fortawesome/fontawesome-svg-core": "^6.1.0",
3941
"@fortawesome/free-solid-svg-icons": "^6.0.0",
4042
"@fortawesome/react-fontawesome": "^0.2.0",
43+
"@fluentui/react-components": "^9.62.0",
44+
"@fluentui/react-icons": "^2.0.271",
4145
"sass": "^1.62.1"
4246
},
4347
"sideEffects": false,
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { FunctionComponent } from "react";
2+
3+
import type { PropertyLineProps } from "./propertyLine";
4+
import { PropertyLine } from "./propertyLine";
5+
import { SyncedSliderLine } from "./syncedSliderLine";
6+
7+
import type { Color3 } from "core/Maths/math.color";
8+
import { Color4 } from "core/Maths/math.color";
9+
10+
type ColorSliderProps = {
11+
color: Color3 | Color4;
12+
};
13+
14+
const ColorSliders: FunctionComponent<ColorSliderProps> = (props) => {
15+
return (
16+
<>
17+
<SyncedSliderLine label="R" propertyKey="r" target={props.color} min={0} max={255} />
18+
<SyncedSliderLine label="G" propertyKey="g" target={props.color} min={0} max={255} />
19+
<SyncedSliderLine label="B" propertyKey="b" target={props.color} min={0} max={255} />
20+
{props.color instanceof Color4 && <SyncedSliderLine label="A" propertyKey="a" target={props.color} min={0} max={1} />}
21+
</>
22+
);
23+
};
24+
25+
/**
26+
* Reusable component which renders a color property line containing a label, colorPicker popout, and expandable RGBA values
27+
* The expandable RGBA values are synced sliders that allow the user to modify the color's RGBA values directly
28+
* @param props - PropertyLine props, replacing children with a color object so that we can properly display the color
29+
* @returns Component wrapping a colorPicker component (coming soon) with a property line
30+
*/
31+
export const ColorPropertyLine: FunctionComponent<ColorSliderProps & PropertyLineProps> = (props) => {
32+
return (
33+
<PropertyLine {...props} expandedContent={<ColorSliders {...props} />}>
34+
{
35+
props.color.toString()
36+
// Will replace with colorPicker in future PR
37+
}
38+
</PropertyLine>
39+
);
40+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { PropsWithChildren, FunctionComponent } from "react";
2+
import { createContext } from "react";
3+
import type { Theme } from "@fluentui/react-components";
4+
import { FluentProvider, webDarkTheme } from "@fluentui/react-components";
5+
6+
export type ToolHostProps = {
7+
/**
8+
* Allows host to pass in a theme
9+
*/
10+
customTheme?: Theme;
11+
};
12+
13+
export const ToolContext = createContext({ useFluent: false as boolean } as const);
14+
15+
/**
16+
* 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
17+
* Today we will only enable fluent if the URL has the `newUX` query parameter is truthy
18+
* @param props
19+
* @returns
20+
*/
21+
export const FluentToolWrapper: FunctionComponent<PropsWithChildren<ToolHostProps>> = (props) => {
22+
const url = new URL(window.location.href);
23+
const enableFluent = url.searchParams.has("newUX") || url.hash.includes("newUX");
24+
25+
return enableFluent ? (
26+
<FluentProvider theme={props.customTheme || webDarkTheme}>
27+
<ToolContext.Provider value={{ useFluent: true }}>{props.children}</ToolContext.Provider>
28+
</FluentProvider>
29+
) : (
30+
props.children
31+
);
32+
};
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { Body1Strong, Button, InfoLabel, makeStyles, tokens } from "@fluentui/react-components";
2+
import { Add24Filled, Copy24Regular, Subtract24Filled } from "@fluentui/react-icons";
3+
import type { FunctionComponent, PropsWithChildren } from "react";
4+
import { useState } from "react";
5+
import { copyCommandToClipboard } from "../../copyCommandToClipboard";
6+
7+
const usePropertyLineStyles = makeStyles({
8+
container: {
9+
width: "100%",
10+
display: "flex",
11+
flexDirection: "column", // Stack line + expanded content
12+
borderBottom: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`,
13+
},
14+
line: {
15+
display: "flex",
16+
alignItems: "center",
17+
justifyContent: "flex-start",
18+
padding: `${tokens.spacingVerticalXS} 0px`,
19+
width: "100%",
20+
},
21+
label: {
22+
width: "33%",
23+
textAlign: "left",
24+
},
25+
rightContent: {
26+
width: "67%",
27+
display: "flex",
28+
alignItems: "center",
29+
justifyContent: "flex-end",
30+
},
31+
button: {
32+
marginLeft: tokens.spacingHorizontalXXS,
33+
width: "100px",
34+
},
35+
fillRestOfRightContentWidth: {
36+
flex: 1,
37+
display: "flex",
38+
justifyContent: "flex-end",
39+
alignItems: "center",
40+
},
41+
expandedContent: {
42+
backgroundColor: tokens.colorNeutralBackground1,
43+
},
44+
});
45+
46+
export type PropertyLineProps = {
47+
/**
48+
* The name of the property to display in the property line.
49+
*/
50+
label: string;
51+
/**
52+
* Optional description for the property, shown on hover of the info icon
53+
*/
54+
description?: string;
55+
/**
56+
* Optional function returning a string to copy to clipboard.
57+
*/
58+
onCopy?: () => string;
59+
/**
60+
* If supplied, an 'expand' icon will be shown which, when clicked, renders this component within the property line.
61+
*/
62+
expandedContent?: JSX.Element;
63+
};
64+
65+
/**
66+
* A reusable component that renders a property line with a label and child content, and an optional description, copy button, and expandable section.
67+
*
68+
* @param props - The properties for the PropertyLine component.
69+
* @returns A React element representing the property line.
70+
*
71+
*/
72+
export const PropertyLine: FunctionComponent<PropsWithChildren<PropertyLineProps>> = (props) => {
73+
const classes = usePropertyLineStyles();
74+
const [expanded, setExpanded] = useState(false);
75+
76+
const { label, description, onCopy, expandedContent, children } = props;
77+
78+
return (
79+
<div className={classes.container}>
80+
<div className={classes.line}>
81+
<InfoLabel className={classes.label} info={description}>
82+
<Body1Strong>{label}</Body1Strong>
83+
</InfoLabel>
84+
<div className={classes.rightContent}>
85+
<div className={classes.fillRestOfRightContentWidth}>{children}</div>
86+
87+
{expandedContent && (
88+
<Button
89+
appearance="subtle"
90+
icon={expanded ? <Subtract24Filled /> : <Add24Filled />}
91+
className={classes.button}
92+
onClick={(e) => {
93+
e.stopPropagation();
94+
setExpanded((expanded) => !expanded);
95+
}}
96+
/>
97+
)}
98+
99+
{onCopy && (
100+
<Button className={classes.button} id="copyProperty" icon={<Copy24Regular />} onClick={() => copyCommandToClipboard(onCopy())} title="Copy to clipboard" />
101+
)}
102+
</div>
103+
</div>
104+
105+
{expanded && expandedContent && <div className={classes.expandedContent}>{expandedContent}</div>}
106+
</div>
107+
);
108+
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { PropertyLine } from "./propertyLine";
2+
import type { PropertyLineProps } from "./propertyLine";
3+
import { SyncedSliderInput } from "../primitives/syncedSlider";
4+
import type { SyncedSliderProps } from "../primitives/syncedSlider";
5+
6+
export type SyncedSliderLineProps<O, K> = PropertyLineProps &
7+
Omit<SyncedSliderProps, "value" | "onChange"> & {
8+
/**
9+
* String key
10+
*/
11+
propertyKey: K;
12+
/**
13+
* target where O[K] is a number
14+
*/
15+
target: O;
16+
/**
17+
* Callback when either the slider or input value changes
18+
*/
19+
onChange?: (value: number) => void;
20+
};
21+
22+
/**
23+
* 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)
24+
* When value changes, updates the object with the new value and calls the onChange callback.
25+
*
26+
* Example usage looks like
27+
* \<SyncedSliderLine propertyKey="x" target=\{vector\} /\>
28+
* \<SyncedSliderLine propertyKey="r" target=\{color\} /\>
29+
* @param props
30+
* @returns
31+
*/
32+
export const SyncedSliderLine = <O extends Record<K, number>, K extends PropertyKey>(props: SyncedSliderLineProps<O, K>): React.ReactElement => {
33+
return (
34+
<PropertyLine {...props}>
35+
<SyncedSliderInput
36+
{...props}
37+
value={props.target[props.propertyKey]}
38+
onChange={(val) => {
39+
props.target[props.propertyKey] = val as O[K];
40+
props.onChange?.(val);
41+
}}
42+
/>
43+
</PropertyLine>
44+
);
45+
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { FunctionComponent } from "react";
2+
3+
import { Body1 } from "@fluentui/react-components";
4+
import { PropertyLine } from "./propertyLine";
5+
import type { PropertyLineProps } from "./propertyLine";
6+
7+
import { SyncedSliderLine } from "./syncedSliderLine";
8+
9+
import { Vector4 } from "core/Maths/math.vector";
10+
import type { Vector3 } from "core/Maths/math.vector";
11+
12+
type VectorSliderProps = {
13+
vector: Vector3 | Vector4;
14+
min?: number;
15+
max?: number;
16+
step?: number;
17+
};
18+
19+
const VectorSliders: FunctionComponent<VectorSliderProps> = (props) => {
20+
const { vector, ...sliderProps } = props;
21+
return (
22+
<>
23+
<SyncedSliderLine label="X" propertyKey="x" target={vector} {...sliderProps} />
24+
<SyncedSliderLine label="Y" propertyKey="y" target={vector} {...sliderProps} />
25+
<SyncedSliderLine label="Z" propertyKey="z" target={vector} {...sliderProps} />
26+
{vector instanceof Vector4 && <SyncedSliderLine label="W" propertyKey="w" target={vector} {...sliderProps} />}
27+
</>
28+
);
29+
};
30+
31+
/**
32+
* Reusable component which renders a vector property line containing a label, vector value, and expandable XYZW values
33+
* The expanded section contains a slider/input box for each component of the vector (x, y, z, w)
34+
* @param props
35+
* @returns
36+
*/
37+
export const VectorPropertyLine: FunctionComponent<VectorSliderProps & PropertyLineProps> = (props) => {
38+
return (
39+
<PropertyLine {...props} expandedContent={<VectorSliders {...props} />}>
40+
<Body1>{props.vector.toString()}</Body1>
41+
</PropertyLine>
42+
);
43+
};

0 commit comments

Comments
 (0)