Initial Move from Azure DevOps to GitHub

This commit is contained in:
Steve Faulkner
2020-05-25 21:30:55 -05:00
commit 36581fb6d9
986 changed files with 195242 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
@import "../../../../less/Common/Constants";
@ButtonIconSize: 18px;
.commandBar {
padding-left: @DefaultSpace;
border-bottom: @ButtonBorderWidth solid @BaseMedium;
display: flex;
overflow: hidden;
height: @topcommandbarheight;
.staticCommands {
list-style: none;
margin: 0px;
padding: 0px;
display: flex;
flex: 0 0 auto;
}
.overflowCommands {
display:flex;
flex: 1 0 auto;
.visibleCommands {
display: inline-flex;
list-style: none;
margin: 0px;
padding: 0px;
}
.partialSplitterContainer {
padding: @SmallSpace @DefaultSpace @SmallSpace @SmallSpace;
.flex-display();
}
}
.commandExpand {
border: none;
padding: 0px;
direction: rtl;
&:hover {
.hover();
cursor: pointer;
& > .commandDropdownContainer {
display: block !important; // TODO: Remove after reusing KO mouseover and mouseout event handlers
}
}
&:focus {
.focus();
}
.commandDropdownLauncher {
direction: ltr;
padding-top: @SmallSpace;
.commandIcon {
vertical-align: text-top;
}
.commandBarEllipses {
font-weight: bold;
font-size: 20px;
}
}
}
.hiddenCommandsContainer > .commandDropdownLauncher {
padding: 0px @DefaultSpace;
}
.commandDropdownContainer {
display: none;
z-index: 1000;
direction: ltr;
position: absolute;
width: fit-content;
padding: 0px;
background-color: @BaseLight;
box-shadow: 1px 2px 6px @BaseMediumHigh, -2px 2px 6px @BaseMediumHigh;
.commandDropdown {
display: flex;
flex-direction: column;
padding: 0px;
margin: 0px;
}
}
.feedbackButton {
margin-right: @LargeSpace;
white-space: nowrap;
}
}
command-button,
.commandButtonReact {
display: inline-flex;
.commandButtonComponent {
width: 100%;
color: @BaseHigh;
background-color: transparent;
text-decoration: none;
border: @ButtonBorderWidth solid transparent;
.flex-display();
&:hover:not(.commandDisabled) {
cursor: pointer;
.hover();
}
&:active:not(.commandDisabled) {
border: @ButtonBorderWidth dashed @AccentMedium;
.active();
}
&:focus:not(.commandDisabled) {
border: @ButtonBorderWidth dashed @AccentMedium;
}
.commandContent {
padding: @DefaultSpace @DefaultSpace @DefaultSpace;
flex: 0 0 auto;
.commandIcon {
margin: 0 @SmallSpace 0 0;
vertical-align: text-top;
width: @ButtonIconSize;
height: @ButtonIconSize;
}
.commandLabel {
padding: 0px;
}
}
.commandContent .hasHiddenItems {
padding-right: @SmallSpace;
}
}
.commandButtonComponent.commandDisabled {
color: @BaseMediumHigh;
opacity: 0.5;
}
.commandExpand {
padding-top: @SmallSpace;
padding-bottom: @SmallSpace;
&:hover {
.hover();
& > .commandDropdownContainer {
display: block !important; // TODO: Remove after reusing KO mouseover and mouseout event handlers
}
}
&:focus {
.focus();
}
.commandDropdownLauncher {
cursor: pointer;
display: inline-flex;
.commandButtonComponent {
padding: 0px;
}
}
.expandDropdown {
padding: @SmallSpace;
img {
vertical-align: top;
}
}
.partialSplitter {
margin: @SmallSpace 0px 6px;
}
}
.commandButtonComponent[tabindex]:focus {
outline: none;
}
.selectedButton {
background-color: @AccentLow;
outline: none
}
}
.partialSplitter {
border-left: @ButtonBorderWidth solid @BaseMediumHigh;
}
.commandDropdown .commandButtonComponent {
padding-left: 0px;
}

View File

