Skip to content

Commit

Permalink
Initial implementation of a generic UI component (#61)
Browse files Browse the repository at this point in the history
* Add generic component

* Add validation. Rename to widgetRenderer

* Remove test code from splash screen

* Clean up infobox

* Fix styling/layout

* Move test code into unit test

* Replace <input> and <labe> by <TextField> and <Text> respectively. Fix style.

* Replace InfoBoxComponent with UI fabric MessageBar. Fix styling for TextField

* Use MessageBar for error message

* Rename WdigetRendererComponent to SmartUiComponent
  • Loading branch information
languy authored Jul 6, 2020
1 parent 3f34936 commit 27024ef
Show file tree
Hide file tree
Showing 8 changed files with 790 additions and 0 deletions.
11 changes: 11 additions & 0 deletions src/Common/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,17 @@ export class KeyCodes {
public static Tab: number = 9;
}

// Normalized per: https://www.w3.org/TR/uievents-key/#named-key-attribute-values
export class NormalizedEventKey {
public static readonly Space = " ";
public static readonly Enter = "Enter";
public static readonly Escape = "Escape";
public static readonly UpArrow = "ArrowUp";
public static readonly DownArrow = "ArrowDown";
public static readonly LeftArrow = "ArrowLeft";
public static readonly RightArrow = "ArrowRight";
}

export class TryCosmosExperience {
public static extendUrl: string = "https://trycosmosdb.azure.com/api/resource/extendportal?userId={0}";
public static deleteUrl: string = "https://trycosmosdb.azure.com/api/resource/deleteportal?userId={0}";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@import "../../../../less/Common/Constants.less";

.radioSwitchComponent {
cursor: pointer;
display: flex;

&>span:nth-child(n+2) {
margin-left: 10px;
}

.caption {
color: @BaseDark;
padding-left: @SmallSpace;
vertical-align: top;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Horizontal switch component
*/

import * as React from "react";
import "./RadioSwitchComponent.less";
import { Icon } from "office-ui-fabric-react/lib/Icon";
import { NormalizedEventKey } from "../../../Common/Constants";

export interface Choice {
key: string;
onSelect: () => void;
label: string;
}

export interface RadioSwitchComponentProps {
choices: Choice[];
selectedKey: string;
onSelectionKeyChange?: (newValue: string) => void;
}

export class RadioSwitchComponent extends React.Component<RadioSwitchComponentProps> {
public render(): JSX.Element {
return (
<div className="radioSwitchComponent">
{this.props.choices.map((choice: Choice) => (
<span
tabIndex={0}
key={choice.key}
onClick={() => this.onSelect(choice)}
onKeyPress={event => this.onKeyPress(event, choice)}
>
<Icon iconName={this.props.selectedKey === choice.key ? "RadioBtnOn" : "RadioBtnOff"} />
<span className="caption">{choice.label}</span>
</span>
))}
</div>
);
}

private onSelect(choice: Choice): void {
this.props.onSelectionKeyChange && this.props.onSelectionKeyChange(choice.key);
choice.onSelect();
}

private onKeyPress(event: React.KeyboardEvent<HTMLSpanElement>, choice: Choice): void {
if (event.key === NormalizedEventKey.Enter || event.key === NormalizedEventKey.Space) {
this.onSelect(choice);
}
}
}
35 changes: 35 additions & 0 deletions src/Explorer/Controls/SmartUi/InputUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* Utilities for validation */

export const onValidateValueChange = (newValue: string, minValue?: number, maxValue?: number): number => {
let numericValue = parseInt(newValue);
if (!isNaN(numericValue) && isFinite(numericValue)) {
if (minValue !== undefined && numericValue < minValue) {
numericValue = minValue;
}
if (maxValue !== undefined && numericValue > maxValue) {
numericValue = maxValue;
}

return Math.floor(numericValue);
}

return undefined;
};

export const onIncrementValue = (newValue: string, step: number, max?: number): number => {
const numericValue = parseInt(newValue);
if (!isNaN(numericValue) && isFinite(numericValue)) {
const newValue = numericValue + step;
return max !== undefined ? Math.min(max, newValue) : newValue;
}
return undefined;
};

export const onDecrementValue = (newValue: string, step: number, min?: number): number => {
const numericValue = parseInt(newValue);
if (!isNaN(numericValue) && isFinite(numericValue)) {
const newValue = numericValue - step;
return min !== undefined ? Math.max(min, newValue) : newValue;
}
return undefined;
};
14 changes: 14 additions & 0 deletions src/Explorer/Controls/SmartUi/SmartUiComponent.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@import "../../../../less/Common/Constants.less";

.widgetRendererContainer {
text-align: left;

.inputLabelContainer {
margin-bottom: 4px;

.inputLabel {
color: #393939;
font-weight: 600;
}
}
}
88 changes: 88 additions & 0 deletions src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React from "react";
import { shallow } from "enzyme";
import { SmartUiComponent, Descriptor, InputType } from "./SmartUiComponent";

describe("SmartUiComponent", () => {
const exampleData: Descriptor = {
root: {
id: "root",
info: {
message: "Start at $24/mo per database",
link: {
href: "https://aka.ms/azure-cosmos-db-pricing",
text: "More Details"
}
},
children: [
{
id: "throughput",
input: {
label: "Throughput (input)",
dataFieldName: "throughput",
type: "number",
min: 400,
max: 500,
step: 10,
defaultValue: 400,
inputType: "spin"
}
},
{
id: "throughput2",
input: {
label: "Throughput (Slider)",
dataFieldName: "throughput2",
type: "number",
min: 400,
max: 500,
step: 10,
defaultValue: 400,
inputType: "slider"
}
},
{
id: "containerId",
input: {
label: "Container id",
dataFieldName: "containerId",
type: "string"
}
},
{
id: "analyticalStore",
input: {
label: "Analytical Store",
trueLabel: "Enabled",
falseLabel: "Disabled",
defaultValue: true,
dataFieldName: "analyticalStore",
type: "boolean"
}
},
{
id: "database",
input: {
label: "Database",
dataFieldName: "database",
type: "enum",
choices: [
{ label: "Database 1", key: "db1", value: "database1" },
{ label: "Database 2", key: "db2", value: "database2" },
{ label: "Database 3", key: "db3", value: "database3" }
],
defaultKey: "db2"
}
}
]
}
};

const exampleCallbacks = (newValues: Map<string, InputType>): void => {
console.log("New values:", newValues);
};

it("should render", () => {
const wrapper = shallow(<SmartUiComponent descriptor={exampleData} onChange={exampleCallbacks} />);
expect(wrapper).toMatchSnapshot();
});
});
Loading

0 comments on commit 27024ef

Please sign in to comment.