Skip to content

[Fluent] DraggableLine, SpinButton/FLoatLineComponent, FileUploadLine with icon for NME #16876

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 14, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 19 additions & 10 deletions packages/dev/sharedUiComponents/src/fluent/hoc/buttonLine.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,42 @@
import { Body1, Button, makeStyles, tokens } from "@fluentui/react-components";
import { Body1, Button as FluentButton, makeStyles, tokens } from "@fluentui/react-components";
import { LineContainer } from "./propertyLine";
import type { FunctionComponent } from "react";
import type { FluentIcon } from "@fluentui/react-icons";

const useButtonLineStyles = makeStyles({
button: {
border: `1px solid ${tokens.colorBrandBackground}`,
},
});

export type ButtonLineProps = {
label: string;
export type ButtonProps = {
onClick: () => void;
icon?: FluentIcon;
label: string;
disabled?: boolean;
icon?: string;
title?: string;
};

/**
* Wraps a button with a label in a line container
* @param props Button props plus a label
* @returns A button inside a line
*/
export const ButtonLine: FunctionComponent<ButtonLineProps> = (props) => {
const classes = useButtonLineStyles();
export const ButtonLine: FunctionComponent<ButtonProps> = (props) => {
return (
<LineContainer>
<Button className={classes.button} {...props}>
<Body1>{props.label}</Body1>
</Button>
<Button {...props} />
</LineContainer>
);
};

export const Button: FunctionComponent<ButtonProps> = (props) => {
const classes = useButtonLineStyles();
// eslint-disable-next-line @typescript-eslint/naming-convention
const { icon: Icon, ...buttonProps } = props;

return (
<FluentButton iconPosition="after" className={classes.button} {...buttonProps} icon={Icon && <Icon />}>
<Body1>{props.label}</Body1>
</FluentButton>
);
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useRef } from "react";
import type { FunctionComponent } from "react";
import { ButtonLine } from "./buttonLine";
import type { ButtonLineProps } from "./buttonLine";
import type { ButtonProps } from "./buttonLine";
import { DocumentAdd16Regular } from "@fluentui/react-icons";

type FileUploadLineProps = Omit<ButtonLineProps, "onClick"> & {
type FileUploadLineProps = Omit<ButtonProps, "onClick"> & {
onClick: (files: FileList) => void;
accept: string;
};
Expand All @@ -25,7 +26,7 @@ export const FileUploadLine: FunctionComponent<FileUploadLineProps> = (props) =>

return (
<>
<ButtonLine onClick={handleButtonClick} label={props.label}></ButtonLine>
<ButtonLine onClick={handleButtonClick} icon={DocumentAdd16Regular} label={props.label}></ButtonLine>
<input ref={inputRef} type="file" accept={props.accept} style={{ display: "none" }} onChange={handleChange} />
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ const usePropertyLineStyles = makeStyles({
width: "100%",
display: "flex",
flexDirection: "column", // Stack line + expanded content
padding: `${tokens.spacingVerticalXS} 0px`,
borderBottom: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`,
},
line: {
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
padding: `${tokens.spacingVerticalXS} 0px`,
width: "100%",
},
label: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { makeStyles, tokens } from "@fluentui/react-components";
import { DeleteFilled } from "@fluentui/react-icons";
import { LineContainer } from "../hoc/propertyLine";

export type DraggableLineProps = {
format: string;
data: string;
tooltip: string;
label: string;
onDelete?: () => void;
};

const useDraggableStyles = makeStyles({
draggable: {
display: "inline-flex",
alignItems: "center",
columnGap: tokens.spacingHorizontalS,
cursor: "grab",
textAlign: "center",
boxSizing: "border-box",
borderBottom: "black",
margin: `${tokens.spacingVerticalXS} 0px`,
border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`,

// eslint-disable-next-line @typescript-eslint/naming-convention
":hover": {
backgroundColor: tokens.colorBrandBackground2Hover,
},
},
icon: {
pointerEvents: "auto", // re‑enable interaction
display: "flex",
alignItems: "center",
},
});

export const DraggableLine: React.FunctionComponent<DraggableLineProps> = (props) => {
const classes = useDraggableStyles();
return (
<div
className={classes.draggable}
title={props.tooltip}
draggable={true}
onDragStart={(event) => {
event.dataTransfer.setData(props.format, props.data);
}}
>
<LineContainer>
{props.label}
{props.onDelete && <DeleteFilled className={classes.icon} onClick={props.onDelete} />}
</LineContainer>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { makeStyles, SpinButton as FluentSpinButton } from "@fluentui/react-comp
import type { SpinButtonOnChangeData, SpinButtonChangeEvent } from "@fluentui/react-components";
import type { FunctionComponent } from "react";
import { useCallback, useState } from "react";
import type { BaseComponentProps } from "../hoc/propertyLine";
import { PropertyLine, type BaseComponentProps, type PropertyLineProps } from "../hoc/propertyLine";

const useSpinStyles = makeStyles({
base: {
Expand All @@ -11,19 +11,24 @@ const useSpinStyles = makeStyles({
},
});

export type SpinButtonProps = BaseComponentProps<number>;
export type SpinButtonProps = BaseComponentProps<number> & {
precision?: number; // Optional precision for the spin button
step?: number; // Optional step value for the spin button
min?: number;
max?: number;
};

export const SpinButton: FunctionComponent<SpinButtonProps> = (props) => {
const classes = useSpinStyles();

const [spinButtonValue, setSpinButtonValue] = useState<number | null>(props.value);
const [spinButtonValue, setSpinButtonValue] = useState<number>(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 !== undefined) {
if (data.value != null) {
setSpinButtonValue(data.value);
} else if (data.displayValue !== undefined) {
const newValue = parseFloat(data.displayValue);
Expand All @@ -37,7 +42,13 @@ export const SpinButton: FunctionComponent<SpinButtonProps> = (props) => {

return (
<div className={classes.base}>
<FluentSpinButton value={spinButtonValue} onChange={onSpinButtonChange} />
<FluentSpinButton {...props} value={spinButtonValue} onChange={onSpinButtonChange} />
</div>
);
};

export const SpinButtonPropertyLine: FunctionComponent<SpinButtonProps & PropertyLineProps> = (props) => (
<PropertyLine {...props}>
<SpinButton {...props} />
</PropertyLine>
);
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ export class ButtonLineComponent extends React.Component<IButtonLineComponentPro
}

renderFluent() {
return <ButtonLine label={this.props.label} icon={this.props.icon} title={this.props.label} onClick={this.props.onClick} disabled={this.props.isDisabled} />;
// NOTE when callsit replaces usage of this with the ButtonLine, can pass in a true fluent icon
const fluentIcon = this.props.icon ? () => <img src={this.props.icon} title={this.props.iconLabel} alt={this.props.iconLabel} className="icon" /> : undefined;
return <ButtonLine label={this.props.label} icon={fluentIcon} onClick={this.props.onClick} disabled={this.props.isDisabled} />;
}
renderOriginal() {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
import * as React from "react";
import { useContext } from "react";
import { ToolContext } from "shared-ui-components/fluent/hoc/fluentToolWrapper";
import { DraggableLine } from "shared-ui-components/fluent/primitives/draggable";

export interface IButtonLineComponentProps {
export type DraggableLineProps = {
format: string;
data: string;
tooltip: string;
}
};

export class DraggableLineComponent extends React.Component<IButtonLineComponentProps> {
constructor(props: IButtonLineComponentProps) {
super(props);
export const DraggableLineComponent: React.FunctionComponent<DraggableLineProps> = (props) => {
const useFluent = useContext(ToolContext);
if (useFluent) {
// When updating the callsites to use fluent directly this label will be clearer since the string replace occurs where the Block_Foo lives
return <DraggableLine {...props} label={props.data.replace("Block", "")} />;
}

override render() {
return (
<div
className="draggableLine"
title={this.props.tooltip}
draggable={true}
onDragStart={(event) => {
event.dataTransfer.setData(this.props.format, this.props.data);
}}
>
{this.props.data.replace("Block", "")}
</div>
);
}
}
return (
<div
className="draggableLine"
title={props.tooltip}
draggable={true}
onDragStart={(event) => {
event.dataTransfer.setData(props.format, props.data);
}}
>
{props.data.replace("Block", "")}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -1,41 +1,42 @@
import * as React from "react";
import { useContext } from "react";
import { ToolContext } from "../fluent/hoc/fluentToolWrapper";
import { DraggableLine } from "../fluent/primitives/draggable";

export interface IDraggableLineWithButtonComponent {
export type DraggableLineProps = {
format: string;
data: string;
tooltip: string;
iconImage: any;
onIconClick: (value: string) => void;
iconTitle: string;
lenSuffixToRemove?: number;
}
};

export class DraggableLineWithButtonComponent extends React.Component<IDraggableLineWithButtonComponent> {
constructor(props: IDraggableLineWithButtonComponent) {
super(props);
export const DraggableLineWithButtonComponent: React.FunctionComponent<DraggableLineProps> = (props) => {
const useFluent = useContext(ToolContext);
if (useFluent) {
// When updating the callsites to use fluent directly this label will be clearer since the string replace occurs where the data string lives
return <DraggableLine {...props} label={props.data.substring(0, props.data.length - (props.lenSuffixToRemove ?? 6))} onDelete={() => props.onIconClick(props.data)} />;
}

override render() {
return (
return (
<div
className="draggableLine withButton"
title={props.tooltip}
draggable={true}
onDragStart={(event) => {
event.dataTransfer.setData(props.format, props.data);
}}
>
{props.data.substring(0, props.data.length - (props.lenSuffixToRemove ?? 6))}
<div
className="draggableLine withButton"
title={this.props.tooltip}
draggable={true}
onDragStart={(event) => {
event.dataTransfer.setData(this.props.format, this.props.data);
className="icon"
onClick={() => {
props.onIconClick(props.data);
}}
title={props.iconTitle}
>
{this.props.data.substring(0, this.props.data.length - (this.props.lenSuffixToRemove ?? 6))}
<div
className="icon"
onClick={() => {
this.props.onIconClick(this.props.data);
}}
title={this.props.iconTitle}
>
<img className="img" title={this.props.iconTitle} src={this.props.iconImage} />
</div>
<img className="img" title={props.iconTitle} src={props.iconImage} />
</div>
);
}
}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export class FileButtonLine extends React.Component<IFileButtonLineProps> {
}

renderFluent() {
return <FileUploadLine {...this.props} onClick={(file) => this.props.onClick(file[0])} />;
const { icon, ...fileProps } = this.props;
return <FileUploadLine {...fileProps} onClick={(file) => this.props.onClick(file[0])} />;
}

renderOriginal() {
Expand Down
26 changes: 25 additions & 1 deletion packages/dev/sharedUiComponents/src/lines/floatLineComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { conflictingValuesPlaceholder } from "./targetsProxy";
import { InputArrowsComponent } from "./inputArrowsComponent";
import { copyCommandToClipboard, getClassNameWithNamespace } from "../copyCommandToClipboard";
import copyIcon from "../imgs/copy.svg";
import { NumberInputPropertyLine } from "../fluent/hoc/inputPropertyLine";
import { ToolContext } from "../fluent/hoc/fluentToolWrapper";
import { SpinButtonPropertyLine } from "shared-ui-components/fluent/primitives/spinButton";

interface IFloatLineComponentProps {
label: string;
Expand Down Expand Up @@ -212,7 +215,25 @@ export class FloatLineComponent extends React.Component<IFloatLineComponentProps
}
}

override render() {
renderFluent() {
const props = {
label: this.props.label,
value: Number(this.state.value),
onChange: (value: number) => this.updateValue(value.toString()),
min: this.props.min,
max: this.props.max,
precision: this.props.digits ?? (this.props.isInteger ? 0 : undefined),
disabled: this.props.disabled,
step: this.props.step ? parseFloat(this.props.step) : undefined,
};

if (this.props.arrows) {
return <SpinButtonPropertyLine {...props} />;
}
return <NumberInputPropertyLine {...props} />;
}

renderOriginal() {
let valueAsNumber: number;

if (this.props.isInteger) {
Expand Down Expand Up @@ -295,4 +316,7 @@ export class FloatLineComponent extends React.Component<IFloatLineComponentProps
</>
);
}
override render() {
return <ToolContext.Consumer>{({ useFluent }) => (useFluent ? this.renderFluent() : this.renderOriginal())}</ToolContext.Consumer>;
}
}
Loading
Loading