@@ -0,0 +1,139 @@
import * as ko from "knockout";
import { CommandButtonComponent, CommandButtonOptions } from "./CommandButton";
const mockLabel = "Some Label";
const id = "Some id";
function buildComponent(buttonOptions: any) {
document.body.innerHTML = CommandButtonComponent.template as any;
const vm = new CommandButtonComponent.viewModel(buttonOptions);
ko.applyBindings(vm);
}
describe("Command Button Component", () => {
function buildButtonOptions(
onClick: () => void,
id?: string,
label?: string,
disabled?: ko.Observable<boolean>,
visible?: ko.Observable<boolean>,
tooltipText?: string
): { buttonProps: CommandButtonOptions } {
return {
buttonProps: {
iconSrc: "images/AddCollection.svg",
id: id,
commandButtonLabel: label || mockLabel,
disabled: disabled,
visible: visible,
tooltipText: tooltipText,
hasPopup: false,
onCommandClick: onClick
}
};
}
function buildSplitterButtonOptions(
onClick: () => void,
id?: string,
label?: string,
disabled?: ko.Observable<boolean>,
visible?: ko.Observable<boolean>,
tooltipText?: string
): { buttonProps: CommandButtonOptions } {
const child: CommandButtonOptions = {
iconSrc: "images/settings_15x15.svg",
id: id,
commandButtonLabel: label || mockLabel,
disabled: disabled,
visible: visible,
tooltipText: tooltipText,
hasPopup: false,
onCommandClick: onClick
};
return {
buttonProps: {
iconSrc: "images/AddCollection.svg",
id: id,
commandButtonLabel: label || mockLabel,
disabled: disabled,
visible: visible,
tooltipText: tooltipText,
hasPopup: false,
onCommandClick: onClick,
children: [child]
}
};
}
afterEach(() => {
ko.cleanNode(document);
document.body.innerHTML = "";
});
describe("Rendering", () => {
it("should display button label", () => {
const buttonOptions = buildButtonOptions(() => {
/** do nothing **/
}, mockLabel);
buildComponent(buttonOptions);
expect(document.getElementsByClassName("commandButtonComponent").item(0).textContent).toContain(mockLabel);
});
it("should display button icon", () => {
const buttonOptions = buildButtonOptions(() => {
/** do nothing **/
});
buildComponent(buttonOptions);
expect(
document
.getElementsByTagName("img")
.item(0)
.getAttribute("src")
).toBeDefined();
});
});
describe("Behavior", () => {
let clickSpy: jasmine.Spy;
beforeEach(() => {
clickSpy = jasmine.createSpy("Command button click spy");
});
it("should trigger the click handler when the command button is clicked", () => {
const buttonOptions = buildButtonOptions(() => clickSpy());
buildComponent(buttonOptions);
document
.getElementsByClassName("commandButtonComponent")
.item(0)
.dispatchEvent(new Event("click"));
expect(clickSpy).toHaveBeenCalled();
});
it("should not trigger the click handler when command button is disabled", () => {
const buttonOptions = buildButtonOptions(() => clickSpy(), id, mockLabel, ko.observable(true));
buildComponent(buttonOptions);
document
.getElementsByClassName("commandButtonComponent")
.item(0)
.dispatchEvent(new Event("click"));
expect(clickSpy).not.toHaveBeenCalled();
});
it("should not have a dropdown if it has no child", () => {
const buttonOptions = buildButtonOptions(() => clickSpy(), id, mockLabel, ko.observable(true));
buildComponent(buttonOptions);
const dropdownSize = document.getElementsByClassName("commandExpand").length;
expect(dropdownSize).toBe(0);
});
it("should have a dropdown if it has a child", () => {
const buttonOptions = buildSplitterButtonOptions(() => clickSpy(), id, mockLabel, ko.observable(true));
buildComponent(buttonOptions);
const dropdownSize = document.getElementsByClassName("commandExpand").length;
expect(dropdownSize).toBe(1);
});
});
});

View File

@@ -0,0 +1,191 @@
/**
* How to use this component:
*
* In your html markup, use:
* <command-button params="{
* iconSrc: '/icon/example/src/',
* onCommandClick: () => { doSomething },
* commandButtonLabel: 'Some Label'
* disabled: true/false
* }">
* </command-button>
*
*/
import * as ko from "knockout";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel";
import { KeyCodes } from "../../../Common/Constants";
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import template from "./command-button.html";
/**
* Options for this component
*/
export interface CommandButtonOptions {
/**
* image source for the button icon
*/
iconSrc: string;
/**
* Id for the button icon
*/
id: string;
/**
* Click handler for command button click
*/
onCommandClick: () => void;
/**
* Label for the button
*/
commandButtonLabel: string | ko.Observable<string>;
/**
* True if this button opens a tab or pane, false otherwise.
*/
hasPopup: boolean;
/**
* Enabled/disabled state of command button
*/
disabled?: ko.Subscribable<boolean>;
/**
* Visibility/Invisibility of the button
*/
visible?: ko.Subscribable<boolean>;
/**
* Whether or not the button should have the 'selectedButton' styling
*/
isSelected?: ko.Observable<boolean>;
/**
* Text to displayed in the tooltip on hover
*/
tooltipText?: string | ko.Observable<string>;
/**
* Callback triggered when the template is bound to the component
*/
onTemplateReady?: () => void;
/**
* tabindex for the command button
*/
tabIndex?: ko.Observable<number>;
/**
* Childrens command buttons to hide in the dropdown
*/
children?: CommandButtonOptions[];
}
export class CommandButtonViewModel extends WaitsForTemplateViewModel implements ViewModels.CommandButton {
public commandClickCallback: () => void;
public commandButtonId: string;
public disabled: ko.Subscribable<boolean>;
public visible: ko.Subscribable<boolean>;
public isSelected: ko.Observable<boolean>;
public iconSrc: string;
public commandButtonLabel: ko.Observable<string>;
public tooltipText: ko.Observable<string>;
public tabIndex: ko.Observable<number>;
public isTemplateReady: ko.Observable<boolean>;
public hasPopup: boolean;
public children: ko.ObservableArray<CommandButtonOptions>;
public constructor(options: { buttonProps: CommandButtonOptions }) {
super();
const props = options.buttonProps;
const commandButtonLabel = props.commandButtonLabel;
const tooltipText = props.tooltipText;
this.commandButtonLabel =
typeof commandButtonLabel === "string" ? ko.observable<string>(commandButtonLabel) : commandButtonLabel;
this.commandButtonId = props.id;
this.disabled = props.disabled || ko.observable(false);
this.visible = props.visible || ko.observable(true);
this.isSelected = props.isSelected || ko.observable(false);
this.iconSrc = props.iconSrc;
this.tabIndex = props.tabIndex || ko.observable(0);
this.hasPopup = props.hasPopup;
this.children = ko.observableArray(props.children);
super.onTemplateReady((isTemplateReady: boolean) => {
if (isTemplateReady && props.onTemplateReady) {
props.onTemplateReady();
}
});
if (tooltipText && typeof tooltipText === "string") {
this.tooltipText = ko.observable<string>(tooltipText);
} else if (tooltipText && typeof tooltipText === "function") {
this.tooltipText = tooltipText;
} else {
this.tooltipText = this.commandButtonLabel;
}
this.commandClickCallback = () => {
if (this.disabled()) {
return;
}
const el = document.querySelector(".commandDropdownContainer") as HTMLElement;
if (el) {
el.style.display = "none";
}
props.onCommandClick();
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
commandButtonClicked: this.commandButtonLabel
});
};
}
public onKeyPress(source: any, event: KeyboardEvent): boolean {
if (event.keyCode === KeyCodes.Space || event.keyCode === KeyCodes.Enter) {
this.commandClickCallback && this.commandClickCallback();
event.stopPropagation();
return false;
}
return true;
}
public onLauncherKeyDown(source: any, event: KeyboardEvent): boolean {
// TODO: Convert JQuery code into Knockout
if (event.keyCode === KeyCodes.DownArrow) {
$(event.target)
.parent()
.siblings()
.children(".commandExpand")
.children(".commandDropdownContainer")
.hide();
$(event.target)
.children(".commandDropdownContainer")
.show()
.focus();
event.stopPropagation();
return false;
}
if (event.keyCode === KeyCodes.UpArrow) {
$(event.target)
.children(".commandDropdownContainer")
.hide();
event.stopPropagation();
return false;
}
return true;
}
}
/**
* Helper class for ko component registration
*/
export const CommandButtonComponent = {
viewModel: CommandButtonViewModel,
template
};

View File

@@ -0,0 +1,280 @@
import { StringUtils } from "../../../Utils/StringUtils";
import { KeyCodes } from "../../../Common/Constants";
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
/**
* React component for Command button component.
*/
import * as React from "react";
import { ArcadiaMenuPickerProps } from "../Arcadia/ArcadiaMenuPicker";
/**
* Options for this component
*/
export interface CommandButtonComponentProps {
/**
* image source for the button icon
*/
iconSrc: string;
/**
* image alt for accessibility
*/
iconAlt: string;
/**
* Click handler for command button click
*/
onCommandClick: (e: React.SyntheticEvent) => void;
/**
* Label for the button
*/
commandButtonLabel: string;
/**
* True if this button opens a tab or pane, false otherwise.
*/
hasPopup: boolean;
/**
* Enabled/disabled state of command button
*/
disabled?: boolean;
/**
* Whether or not the button should have the 'selectedButton' styling
*/
isSelected?: boolean;
/**
* Text to displayed in the tooltip on hover
*/
tooltipText?: string;
/**
* tabindex for the command button
*/
tabIndex?: number;
/**
* Childrens command buttons to hide in the dropdown
*/
children?: CommandButtonComponentProps[];
/**
* Optional id
*/
id?: string;
/**
* Optional class name
*/
className?: string;
/**
* If true, display as dropdown
*/
isDropdown?: boolean;
/**
* Placeholder if dropdown
*/
dropdownPlaceholder?: string;
/**
* Dropdown selection
*/
dropdownSelectedKey?: string;
/**
* This is the key of the dropdown item
* The text is commandLabel
*/
dropdownItemKey?: string;
/**
* Possible width
*/
dropdownWidth?: number;
/**
* Vertical bar to divide buttons
*/
isDivider?: boolean;
/**
* Aria-label for the button
*/
ariaLabel: string;
//TODO: generalize customized command bar
/**
* If set to true, will render arcadia picker
*/
isArcadiaPicker?: boolean;
/**
* props to render arcadia picker
*/
arcadiaProps?: ArcadiaMenuPickerProps;
}
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {
private dropdownElt: HTMLElement;
private expandButtonElt: HTMLElement;
public componentDidUpdate(): void {
if (!this.dropdownElt || !this.expandButtonElt) {
return;
}
const dropdownElt = $(this.dropdownElt).offset({ left: $(this.expandButtonElt).offset().left });
}
private onKeyPress(event: React.KeyboardEvent): boolean {
if (event.keyCode === KeyCodes.Space || event.keyCode === KeyCodes.Enter) {
this.commandClickCallback && this.commandClickCallback(event);
event.stopPropagation();
return false;
}
return true;
}
private onLauncherKeyDown(event: React.KeyboardEvent<HTMLDivElement>): boolean {
if (event.keyCode === KeyCodes.DownArrow) {
$(this.dropdownElt).hide();
$(this.dropdownElt)
.show()
.focus();
event.stopPropagation();
return false;
}
if (event.keyCode === KeyCodes.UpArrow) {
$(this.dropdownElt).hide();
event.stopPropagation();
return false;
}
return true;
}
private getCommandButtonId(): string {
if (this.props.id) {
return this.props.id;
} else {
return `commandButton-${StringUtils.stripSpacesFromString(this.props.commandButtonLabel)}`;
}
}
public static renderButton(options: CommandButtonComponentProps, key?: string): JSX.Element {
return <CommandButtonComponent key={key} {...options} />;
}
private commandClickCallback(e: React.SyntheticEvent): void {
if (this.props.disabled) {
return;
}
// TODO Query component's parent, not document
const el = document.querySelector(".commandDropdownContainer") as HTMLElement;
if (el) {
el.style.display = "none";
}
this.props.onCommandClick(e);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
commandButtonClicked: this.props.commandButtonLabel
});
}
private renderChildren(): JSX.Element {
if (!this.props.children || this.props.children.length < 1) {
return <React.Fragment />;
}
return (
<div
className="commandExpand"
tabIndex={0}
ref={(ref: HTMLElement) => {
this.expandButtonElt = ref;
}}
onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => this.onLauncherKeyDown(e)}
>
<div className="commandDropdownLauncher">
<span className="partialSplitter" />
<span className="expandDropdown">
<img src={CollapseChevronDownIcon} />
</span>
</div>
<div
className="commandDropdownContainer"
ref={(ref: HTMLElement) => {
this.dropdownElt = ref;
}}
>
<div className="commandDropdown">
{this.props.children.map(
(c: CommandButtonComponentProps, index: number): JSX.Element => {
return CommandButtonComponent.renderButton(c, `${index}`);
}
)}
</div>
</div>
</div>
);
}
public static renderLabel(
props: CommandButtonComponentProps,
key?: string,
refct?: (input: HTMLElement) => void
): JSX.Element {
if (!props.commandButtonLabel) {
return <React.Fragment />;
}
return (
<span className="commandLabel" key={key} ref={refct}>
{props.commandButtonLabel}
</span>
);
}
public render(): JSX.Element {
let mainClassName = "commandButtonComponent";
if (this.props.disabled) {
mainClassName += " commandDisabled";
}
if (this.props.isSelected) {
mainClassName += " selectedButton";
}
let contentClassName = "commandContent";
if (this.props.children && this.props.children.length > 0) {
contentClassName += " hasHiddenItems";
}
return (
<div className="commandButtonReact">
<span
className={mainClassName}
role="menuitem"
tabIndex={this.props.tabIndex}
onKeyPress={(e: React.KeyboardEvent<HTMLSpanElement>) => this.onKeyPress(e)}
title={this.props.tooltipText}
id={this.getCommandButtonId()}
aria-disabled={this.props.disabled}
aria-haspopup={this.props.hasPopup}
aria-label={this.props.ariaLabel}
onClick={(e: React.MouseEvent<HTMLSpanElement>) => this.commandClickCallback(e)}
>
<div className={contentClassName}>
<img className="commandIcon" src={this.props.iconSrc} alt={this.props.iconAlt} />
{CommandButtonComponent.renderLabel(this.props)}
</div>
</span>
{this.props.children && this.renderChildren()}
</div>
);
}
}

View File

@@ -0,0 +1,40 @@
<span
class="commandButtonComponent"
role="menuitem"
tabindex="0"
data-bind="setTemplateReady: true,
css: {
commandDisabled: disabled,
selectedButton: isSelected
},
event: {
keypress: onKeyPress
},
attr: {
title: tooltipText,
id: commandButtonId,
tabindex: tabIndex ,
'aria-disabled': disabled,
'aria-haspopup': hasPopup
},
click: commandClickCallback,
visible: visible"
>
<div class="commandContent" data-bind="css: { hasHiddenItems: children().length > 0 }">
<img class="commandIcon" data-bind="attr: {src: iconSrc, alt: commandButtonLabel}" />
<span class="commandLabel" data-bind="text: commandButtonLabel"></span>
</div>
</span>
<!-- ko if: children().length > 0 -->
<div class="commandExpand" tabindex="0" data-bind="visible: visible, event: { keydown: onLauncherKeyDown }">
<div class="commandDropdownLauncher">
<span class="partialSplitter"></span>
<span class="expandDropdown"> <img src="/QueryBuilder/CollapseChevronDown_16x.png" /> </span>
</div>
<div class="commandDropdownContainer">
<div class="commandDropdown" data-bind="foreach: children">
<command-button params="{buttonProps: $data}"></command-button>
</div>
</div>
</div>
<!-- /ko -->