From 8e1b55a184e77cc9c2c3156045e8adcaed8a2c7f Mon Sep 17 00:00:00 2001 From: Boris Emorine Date: Wed, 29 Mar 2017 17:11:30 -0700 Subject: [PATCH 01/64] First step at stepper implementation. --- apps/fabric-examples/src/AppDefinition.tsx | 7 +++++++ apps/fabric-website/src/components/App/AppState.ts | 5 +++++ .../src/components/Slider/Slider.tsx | 6 +++--- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/apps/fabric-examples/src/AppDefinition.tsx b/apps/fabric-examples/src/AppDefinition.tsx index eecc5381eb7dc..86652034b4be0 100644 --- a/apps/fabric-examples/src/AppDefinition.tsx +++ b/apps/fabric-examples/src/AppDefinition.tsx @@ -219,6 +219,13 @@ export const AppDefinition: IAppDefinition = { status: ExampleStatus.beta, url: '#/examples/spinner' }, + { + getComponent: cb => require.ensure([], () => cb(require('./pages/StepperPage/StepperPage').StepperPage)), + key: 'Stepper', + name: 'Stepper', + status: ExampleStatus.beta, + url: '#/examples/stepper' + }, { getComponent: cb => require.ensure([], () => cb(require('./pages/TeachingBubblePage/TeachingBubblePage').TeachingBubblePage)), key: 'TeachingBubble', diff --git a/apps/fabric-website/src/components/App/AppState.ts b/apps/fabric-website/src/components/App/AppState.ts index 5b325dfd27bb6..55cf14d8a5e2c 100644 --- a/apps/fabric-website/src/components/App/AppState.ts +++ b/apps/fabric-website/src/components/App/AppState.ts @@ -245,6 +245,11 @@ export const AppState: IAppState = { url: '#/components/spinner', getComponent: cb => require.ensure([], (require) => cb(require('../../pages/Components/SpinnerComponentPage').SpinnerComponentPage)) }, + { + title: 'Stepper', + url: '#/components/stepper', + getComponent: cb => require.ensure([], (require) => cb(require('../../pages/Components/StepperComponentPage').StepperComponentPage)) + }, { title: 'TextField', url: '#/components/textfield', diff --git a/packages/office-ui-fabric-react/src/components/Slider/Slider.tsx b/packages/office-ui-fabric-react/src/components/Slider/Slider.tsx index 03ee52c1b2fe9..7090f629fc100 100644 --- a/packages/office-ui-fabric-react/src/components/Slider/Slider.tsx +++ b/packages/office-ui-fabric-react/src/components/Slider/Slider.tsx @@ -116,11 +116,11 @@ export class Slider extends BaseComponent implements disabled={ disabled } type='button' role='slider' - > + >
+ > implements style={ getRTL() ? { 'right': thumbOffsetPercent + '%' } : { 'left': thumbOffsetPercent + '%' } } - /> + />
From d54e9f4cfe74fa867249ac6a8dddd52c38668383 Mon Sep 17 00:00:00 2001 From: Boris Emorine Date: Wed, 29 Mar 2017 17:12:45 -0700 Subject: [PATCH 02/64] Add first implementation of stepper. --- .../src/pages/StepperPage/StepperPage.tsx | 63 ++++++++++++++ .../examples/Stepper.Basic.Example.scss | 4 + .../examples/Stepper.Basic.Example.tsx | 17 ++++ .../office-ui-fabric-react/src/Stepper.ts | 1 + .../src/components/Stepper/Stepper.Props.ts | 58 +++++++++++++ .../src/components/Stepper/Stepper.scss | 30 +++++++ .../src/components/Stepper/Stepper.tsx | 82 +++++++++++++++++++ .../src/components/Stepper/index.ts | 2 + .../src/components/Stepper/interfaces.ts | 7 ++ 9 files changed, 264 insertions(+) create mode 100644 apps/fabric-examples/src/pages/StepperPage/StepperPage.tsx create mode 100644 apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.scss create mode 100644 apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx create mode 100644 packages/office-ui-fabric-react/src/Stepper.ts create mode 100644 packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts create mode 100644 packages/office-ui-fabric-react/src/components/Stepper/Stepper.scss create mode 100644 packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx create mode 100644 packages/office-ui-fabric-react/src/components/Stepper/index.ts create mode 100644 packages/office-ui-fabric-react/src/components/Stepper/interfaces.ts diff --git a/apps/fabric-examples/src/pages/StepperPage/StepperPage.tsx b/apps/fabric-examples/src/pages/StepperPage/StepperPage.tsx new file mode 100644 index 0000000000000..36000ba8148ec --- /dev/null +++ b/apps/fabric-examples/src/pages/StepperPage/StepperPage.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; +import { + ExampleCard, + IComponentDemoPageProps, + ComponentPage, + PropertiesTableSet +} from '@uifabric/example-app-base'; +import { StepperBasicExample } from './examples/Stepper.Basic.Example'; + +const StepperBasicExampleCode = require('./examples/Stepper.Basic.Example.tsx') as string; + +export class StepperPage extends React.Component { + public render() { + return ( + + + + } + propertiesTables={ + ('office-ui-fabric-react/lib/components/Stepper/Stepper.Props.ts') + ] } + /> + } + overview={ +
+

+ A Stepper... +

+
+ } + bestPractices={ +
+ } + dos={ +
+
    +
  • Use a Stepper when...
  • +
+
+ } + donts={ +
+
    +
  • Don’t use a Stepper when...
  • +
+
+ } + related={ + Fabric JS + } + isHeaderVisible={ this.props.isHeaderVisible }> +
+ ); + } +} diff --git a/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.scss b/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.scss new file mode 100644 index 0000000000000..f49d13b8ae238 --- /dev/null +++ b/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.scss @@ -0,0 +1,4 @@ +.ms-BasicSteppersExample .ms-Stepper { + display: inline-block; + margin: 10px 0; +} \ No newline at end of file diff --git a/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx b/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx new file mode 100644 index 0000000000000..61ff0d92ee280 --- /dev/null +++ b/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { + Stepper +} from 'office-ui-fabric-react/lib/Stepper'; +import { Label } from 'office-ui-fabric-react/lib/Label'; +import './Stepper.Basic.Example.scss'; + +export class StepperBasicExample extends React.Component { + public render() { + return ( +
+ + +
+ ); + } +} diff --git a/packages/office-ui-fabric-react/src/Stepper.ts b/packages/office-ui-fabric-react/src/Stepper.ts new file mode 100644 index 0000000000000..bd5b02d4f0135 --- /dev/null +++ b/packages/office-ui-fabric-react/src/Stepper.ts @@ -0,0 +1 @@ +export * from './components/Stepper/index'; diff --git a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts new file mode 100644 index 0000000000000..442c65db67293 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts @@ -0,0 +1,58 @@ +import * as React from 'react'; + +export interface IStepperProps { + + /** + * The initial value of the Stepper. Use this if you intend for the Stepper to be an uncontrolled component. + * This value is mutually exclusive to value. Use one or the other. + */ + defaultValue?: number; + + /** + * The initial value of the Stepper. Use this if you intend to pass in a new value as a result of onChange events. + * This value is mutually exclusive to defaultValue. Use one or the other. + */ + value?: number; + + /** + * The min value of the Stepper. + * @default 0 + */ + min?: number; + + /** + * The max value of the Stepper. + * @default 10 + */ + max?: number; + + /** + * The diffrrence between the two adjacent values of the Stepper. + * @default 1 + */ + step?: number; + + /** + * Callback when the value has been changed. + */ + onChange?: (value: number) => void; + + /** + * A description of the Stepper for the benefit of screen readers. + */ + ariaLabel?: string; + + /** + * Whether or not the Stepper is disabled. + */ + disabled?: boolean; + + /** + * Optional className for Stepper. + */ + className?: string; +} + +export interface IStepper { + value?: number +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.scss b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.scss new file mode 100644 index 0000000000000..cad7398252f35 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.scss @@ -0,0 +1,30 @@ +:global { + + .arrowBox { + display: block; + float: left; + height: 20px; + border:1px solid #e1e1e1; + } + + .textField { + display: block; + float: left; + height: 16px; + width: 48px; + border:1px solid #e1e1e1; + border-right-width: 0px; + } + + .upButton, .downButton { + display: block; + height: 10px; + width: 20px; + border: 1px solid transparent; + padding-top: 2px; + background-color: transparent; + text-align: center; + cursor: default; + } + +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx new file mode 100644 index 0000000000000..eb62743eb67ea --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; +import { + TextField, + IconButton +} from '../../../lib/'; +import { + BaseComponent, + css, + getId +} from '../../Utilities'; +import { + IStepper, + IStepperProps +} from './Stepper.Props'; +import './Stepper.scss'; + +export interface IStepperState { + value?: number; +} + +export class Stepper extends React.Component implements IStepper { + public static defaultProps: IStepperProps = { + step: 1, + min: 0, + max: 10, + disabled: false, + }; + + constructor(props?: IStepperProps) { + super(props); + + let value = props.value || props.defaultValue || props.min + this.state = { + value: value, + }; + } + + /** + * Invoked when a component is receiving new props. This method is not called for the initial render. + */ + public componentWillReceiveProps(newProps: IStepperProps): void { + + if (newProps.value !== undefined) { + let value = Math.max(newProps.min, Math.min(newProps.max, newProps.value)); + + this.setState({ + value: value, + }); + } + } + + public render() { + const { + className, + disabled, + min, + max + } = this.props; + + const { + value, + } = this.state; + + return ( +
+ < TextField value={ String(value) } resizable={ false } validateOnFocusOut={ true } style={ { width: '48px' } } className="textField" /> + < span className='arrowBox' > + + + + + ) as React.ReactElement<{}>; + } +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/Stepper/index.ts b/packages/office-ui-fabric-react/src/components/Stepper/index.ts new file mode 100644 index 0000000000000..0786fff919084 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Stepper/index.ts @@ -0,0 +1,2 @@ +export * from './Stepper'; +export * from './Stepper.Props'; diff --git a/packages/office-ui-fabric-react/src/components/Stepper/interfaces.ts b/packages/office-ui-fabric-react/src/components/Stepper/interfaces.ts new file mode 100644 index 0000000000000..861c78cd93a7b --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Stepper/interfaces.ts @@ -0,0 +1,7 @@ + +export enum StepperSize { + xSmall = 0, + small = 1, + medium = 2, + large = 3 +} \ No newline at end of file From 4369f6701ee411bde5064996bb9bd0eaf613c262 Mon Sep 17 00:00:00 2001 From: "REDMOND\\jspurlin" Date: Mon, 3 Apr 2017 11:23:48 -0700 Subject: [PATCH 03/64] Add functionality to stepper --- .../examples/Stepper.Basic.Example.tsx | 4 +- .../src/components/Stepper/Stepper.Props.ts | 12 + .../src/components/Stepper/Stepper.scss | 41 +- .../src/components/Stepper/Stepper.tsx | 415 +++++++++++++++++- 4 files changed, 450 insertions(+), 22 deletions(-) diff --git a/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx b/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx index 61ff0d92ee280..e2a00ed58d8fb 100644 --- a/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx +++ b/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx @@ -9,8 +9,8 @@ export class StepperBasicExample extends React.Component { public render() { return (
- - + +
); } diff --git a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts index 442c65db67293..72b153d2ae2ff 100644 --- a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts +++ b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts @@ -5,6 +5,7 @@ export interface IStepperProps { /** * The initial value of the Stepper. Use this if you intend for the Stepper to be an uncontrolled component. * This value is mutually exclusive to value. Use one or the other. + * @default 0 */ defaultValue?: number; @@ -42,6 +43,11 @@ export interface IStepperProps { */ ariaLabel?: string; + /** + * A list of ids to be used as the label for the Stepper for the benefit of screen readers. + */ + ariaLabelledby?: string; + /** * Whether or not the Stepper is disabled. */ @@ -51,6 +57,12 @@ export interface IStepperProps { * Optional className for Stepper. */ className?: string; + + /** + * Acceptable units for spinner (e.g. suffix for the textFeild value), defaults to '' if nothing given here, + * otherwise [0] is used by default + */ + validUnitOptions?: string[]; } export interface IStepper { diff --git a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.scss b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.scss index cad7398252f35..b1cb216ad9827 100644 --- a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.scss +++ b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.scss @@ -1,30 +1,53 @@ :global { +.arrowBox, .textField, +.upButton, .downButton, .stepperContainer { + outline: none; + font-size: 12px; +} + +.stepperContainer { + width: 100px; + height: 32px; +} + .arrowBox { display: block; float: left; - height: 20px; - border:1px solid #e1e1e1; + height: 30px; + border: 1px solid #e1e1e1; + border-left-width: 0px; + cursor: default; + padding: 0px; } .textField { display: block; float: left; - height: 16px; - width: 48px; - border:1px solid #e1e1e1; + height: 250px; + width: 70px; border-right-width: 0px; + overflow: hidden; + cursor: text; + user-select: text; } .upButton, .downButton { display: block; - height: 10px; - width: 20px; - border: 1px solid transparent; - padding-top: 2px; + height: 16px; + width: 26px; + //border: 1px solid transparent; + padding-top: 0px; background-color: transparent; text-align: center; cursor: default; } + .upButton span, .downButton span { + line-height: 6px !important; + font-size: 14px; + padding-top: 4px; + margin: 0px; + } + } \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx index eb62743eb67ea..060bb6cc024c0 100644 --- a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx +++ b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx @@ -6,7 +6,9 @@ import { import { BaseComponent, css, - getId + getId, + KeyCodes, + assign } from '../../Utilities'; import { IStepper, @@ -15,36 +17,78 @@ import { import './Stepper.scss'; export interface IStepperState { + /** + * the value of the stepper + */ value?: number; + + /** + * Are we spinning? Used in case we are spinning + * and the text field gets focus (we should stop spinning) + */ + spinning?: boolean; + + /** + * the index into the unit options array that corresponds + * to the current unit that is being used in the textField + */ + currentUnitIndex?: number; } -export class Stepper extends React.Component implements IStepper { +export class Stepper extends BaseComponent implements IStepper { + private _textField: TextField; public static defaultProps: IStepperProps = { step: 1, min: 0, - max: 10, + max: 100, disabled: false, + defaultValue: 0, + validUnitOptions: ['"', 'in', 'cm', 'pt', 'px'] }; + private _currentStepFunctionHandle: number; + private _stepDelay = 50; + + private _formattedValidUnitOptions: string[] = []; + constructor(props?: IStepperProps) { super(props); - let value = props.value || props.defaultValue || props.min + let value = props.value || props.defaultValue || props.min; this.state = { value: value, + spinning: false, + currentUnitIndex: -1 }; + + // bind this (for this class) to all the methods + this._getErrorMessage = this._getErrorMessage.bind(this); + this._parseNumIfValidSuffix = this._parseNumIfValidSuffix.bind(this); + this._increment = this._increment.bind(this); + this._decrement = this._decrement.bind(this); + this._stop = this._stop.bind(this); + this.focus = this.focus.bind(this); + this._handleKeyDown = this._handleKeyDown.bind(this); + this._handleKeyUp = this._handleKeyUp.bind(this); + this._getCurrentUnit = this._getCurrentUnit.bind(this); + this._normalizeUnit = this._normalizeUnit.bind(this); + this._formatUnitOptions = this._formatUnitOptions.bind(this); + this._updateValueByStep = this._updateValueByStep.bind(this); + + // Format all the unit options passed in to + // simplify the work we have to do when rendering the full value + this._formatUnitOptions(); } /** * Invoked when a component is receiving new props. This method is not called for the initial render. */ public componentWillReceiveProps(newProps: IStepperProps): void { - if (newProps.value !== undefined) { let value = Math.max(newProps.min, Math.min(newProps.max, newProps.value)); this.setState({ - value: value, + value: value }); } } @@ -59,24 +103,373 @@ export class Stepper extends React.Component imple const { value, + currentUnitIndex, } = this.state; return ( -
- < TextField value={ String(value) } resizable={ false } validateOnFocusOut={ true } style={ { width: '48px' } } className="textField" /> - < span className='arrowBox' > +
+ + + title='Increase' + aria-hidden='true' + onMouseDown={ () => { this._increment() } } + onMouseLeave={ this._stop } + onMouseUp={ this._stop } + onBlur={ this._stop } + tabIndex={ -1 } + /> + title='Decrease' + aria-hidden='true' + onMouseDown={ () => { this._decrement() } } + onMouseLeave={ this._stop } + onMouseUp={ this._stop } + onBlur={ this._stop } + tabIndex={ -1 } + /> ) as React.ReactElement<{}>; } + + /** + * OnFocus select the contents of the textField + */ + public focus() { + if (this.state.spinning) { + this._stop(); + } + + this._textField.focus(); + this._textField.select(); + } + + /** + * Format the unit options passed in so we do not have + * to think about it when rendering the unit with the value + */ + private _formatUnitOptions() { + if (this.props.validUnitOptions == null || this.props.validUnitOptions.length == 0) { + return; + } + + this.props.validUnitOptions.forEach(element => { + this._formattedValidUnitOptions = this._formattedValidUnitOptions.concat(this._normalizeUnit(element)); + }); + } + + /** + * Normalize the given unit (e.g. if it is only alpha characters + * and does not start with a space, add a space otherwise + * do not alter the value) + * @param unitValue - the unit to normalize + * @returns the normalized unit + */ + private _normalizeUnit(unitValue: string): string { + let unitIndex: number = this.state.currentUnitIndex; + + if (unitValue == null || unitValue.length == 0) { + return ''; + } + + let formattedUnit: string = unitValue; + + // if our unitValue only contains alpha chars and does not + // already start with a space, add one + if (formattedUnit.match('^([a-zA-Z]+)$') && formattedUnit.indexOf(' ') != 0) { + formattedUnit = String(' ').concat(formattedUnit); + } + + return formattedUnit; + } + + /** + * Returns the currently formatted unit (based off of state) + */ + private _getCurrentUnit(): string { + const currentUnitIndex = this.state.currentUnitIndex; + + if (this._formattedValidUnitOptions == null || + this._formattedValidUnitOptions.length == 0 || + currentUnitIndex >= this._formattedValidUnitOptions.length) { + return ''; + } + else if (currentUnitIndex <= -1) { + return this._formattedValidUnitOptions[0]; + } + + return this._formattedValidUnitOptions[currentUnitIndex]; + } + + /** + * This is used when validating text entry + * in the text field (not when changed via the buttons) + * @param newValue - the pending value to check if it is valid + * @returns an error message to display to the user, empty string if no error + */ + private _getErrorMessage(newValue: string): string { + const { + value, + currentUnitIndex, + } = this.state; + + let errorMessage: string = ''; + let valueToSet: number = null; + let unitIndexToSet: number = null; + + // Attempt to parse the number from the new value, + // checking against the valid unit options. It returns + // a tuple of [ , ] + let parsedNumberInfo: number[] = this._parseNumIfValidSuffix(newValue); + + // Did we get back a well formatted response from the parse? + if (parsedNumberInfo != null && parsedNumberInfo.length == 2) { + let parsedNumberValue: number = parsedNumberInfo[0]; + let parsedNumberUnitIndex: number = parsedNumberInfo[1]; + + // We have a perspective number value, make sure it is valid + // before going any further + if (parsedNumberValue != null && + (parsedNumberValue >= this.props.min && parsedNumberValue <= this.props.max)) { + valueToSet = parsedNumberValue; + } + + // We have a perspective index into the unit options array, make sure + // it is valid before going any further + if (parsedNumberUnitIndex != null && parsedNumberUnitIndex < this._formattedValidUnitOptions.length) { + unitIndexToSet = parsedNumberUnitIndex; + } + } + + // At this point, if parsing the new value was successful, + // the value and unit index to set should not be null + if (valueToSet == null || unitIndexToSet == null) { + errorMessage = `${newValue} is not a valid value`; + } + + // If the value to set is null, fall back + // to a known valid value + if (valueToSet == null) { + if (value == null) { + valueToSet = this.props.min; + } + else { + valueToSet = value; + } + } + + // If the unit index is null, fall back + // to a known valid unit index + if (unitIndexToSet == null) { + if (currentUnitIndex == null) { + unitIndexToSet = -1; + } + else { + unitIndexToSet = currentUnitIndex; + } + } + + let stateToSet: any = {}; + + if (value != valueToSet) { + assign(stateToSet, { value: valueToSet }); + } + + if (currentUnitIndex != unitIndexToSet) { + assign(stateToSet, { currentUnitIndex: unitIndexToSet }); + } + + if (stateToSet != {}) { + this.setState({ ...stateToSet }); + } + + return errorMessage; + } + + /** + * Attempt to parse the number and units that + * the user entered + * @param value - the value to process + * @returns a number array of the format: + * null if we fail to parse the value, or [, ] + */ + private _parseNumIfValidSuffix(value: string): number[] { + if (value == null) { + return null; + } + + let valToConvert = value.trim(); + if (valToConvert.length == 0) { + return null; + } + + // Check to see if the value is alreay just a number, + // if so, convert it and return (keeping the current unit) + if (!isNaN(Number(valToConvert))) { + return [Number(valToConvert), this.state.currentUnitIndex]; + } + + let index = -1; + let optionIndex = 0; + + // loop over the valid unit options to see if we've got a match + // at the end of the string (after the being trimmed) + do { + index = valToConvert.indexOf(this.props.validUnitOptions[optionIndex]); + } while (index === -1 && ++optionIndex < this.props.validUnitOptions.length); + + // Did we find a valid unit match? If not, return null + if (index === -1 || index + this.props.validUnitOptions[optionIndex].length != valToConvert.length) { + return null; + } + + // If we mad it here we found a matching unit, now + // parse the number from the value + let parsedNumbers: string = valToConvert.substring(0, index); + let numberValue: number = Number(parsedNumbers); + + // Is what we parsed a valid number? + numberValue = isNaN(numberValue) ? null : numberValue; + + return [numberValue, optionIndex]; + } + + /** + * Used to increment the current value by the provided step count + * @param shouldSpin - Should we continue to increment? + * True by default and useful if we get here from a mousedown event + * on one of the buttons. False if this fired from a keydown event + */ + private _increment(shouldSpin: boolean = true) { + this._updateValueByStep(shouldSpin, true /* increase */); + } + + /** + * Used to decrement the current value by the provided step count + * @param shouldSpin - Should we continue to decrement? + * True by default and useful if we get here from a mousedown event + * on one of the buttons. False if this fired from a keydown event + */ + private _decrement(shouldSpin: boolean = true) { + this._updateValueByStep(shouldSpin, false /* increase */); + } + + /** + * Update (increment or decrement) the current value by the step value (from props) + * @param shouldSpin - When finished updating, should we spin up another update? + * (for example, if responding to mousedown should be true so that the value will spin + * (since only one mousedown fires); but if responding to a keydown this should be false + * (since keydowns continue to fire as long as the key is depressed)) + * @param increase - Are we increasing the value? + */ + private _updateValueByStep(shouldSpin: boolean, increase: boolean) { + if (this.props.disabled) { + this._stop(); + return; + } + + let direction: number = increase ? 1 : -1; + let updatedValue: number = this.state.value + (direction * this.props.step); + let isnewValueWithinRange: boolean = updatedValue <= this.props.max && updatedValue >= this.props.min; + + // Only update the value if it is valid + if (isnewValueWithinRange) { + let stateToSet: any = {}; + + assign(stateToSet, { value: updatedValue }); + + if (this.state.spinning != shouldSpin) { + assign(stateToSet, { spinning: shouldSpin }) + } + + if (stateToSet != {}) { + this.setState({ ...stateToSet }); + } + + // spin up the next update if should spin is true + if (shouldSpin) { + this._currentStepFunctionHandle = window.setTimeout(() => { this._updateValueByStep(shouldSpin, increase); }, this._stepDelay); + return; + } + } + + this._stop(); + } + + /** + * Stop spinning (clear any currently pending update and set spinning to false) + */ + private _stop() { + if (this._currentStepFunctionHandle != null) { + window.clearTimeout(this._currentStepFunctionHandle); + this._currentStepFunctionHandle == 0; + } + + if (this.state.spinning) { + this.setState({ spinning: false }); + } + } + + /** + * Handle keydown on the text field. We need to update + * the value when up or down arrow are depressed + * @param event - the keyboardEvent that was fired + */ + private _handleKeyDown(event: React.KeyboardEvent) { + if (this.props.disabled) { + this._stop(); + return; + } + + if (event.which === KeyCodes.up) { + this._increment(false /* shouldSpin */); + } + else if (event.which === KeyCodes.down) { + this._decrement(false /* shouldSpin */); + } + + else if (event.which === KeyCodes.enter) { + event.currentTarget.blur(); + } + + } + + /** + * Make sure that we have stopped spinning on keyUp + * if the up or down arrow fired this event + * @param event stop spinning if we + */ + private _handleKeyUp(event: React.KeyboardEvent) { + if (this.props.disabled) { + this._stop(); + return; + } + + if (event.which === KeyCodes.up || event.which === KeyCodes.down) { + this._stop(); + } + } } \ No newline at end of file From 41ac16aa4641534a0d4e482f77eebc2461d41782 Mon Sep 17 00:00:00 2001 From: "REDMOND\\jspurlin" Date: Tue, 4 Apr 2017 17:45:37 -0700 Subject: [PATCH 04/64] Refine the Stepper class and add tests --- .../examples/Stepper.Basic.Example.tsx | 11 +- .../src/components/Stepper/Stepper.Props.ts | 22 +- .../src/components/Stepper/Stepper.test.tsx | 606 ++++++++++++++++++ .../src/components/Stepper/Stepper.tsx | 49 +- 4 files changed, 667 insertions(+), 21 deletions(-) create mode 100644 packages/office-ui-fabric-react/src/components/Stepper/Stepper.test.tsx diff --git a/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx b/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx index e2a00ed58d8fb..f763732e1cef2 100644 --- a/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx +++ b/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx @@ -1,16 +1,15 @@ import * as React from 'react'; -import { - Stepper -} from 'office-ui-fabric-react/lib/Stepper'; -import { Label } from 'office-ui-fabric-react/lib/Label'; +import { Stepper, IStepperState, IStepperProps } from 'office-ui-fabric-react/lib/Stepper'; +import { Label, assign } from 'office-ui-fabric-react/lib'; +//import { assign } from 'office-ui-fabric-react/lib/Utilities'; import './Stepper.Basic.Example.scss'; export class StepperBasicExample extends React.Component { + public render() { return (
- - +
); } diff --git a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts index 72b153d2ae2ff..95bb12b2fdbd7 100644 --- a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts +++ b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import { IStepperState } from './Stepper'; export interface IStepperProps { @@ -43,11 +44,6 @@ export interface IStepperProps { */ ariaLabel?: string; - /** - * A list of ids to be used as the label for the Stepper for the benefit of screen readers. - */ - ariaLabelledby?: string; - /** * Whether or not the Stepper is disabled. */ @@ -63,6 +59,22 @@ export interface IStepperProps { * otherwise [0] is used by default */ validUnitOptions?: string[]; + + /** + * Label for the spinner + */ + label?: string; + + /** + * The method is used to get the validation error message and determine whether the input value is valid or not. + * + * When it returns string: + * - If valid, it returns empty string. + * - If invalid, it returns the error message string and the text field will + * show a red border and show an error message below the text field. + * + */ + onGetErrorMessage?: (value: string, state: IStepperState, props: IStepperProps) => string; } export interface IStepper { diff --git a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.test.tsx b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.test.tsx new file mode 100644 index 0000000000000..6070670f13db7 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.test.tsx @@ -0,0 +1,606 @@ +import 'es6-promise'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import * as ReactTestUtils from 'react-addons-test-utils'; +import { + Stepper, + IStepperState +} from './Stepper'; +import { IStepperProps } from './Stepper.Props'; +import { KeyCodes } from '../../Utilities'; + +const expect: Chai.ExpectStatic = chai.expect; + +describe('Stepper', () => { + function renderIntoDocument(element: React.ReactElement): HTMLElement { + const component = ReactTestUtils.renderIntoDocument(element); + const renderedDOM: Element = ReactDOM.findDOMNode(component as React.ReactInstance); + return renderedDOM as HTMLElement; + } + + function mockEvent(targetValue: string = ''): React.SyntheticEvent { + const target: EventTarget = { value: targetValue } as HTMLInputElement; + const event: React.SyntheticEvent = { target } as React.SyntheticEvent; + return event; + } + + it('should render a spinner with the default value on the input element', () => { + const exampleLabelValue: string = 'Stepper'; + const exampleMinValue: number = 2; + const exampleMaxValue: number = 22; + const exampleDefaultValue: number = 12; + + const renderedDOM: HTMLElement = renderIntoDocument( + + ); + + // Assert on the input element. + const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + const labelDOM: HTMLLabelElement = renderedDOM.getElementsByTagName('label')[0]; + expect(inputDOM.value).to.equal(String(exampleDefaultValue)); + expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); + expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); + expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleDefaultValue)); + expect(inputDOM.getAttribute('aria-labelledby')).to.equals(labelDOM.id); + + // Assert on the label element. + expect(labelDOM.textContent).to.equal(exampleLabelValue); + expect(labelDOM.htmlFor).to.equal(inputDOM.id); + }); + + it('should increment the value in the stepper via the up button', () => { + const exampleLabelValue: string = 'Stepper'; + const exampleMinValue: number = 2; + const exampleMaxValue: number = 22; + const exampleDefaultValue: number = 12; + + const renderedDOM: HTMLElement = renderIntoDocument( + + ); + + // Assert on the input element. + const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + const buttonDOM: Element = renderedDOM.getElementsByClassName('upButton')[0]; + + expect(buttonDOM.tagName).to.equal('BUTTON'); + + ReactTestUtils.Simulate.mouseDown(buttonDOM, + { + type: 'mousedown', + clientX: 0, + clientY: 0 + }); + + ReactTestUtils.Simulate.mouseUp(buttonDOM, + { + type: 'mouseup', + clientX: 0, + clientY: 0 + }); + + expect(inputDOM.value).to.equal('13'); + expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); + expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); + expect(inputDOM.getAttribute('aria-valuenow')).to.equal('13'); + + }); + + it('should decrement the value in the stepper by the down button', () => { + const exampleLabelValue: string = 'Stepper'; + const exampleMinValue: number = 2; + const exampleMaxValue: number = 22; + const exampleDefaultValue: number = 12; + + const renderedDOM: HTMLElement = renderIntoDocument( + + ); + + // Assert on the input element. + const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + const buttonDOM: Element = renderedDOM.getElementsByClassName('downButton')[0]; + + expect(buttonDOM.tagName).to.equal('BUTTON'); + + ReactTestUtils.Simulate.mouseDown(buttonDOM, + { + type: 'mousedown', + clientX: 0, + clientY: 0 + }); + + ReactTestUtils.Simulate.mouseUp(buttonDOM, + { + type: 'mouseup', + clientX: 0, + clientY: 0 + }); + + expect(inputDOM.value).to.equal('11'); + expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); + expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); + expect(inputDOM.getAttribute('aria-valuenow')).to.equal('11'); + + }); + + it('should increment the value in the stepper by the up arrow', () => { + const exampleLabelValue: string = 'Stepper'; + const exampleMinValue: number = 2; + const exampleMaxValue: number = 22; + const exampleDefaultValue: number = 12; + + const renderedDOM: HTMLElement = renderIntoDocument( + + ); + + // Assert on the input element. + const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + + ReactTestUtils.Simulate.keyDown(inputDOM, + { + which: KeyCodes.up + }); + + ReactTestUtils.Simulate.keyUp(inputDOM, + { + which: KeyCodes.up + }); + + expect(inputDOM.value).to.equal('13'); + expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); + expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); + expect(inputDOM.getAttribute('aria-valuenow')).to.equal('13'); + + }); + + it('should decrement the value in the stepper by the down arrow', () => { + const exampleLabelValue: string = 'Stepper'; + const exampleMinValue: number = 2; + const exampleMaxValue: number = 22; + const exampleDefaultValue: number = 12; + + const renderedDOM: HTMLElement = renderIntoDocument( + + ); + + // Assert on the input element. + const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + + ReactTestUtils.Simulate.keyDown(inputDOM, + { + which: KeyCodes.down + }); + + ReactTestUtils.Simulate.keyUp(inputDOM, + { + which: KeyCodes.down + }); + + expect(inputDOM.value).to.equal('11'); + expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); + expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); + expect(inputDOM.getAttribute('aria-valuenow')).to.equal('11'); + + }); + + it('should increment the value in the stepper by a step value of 2', () => { + const exampleLabelValue: string = 'Stepper'; + const exampleMinValue: number = 2; + const exampleMaxValue: number = 22; + const exampleDefaultValue: number = 12; + + const renderedDOM: HTMLElement = renderIntoDocument( + + ); + + // Assert on the input element. + const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + + ReactTestUtils.Simulate.keyDown(inputDOM, + { + which: KeyCodes.up + }); + + ReactTestUtils.Simulate.keyUp(inputDOM, + { + which: KeyCodes.up + }); + + expect(inputDOM.value).to.equal('14'); + expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); + expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); + expect(inputDOM.getAttribute('aria-valuenow')).to.equal('14'); + + }); + + it('should decrement the value in the stepper by a step value of 2', () => { + const exampleLabelValue: string = 'Stepper'; + const exampleMinValue: number = 2; + const exampleMaxValue: number = 22; + const exampleDefaultValue: number = 12; + + const renderedDOM: HTMLElement = renderIntoDocument( + + ); + + // Assert on the input element. + const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + + ReactTestUtils.Simulate.keyDown(inputDOM, + { + which: KeyCodes.down + }); + + ReactTestUtils.Simulate.keyUp(inputDOM, + { + which: KeyCodes.down + }); + + expect(inputDOM.value).to.equal('10'); + expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); + expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); + expect(inputDOM.getAttribute('aria-valuenow')).to.equal('10'); + + }); + + it('should set the value of the stepper by manual entry', () => { + const exampleLabelValue: string = 'Stepper'; + const exampleMinValue: number = 2; + const exampleMaxValue: number = 22; + const exampleDefaultValue: number = 12; + const exampleNewValue: number = 21; + + const renderedDOM: HTMLElement = renderIntoDocument( + + ); + + // Assert on the input element. + const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + ReactTestUtils.Simulate.input(inputDOM, mockEvent(String(exampleNewValue))); + ReactTestUtils.Simulate.blur(inputDOM); + + expect(inputDOM.value).to.equal(String(exampleNewValue)); + expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); + expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); + expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleNewValue)); + + }); + + it('should reset the value of the stepper with invalid manual entry', () => { + const exampleLabelValue: string = 'Stepper'; + const exampleMinValue: number = 2; + const exampleMaxValue: number = 22; + const exampleDefaultValue: number = 12; + const exampleNewValue: string = 'garbage'; + + const renderedDOM: HTMLElement = renderIntoDocument( + + ); + + // Assert on the input element. + const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + ReactTestUtils.Simulate.input(inputDOM, mockEvent(exampleNewValue)); + ReactTestUtils.Simulate.blur(inputDOM); + + expect(inputDOM.value).to.equal(String(exampleDefaultValue)); + expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); + expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); + expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleDefaultValue)); + + }); + + it('should revert existing value when input value is higher than the max of the stepper', () => { + const exampleLabelValue: string = 'Stepper'; + const exampleMinValue: number = 2; + const exampleMaxValue: number = 22; + const exampleDefaultValue: number = 12; + const exampleNewValue: number = 23; + + const renderedDOM: HTMLElement = renderIntoDocument( + + ); + + // Assert on the input element. + const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + ReactTestUtils.Simulate.input(inputDOM, mockEvent(String(exampleNewValue))); + ReactTestUtils.Simulate.blur(inputDOM); + + expect(inputDOM.value).to.equal(String(exampleDefaultValue)); + expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); + expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); + expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleDefaultValue)); + }); + + it('should revert existing value when input value is lower than the min of the stepper', () => { + const exampleLabelValue: string = 'Stepper'; + const exampleMinValue: number = 2; + const exampleMaxValue: number = 22; + const exampleDefaultValue: number = 12; + const exampleNewValue: number = 0; + + const renderedDOM: HTMLElement = renderIntoDocument( + + ); + + // Assert on the input element. + const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + ReactTestUtils.Simulate.input(inputDOM, mockEvent(String(exampleNewValue))); + ReactTestUtils.Simulate.blur(inputDOM); + + expect(inputDOM.value).to.equal(String(exampleDefaultValue)); + expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); + expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); + expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleDefaultValue)); + + }); + + it('should show value with unit on the stepper', () => { + const exampleLabelValue: string = 'Stepper'; + const exampleMinValue: number = 2; + const exampleMaxValue: number = 22; + const exampleDefaultValue: number = 12; + const exampleNewValue: number = 13; + const exampleValidUnitOptions: string[] = ['"']; + + const renderedDOM: HTMLElement = renderIntoDocument( + + ); + + // Assert on the input element. + const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + ReactTestUtils.Simulate.input(inputDOM, mockEvent(String(exampleNewValue))); + ReactTestUtils.Simulate.blur(inputDOM); + + expect(inputDOM.value).to.equal(String(exampleNewValue) + exampleValidUnitOptions[0]); + expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); + expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); + expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleNewValue)); + + }); + + it('should show new value with unit update on the stepper', () => { + const exampleLabelValue: string = 'Stepper'; + const exampleMinValue: number = 2; + const exampleMaxValue: number = 22; + const exampleDefaultValue: number = 12; + const exampleNewValue: number = 13; + const exampleValidUnitOptions: string[] = ['"', 'pt']; + + const renderedDOM: HTMLElement = renderIntoDocument( + + ); + + // Assert on the input element. + const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + ReactTestUtils.Simulate.input(inputDOM, mockEvent(String(exampleNewValue) + exampleValidUnitOptions[1])); + ReactTestUtils.Simulate.blur(inputDOM); + + expect(inputDOM.value).to.equal(String(exampleNewValue) + ' ' + exampleValidUnitOptions[1]); + expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); + expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); + expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleNewValue)); + + }); + + it('should revert existing value and unit when input unit is invalid the stepper', () => { + const exampleLabelValue: string = 'Stepper'; + const exampleMinValue: number = 2; + const exampleMaxValue: number = 22; + const exampleDefaultValue: number = 12; + const exampleNewValue: number = 13; + const exampleValidUnitOptions: string[] = ['"', 'pt']; + const exampleNewInvalidUnit: string = 'invalidValue'; + + const renderedDOM: HTMLElement = renderIntoDocument( + + ); + + // Assert on the input element. + const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + ReactTestUtils.Simulate.input(inputDOM, mockEvent(String(exampleNewValue) + ' ' + exampleNewInvalidUnit)); + ReactTestUtils.Simulate.blur(inputDOM); + + expect(inputDOM.value).to.equal(String(exampleDefaultValue) + exampleValidUnitOptions[0]); + expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); + expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); + expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleDefaultValue)); + + }); + + it('should use onErrorMessage passed to the stepper (with valid input)', () => { + const errorMessage: string = 'The value is invalid'; + const exampleLabelValue: string = 'Stepper'; + const exampleMinValue: number = 2; + const exampleMaxValue: number = 22; + const exampleDefaultValue: number = 12; + const exampleNewValue: number = 21; + + function validator(newValue: string, state: IStepperState, props: IStepperProps): string { + let numberValue: number = Number(newValue); + + return (!isNaN(numberValue) && numberValue >= props.min && numberValue <= props.max) ? '' : errorMessage; + } + + const renderedDOM: HTMLElement = renderIntoDocument( + + ); + + // Assert on the input element. + const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + ReactTestUtils.Simulate.input(inputDOM, mockEvent(String(exampleNewValue))); + ReactTestUtils.Simulate.blur(inputDOM); + + expect(inputDOM.value).to.equal(String(exampleNewValue)); + expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); + expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); + expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleNewValue)); + + }); + + it('should use onErrorMessage passed to the stepper (with invalid input)', () => { + const errorMessage: string = 'The value is invalid'; + const exampleLabelValue: string = 'Stepper'; + const exampleMinValue: number = 2; + const exampleMaxValue: number = 22; + const exampleDefaultValue: number = 12; + const exampleNewValue: number = 100; + + function validator(newValue: string, state: IStepperState, props: IStepperProps): string { + let numberValue: number = Number(newValue); + + return (!isNaN(numberValue) && numberValue >= props.min && numberValue <= props.max) ? '' : errorMessage; + } + + function assertErrorMessage(renderedDOM: HTMLElement, expectedErrorMessage: string): void { + const errorMessageDOM: HTMLElement = + renderedDOM.querySelector('[data-automation-id=error-message]') as HTMLElement; + expect(errorMessageDOM.textContent).to.equal(expectedErrorMessage); + } + + const renderedDOM: HTMLElement = renderIntoDocument( + + ); + + // Assert on the input element. + const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + ReactTestUtils.Simulate.input(inputDOM, mockEvent(String(exampleNewValue))); + ReactTestUtils.Simulate.blur(inputDOM); + + expect(inputDOM.value).to.equal(String(exampleNewValue)); + expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); + expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); + expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleDefaultValue)); + assertErrorMessage(renderedDOM, errorMessage); + + }); + + it('should stop spinning if text field is focused while actively spinning', () => { + const exampleLabelValue: string = 'Stepper'; + const exampleMinValue: number = 2; + const exampleMaxValue: number = 22; + const exampleDefaultValue: number = 2; + + function delay(millisecond: number): Promise { + return new Promise((resolve) => setTimeout(resolve, millisecond)); + } + + const renderedDOM: HTMLElement = renderIntoDocument( + + ); + + // Assert on the input element. + const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; + const buttonDOM: Element = renderedDOM.getElementsByClassName('upButton')[0]; + + expect(buttonDOM.tagName).to.equal('BUTTON'); + + ReactTestUtils.Simulate.mouseDown(buttonDOM, + { + type: 'mousedown', + clientX: 0, + clientY: 0 + }); + + delay(500).then(() => ReactTestUtils.Simulate.focus(inputDOM)); + + let currentValue = inputDOM.value; + expect(currentValue).to.not.equal('2'); + expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); + expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); + expect(inputDOM.getAttribute('aria-valuenow')).to.equal(currentValue); + + let newCurrentValue = inputDOM.value; + expect(currentValue).to.equal(newCurrentValue); + }); +}); diff --git a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx index 060bb6cc024c0..9a857bb28414b 100644 --- a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx +++ b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; import { TextField, - IconButton + IconButton, + Label } from '../../../lib/'; import { BaseComponent, @@ -37,13 +38,16 @@ export interface IStepperState { export class Stepper extends BaseComponent implements IStepper { private _textField: TextField; + private _stepperId: string; + private _labelId: string; + + private _onGetErrorMessage?: (value: string, state: IStepperState, props: IStepperProps) => string; public static defaultProps: IStepperProps = { step: 1, min: 0, max: 100, disabled: false, - defaultValue: 0, - validUnitOptions: ['"', 'in', 'cm', 'pt', 'px'] + defaultValue: 0 }; private _currentStepFunctionHandle: number; @@ -61,6 +65,14 @@ export class Stepper extends BaseComponent impleme currentUnitIndex: -1 }; + this._labelId = getId('Label'); + this._stepperId = getId('Stepper'); + + if (props.onGetErrorMessage) { + this._onGetErrorMessage = props.onGetErrorMessage; + this._onGetErrorMessage = this._onGetErrorMessage.bind(this); + } + // bind this (for this class) to all the methods this._getErrorMessage = this._getErrorMessage.bind(this); this._parseNumIfValidSuffix = this._parseNumIfValidSuffix.bind(this); @@ -98,24 +110,25 @@ export class Stepper extends BaseComponent impleme className, disabled, min, - max + max, + label } = this.props; const { - value, - currentUnitIndex, + value } = this.state; return (
+ { label && } impleme * @returns the normalized unit */ private _normalizeUnit(unitValue: string): string { - let unitIndex: number = this.state.currentUnitIndex; if (unitValue == null || unitValue.length == 0) { return ''; @@ -231,12 +243,23 @@ export class Stepper extends BaseComponent impleme * @returns an error message to display to the user, empty string if no error */ private _getErrorMessage(newValue: string): string { + let errorMessage: string = ''; + + if (this._onGetErrorMessage) { + errorMessage = this._onGetErrorMessage(newValue, this.state, this.props); + + if (errorMessage == '' && Number(newValue) != this.state.value) { + this.setState({ value: Number(newValue) }); + } + + return errorMessage; + } + const { value, currentUnitIndex, } = this.state; - let errorMessage: string = ''; let valueToSet: number = null; let unitIndexToSet: number = null; @@ -332,6 +355,12 @@ export class Stepper extends BaseComponent impleme return [Number(valToConvert), this.state.currentUnitIndex]; } + // if we got a value that is not a number, we better have + // some valid unit options, otherwise we know this is invalid and we are done + if (this.props.validUnitOptions == null || this.props.validUnitOptions.length == 0) { + return null + } + let index = -1; let optionIndex = 0; From 0d1412f1d7fb30aece67e786d16304462468a5b9 Mon Sep 17 00:00:00 2001 From: "REDMOND\\jspurlin" Date: Wed, 5 Apr 2017 07:54:31 -0700 Subject: [PATCH 05/64] let's make sure to put focus back on the text field when submitting via enter --- .../office-ui-fabric-react/src/components/Stepper/Stepper.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx index 9a857bb28414b..7758f13bf721f 100644 --- a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx +++ b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx @@ -482,6 +482,7 @@ export class Stepper extends BaseComponent impleme else if (event.which === KeyCodes.enter) { event.currentTarget.blur(); + this.focus(); } } From 8570e56d6e5848dd9d42d95a3a95310def2c2848 Mon Sep 17 00:00:00 2001 From: Boris Emorine Date: Wed, 5 Apr 2017 16:11:42 -0700 Subject: [PATCH 06/64] Added documentation to Stepper. --- .../src/pages/StepperPage/StepperPage.tsx | 10 +++++++--- .../examples/Stepper.Basic.Example.scss | 5 ++--- .../examples/Stepper.Basic.Example.tsx | 15 ++++++++++++++- .../src/components/Stepper/Stepper.Props.ts | 4 ++-- .../src/components/Stepper/Stepper.scss | 2 +- .../src/components/Stepper/interfaces.ts | 7 ------- 6 files changed, 26 insertions(+), 17 deletions(-) delete mode 100644 packages/office-ui-fabric-react/src/components/Stepper/interfaces.ts diff --git a/apps/fabric-examples/src/pages/StepperPage/StepperPage.tsx b/apps/fabric-examples/src/pages/StepperPage/StepperPage.tsx index 36000ba8148ec..32d11adf508f5 100644 --- a/apps/fabric-examples/src/pages/StepperPage/StepperPage.tsx +++ b/apps/fabric-examples/src/pages/StepperPage/StepperPage.tsx @@ -32,7 +32,7 @@ export class StepperPage extends React.Component { overview={

- A Stepper... + A Stepper allows the user to incrementaly adjust a value in small steps. It is mainly used for numeric values, but other values are supported too.

} @@ -42,14 +42,18 @@ export class StepperPage extends React.Component { dos={
    -
  • Use a Stepper when...
  • +
  • Use a Stepper when changing a value with precise control.
  • +
  • Use a Stepper when values are tied to a unit.
  • +
  • Include a label indicating what value the Stepper changes.
} donts={
    -
  • Don’t use a Stepper when...
  • +
  • Don’t use a Stepper if the range of values is large.
  • +
  • Don’t use a Slider for binary settings.
  • +
  • Don't use a Stepper for a range of three values or less.
} diff --git a/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.scss b/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.scss index f49d13b8ae238..c38144d4a29a3 100644 --- a/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.scss +++ b/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.scss @@ -1,4 +1,3 @@ -.ms-BasicSteppersExample .ms-Stepper { - display: inline-block; - margin: 10px 0; +.ms-BasicSteppersExample { + max-width: 300px; } \ No newline at end of file diff --git a/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx b/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx index f763732e1cef2..41b44e4bdecef 100644 --- a/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx +++ b/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx @@ -9,7 +9,20 @@ export class StepperBasicExample extends React.Component { public render() { return (
- + + + + +
); } diff --git a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts index 95bb12b2fdbd7..8f8b32a66c7e6 100644 --- a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts +++ b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts @@ -56,12 +56,12 @@ export interface IStepperProps { /** * Acceptable units for spinner (e.g. suffix for the textFeild value), defaults to '' if nothing given here, - * otherwise [0] is used by default + * otherwise [0] is used by default. */ validUnitOptions?: string[]; /** - * Label for the spinner + * Label for the spinner. */ label?: string; diff --git a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.scss b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.scss index b1cb216ad9827..7ac75523b21ed 100644 --- a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.scss +++ b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.scss @@ -24,7 +24,7 @@ .textField { display: block; float: left; - height: 250px; + height: 32px; width: 70px; border-right-width: 0px; overflow: hidden; diff --git a/packages/office-ui-fabric-react/src/components/Stepper/interfaces.ts b/packages/office-ui-fabric-react/src/components/Stepper/interfaces.ts deleted file mode 100644 index 861c78cd93a7b..0000000000000 --- a/packages/office-ui-fabric-react/src/components/Stepper/interfaces.ts +++ /dev/null @@ -1,7 +0,0 @@ - -export enum StepperSize { - xSmall = 0, - small = 1, - medium = 2, - large = 3 -} \ No newline at end of file From f78f6862bb9a40cc76aea56f7498e8fd3d1e18d9 Mon Sep 17 00:00:00 2001 From: Boris Emorine Date: Thu, 13 Apr 2017 10:59:56 -0700 Subject: [PATCH 07/64] Add flexibility to current stepper implementation. --- .../examples/Stepper.Basic.Example.tsx | 15 +- .../src/components/Stepper/Stepper.Props.ts | 16 +- .../src/components/Stepper/Stepper.scss | 50 ++- .../src/components/Stepper/Stepper.test.tsx | 183 ++------- .../src/components/Stepper/Stepper.tsx | 384 +++++------------- 5 files changed, 201 insertions(+), 447 deletions(-) diff --git a/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx b/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx index 41b44e4bdecef..4b32fdb7b58df 100644 --- a/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx +++ b/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx @@ -12,15 +12,20 @@ export class StepperBasicExample extends React.Component { { + return '2'; + } } + onIncrement={ (value: string) => { + return String(+value + 1); + } } + onDecrement={ (value: string) => { + return String(+value - 1); + } } />
diff --git a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts index 8f8b32a66c7e6..4e507783fa580 100644 --- a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts +++ b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts @@ -8,13 +8,13 @@ export interface IStepperProps { * This value is mutually exclusive to value. Use one or the other. * @default 0 */ - defaultValue?: number; + defaultValue?: string; /** * The initial value of the Stepper. Use this if you intend to pass in a new value as a result of onChange events. * This value is mutually exclusive to defaultValue. Use one or the other. */ - value?: number; + value?: string; /** * The min value of the Stepper. @@ -54,12 +54,6 @@ export interface IStepperProps { */ className?: string; - /** - * Acceptable units for spinner (e.g. suffix for the textFeild value), defaults to '' if nothing given here, - * otherwise [0] is used by default. - */ - validUnitOptions?: string[]; - /** * Label for the spinner. */ @@ -74,7 +68,11 @@ export interface IStepperProps { * show a red border and show an error message below the text field. * */ - onGetErrorMessage?: (value: string, state: IStepperState, props: IStepperProps) => string; + onBlur?: (value: string, state: IStepperState, props: IStepperProps) => string; + + onIncrement?: (value: string) => string; + + onDecrement?: (value: string) => string; } export interface IStepper { diff --git a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.scss b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.scss index 7ac75523b21ed..fbae71f09b25b 100644 --- a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.scss +++ b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.scss @@ -1,3 +1,6 @@ +@import '../../common/common'; +@import '../Label/LabelMixins.scss'; + :global { .arrowBox, .textField, @@ -9,6 +12,7 @@ .stepperContainer { width: 100px; height: 32px; + min-width: 100px; } .arrowBox { @@ -21,15 +25,55 @@ padding: 0px; } - .textField { + .spinButton-input { + @include ms-u-normalize; + @include ms-baseFont; + border: 1px solid $ms-color-neutralTertiaryAlt; + border-radius: 0; + font-weight: $ms-font-weight-regular; + font-size: $ms-font-size-m; + color: $ms-color-neutralPrimary; + height: 32px; + @include padding(0, 12px, 0, 12px); + outline: 0; + text-overflow: ellipsis; display: block; float: left; - height: 32px; - width: 70px; + width: 70%; border-right-width: 0px; overflow: hidden; cursor: text; user-select: text; + + &:hover { + border-color: $ms-color-neutralSecondaryAlt; + } + + &:focus { + border-color: $ms-color-themePrimary; + } + + &:hover, + &:focus { + @media screen and (-ms-high-contrast: active) { + border-color: $ms-color-contrastBlackSelected; + } + + @media screen and (-ms-high-contrast: black-on-white) { + border-color: $ms-color-contrastWhiteSelected; + } + } + + &[disabled] { + background-color: $ms-color-neutralLighter; + border-color: $ms-color-neutralLighter; + pointer-events: none; + cursor: default; + } + + @include input-placeholder { + color: $ms-color-neutralSecondary; + } } .upButton, .downButton { diff --git a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.test.tsx b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.test.tsx index 6070670f13db7..49c75db28715f 100644 --- a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.test.tsx +++ b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.test.tsx @@ -28,7 +28,7 @@ describe('Stepper', () => { const exampleLabelValue: string = 'Stepper'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; - const exampleDefaultValue: number = 12; + const exampleDefaultValue: string = '12'; const renderedDOM: HTMLElement = renderIntoDocument( { // Assert on the input element. const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; const labelDOM: HTMLLabelElement = renderedDOM.getElementsByTagName('label')[0]; - expect(inputDOM.value).to.equal(String(exampleDefaultValue)); + + expect(inputDOM.value).to.equal(exampleDefaultValue); expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleDefaultValue)); expect(inputDOM.getAttribute('aria-labelledby')).to.equals(labelDOM.id); - // Assert on the label element. + // // Assert on the label element. expect(labelDOM.textContent).to.equal(exampleLabelValue); expect(labelDOM.htmlFor).to.equal(inputDOM.id); }); @@ -57,7 +58,7 @@ describe('Stepper', () => { const exampleLabelValue: string = 'Stepper'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; - const exampleDefaultValue: number = 12; + const exampleDefaultValue: string = '12'; const renderedDOM: HTMLElement = renderIntoDocument( { const exampleLabelValue: string = 'Stepper'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; - const exampleDefaultValue: number = 12; + const exampleDefaultValue: string = '12'; const renderedDOM: HTMLElement = renderIntoDocument( { const exampleLabelValue: string = 'Stepper'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; - const exampleDefaultValue: number = 12; + const exampleDefaultValue: string = '12'; const renderedDOM: HTMLElement = renderIntoDocument( { const exampleLabelValue: string = 'Stepper'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; - const exampleDefaultValue: number = 12; + const exampleDefaultValue: string = '12'; const renderedDOM: HTMLElement = renderIntoDocument( { const exampleLabelValue: string = 'Stepper'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; - const exampleDefaultValue: number = 12; + const exampleDefaultValue: string = '12'; const renderedDOM: HTMLElement = renderIntoDocument( { const exampleLabelValue: string = 'Stepper'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; - const exampleDefaultValue: number = 12; + const exampleDefaultValue: string = '12'; const renderedDOM: HTMLElement = renderIntoDocument( { const exampleLabelValue: string = 'Stepper'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; - const exampleDefaultValue: number = 12; - const exampleNewValue: number = 21; + const exampleDefaultValue: string = '12'; + const exampleNewValue: string = '21'; const renderedDOM: HTMLElement = renderIntoDocument( { // Assert on the input element. const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; - ReactTestUtils.Simulate.input(inputDOM, mockEvent(String(exampleNewValue))); + ReactTestUtils.Simulate.input(inputDOM, mockEvent(exampleNewValue)); ReactTestUtils.Simulate.blur(inputDOM); - expect(inputDOM.value).to.equal(String(exampleNewValue)); + expect(inputDOM.value).to.equal(exampleNewValue); expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleNewValue)); - }); it('should reset the value of the stepper with invalid manual entry', () => { const exampleLabelValue: string = 'Stepper'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; - const exampleDefaultValue: number = 12; + const exampleDefaultValue: string = '12'; const exampleNewValue: string = 'garbage'; const renderedDOM: HTMLElement = renderIntoDocument( @@ -328,19 +328,18 @@ describe('Stepper', () => { ReactTestUtils.Simulate.input(inputDOM, mockEvent(exampleNewValue)); ReactTestUtils.Simulate.blur(inputDOM); - expect(inputDOM.value).to.equal(String(exampleDefaultValue)); + expect(inputDOM.value).to.equal(exampleDefaultValue); expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleDefaultValue)); - }); - it('should revert existing value when input value is higher than the max of the stepper', () => { + it('should revert to max value when input value is higher than the max of the stepper', () => { const exampleLabelValue: string = 'Stepper'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; - const exampleDefaultValue: number = 12; - const exampleNewValue: number = 23; + const exampleDefaultValue: string = '12'; + const exampleNewValue: string = '23'; const renderedDOM: HTMLElement = renderIntoDocument( { // Assert on the input element. const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; - ReactTestUtils.Simulate.input(inputDOM, mockEvent(String(exampleNewValue))); + ReactTestUtils.Simulate.input(inputDOM, mockEvent(exampleNewValue)); ReactTestUtils.Simulate.blur(inputDOM); - expect(inputDOM.value).to.equal(String(exampleDefaultValue)); + expect(inputDOM.value).to.equal(String(exampleMaxValue)); expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); - expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleDefaultValue)); + expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleMaxValue)); }); it('should revert existing value when input value is lower than the min of the stepper', () => { const exampleLabelValue: string = 'Stepper'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; - const exampleDefaultValue: number = 12; - const exampleNewValue: number = 0; + const exampleDefaultValue: string = '12'; + const exampleNewValue: string = '0'; const renderedDOM: HTMLElement = renderIntoDocument( { ReactTestUtils.Simulate.input(inputDOM, mockEvent(String(exampleNewValue))); ReactTestUtils.Simulate.blur(inputDOM); - expect(inputDOM.value).to.equal(String(exampleDefaultValue)); + expect(inputDOM.value).to.equal(String(exampleMinValue)); expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); - expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleDefaultValue)); - + expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleMinValue)); }); - it('should show value with unit on the stepper', () => { - const exampleLabelValue: string = 'Stepper'; - const exampleMinValue: number = 2; - const exampleMaxValue: number = 22; - const exampleDefaultValue: number = 12; - const exampleNewValue: number = 13; - const exampleValidUnitOptions: string[] = ['"']; - - const renderedDOM: HTMLElement = renderIntoDocument( - - ); - - // Assert on the input element. - const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; - ReactTestUtils.Simulate.input(inputDOM, mockEvent(String(exampleNewValue))); - ReactTestUtils.Simulate.blur(inputDOM); - - expect(inputDOM.value).to.equal(String(exampleNewValue) + exampleValidUnitOptions[0]); - expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); - expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); - expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleNewValue)); - - }); - - it('should show new value with unit update on the stepper', () => { - const exampleLabelValue: string = 'Stepper'; - const exampleMinValue: number = 2; - const exampleMaxValue: number = 22; - const exampleDefaultValue: number = 12; - const exampleNewValue: number = 13; - const exampleValidUnitOptions: string[] = ['"', 'pt']; - - const renderedDOM: HTMLElement = renderIntoDocument( - - ); - - // Assert on the input element. - const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; - ReactTestUtils.Simulate.input(inputDOM, mockEvent(String(exampleNewValue) + exampleValidUnitOptions[1])); - ReactTestUtils.Simulate.blur(inputDOM); - - expect(inputDOM.value).to.equal(String(exampleNewValue) + ' ' + exampleValidUnitOptions[1]); - expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); - expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); - expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleNewValue)); - - }); - - it('should revert existing value and unit when input unit is invalid the stepper', () => { - const exampleLabelValue: string = 'Stepper'; - const exampleMinValue: number = 2; - const exampleMaxValue: number = 22; - const exampleDefaultValue: number = 12; - const exampleNewValue: number = 13; - const exampleValidUnitOptions: string[] = ['"', 'pt']; - const exampleNewInvalidUnit: string = 'invalidValue'; - - const renderedDOM: HTMLElement = renderIntoDocument( - - ); - - // Assert on the input element. - const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; - ReactTestUtils.Simulate.input(inputDOM, mockEvent(String(exampleNewValue) + ' ' + exampleNewInvalidUnit)); - ReactTestUtils.Simulate.blur(inputDOM); - - expect(inputDOM.value).to.equal(String(exampleDefaultValue) + exampleValidUnitOptions[0]); - expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); - expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); - expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleDefaultValue)); - - }); - - it('should use onErrorMessage passed to the stepper (with valid input)', () => { + it('should use onBlur passed to the stepper (with valid input)', () => { const errorMessage: string = 'The value is invalid'; const exampleLabelValue: string = 'Stepper'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; - const exampleDefaultValue: number = 12; - const exampleNewValue: number = 21; + const exampleDefaultValue: string = '12'; + const exampleNewValue: string = '21'; function validator(newValue: string, state: IStepperState, props: IStepperProps): string { - let numberValue: number = Number(newValue); - - return (!isNaN(numberValue) && numberValue >= props.min && numberValue <= props.max) ? '' : errorMessage; + let numberValue: number = +newValue; + return (!isNaN(numberValue) && numberValue >= props.min && numberValue <= props.max) ? newValue : errorMessage; } const renderedDOM: HTMLElement = renderIntoDocument( @@ -501,7 +407,7 @@ describe('Stepper', () => { min={ exampleMinValue } max={ exampleMaxValue } defaultValue={ exampleDefaultValue } - onGetErrorMessage={ validator } + onBlur={ validator } /> ); @@ -514,27 +420,20 @@ describe('Stepper', () => { expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleNewValue)); - }); - it('should use onErrorMessage passed to the stepper (with invalid input)', () => { + it('should use onBlur passed to the stepper (with invalid input)', () => { const errorMessage: string = 'The value is invalid'; const exampleLabelValue: string = 'Stepper'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; - const exampleDefaultValue: number = 12; - const exampleNewValue: number = 100; + const exampleDefaultValue: string = '12'; + const exampleNewValue: string = '100'; function validator(newValue: string, state: IStepperState, props: IStepperProps): string { let numberValue: number = Number(newValue); - return (!isNaN(numberValue) && numberValue >= props.min && numberValue <= props.max) ? '' : errorMessage; - } - - function assertErrorMessage(renderedDOM: HTMLElement, expectedErrorMessage: string): void { - const errorMessageDOM: HTMLElement = - renderedDOM.querySelector('[data-automation-id=error-message]') as HTMLElement; - expect(errorMessageDOM.textContent).to.equal(expectedErrorMessage); + return (!isNaN(numberValue) && numberValue >= props.min && numberValue <= props.max) ? newValue : errorMessage; } const renderedDOM: HTMLElement = renderIntoDocument( @@ -543,7 +442,7 @@ describe('Stepper', () => { min={ exampleMinValue } max={ exampleMaxValue } defaultValue={ exampleDefaultValue } - onGetErrorMessage={ validator } + onBlur={ validator } /> ); @@ -552,19 +451,17 @@ describe('Stepper', () => { ReactTestUtils.Simulate.input(inputDOM, mockEvent(String(exampleNewValue))); ReactTestUtils.Simulate.blur(inputDOM); - expect(inputDOM.value).to.equal(String(exampleNewValue)); + expect(inputDOM.value).to.equal(String(errorMessage)); expect(inputDOM.getAttribute('aria-valuemin')).to.equal(String(exampleMinValue)); expect(inputDOM.getAttribute('aria-valuemax')).to.equal(String(exampleMaxValue)); - expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleDefaultValue)); - assertErrorMessage(renderedDOM, errorMessage); - + expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(errorMessage)); }); it('should stop spinning if text field is focused while actively spinning', () => { const exampleLabelValue: string = 'Stepper'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; - const exampleDefaultValue: number = 2; + const exampleDefaultValue: string = '12'; function delay(millisecond: number): Promise { return new Promise((resolve) => setTimeout(resolve, millisecond)); diff --git a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx index 7758f13bf721f..b2cd9ea92a084 100644 --- a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx +++ b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { - TextField, IconButton, Label } from '../../../lib/'; @@ -21,75 +20,97 @@ export interface IStepperState { /** * the value of the stepper */ - value?: number; + value?: string; /** * Are we spinning? Used in case we are spinning * and the text field gets focus (we should stop spinning) */ spinning?: boolean; - - /** - * the index into the unit options array that corresponds - * to the current unit that is being used in the textField - */ - currentUnitIndex?: number; } export class Stepper extends BaseComponent implements IStepper { - private _textField: TextField; - private _stepperId: string; + private _input: HTMLInputElement; + private _inputId: string; private _labelId: string; + private _lastValidValue: string; + + private _onBlur?: (value: string, state: IStepperState, props: IStepperProps) => string; + private _onIncrement?: (value: string) => string; + private _onDecrement?: (value: string) => string; + private _defaultOnBlur = (value: string, state: IStepperState, props: IStepperProps) => { + if (isNaN(+value)) { + return this._lastValidValue; + } + const newValue = Math.min(this.props.max, Math.max(this.props.min, +value)); + return String(newValue); + } + private _defaultOnIncrement = (value: string) => { + let newValue = Math.min(+value + this.props.step, this.props.max); + return String(newValue); + }; + private _defaultOnDecrement = (value: string) => { + let newValue = Math.max(+value - this.props.step, this.props.min); + return String(newValue); + }; - private _onGetErrorMessage?: (value: string, state: IStepperState, props: IStepperProps) => string; public static defaultProps: IStepperProps = { step: 1, min: 0, max: 100, disabled: false, - defaultValue: 0 + defaultValue: '0' }; private _currentStepFunctionHandle: number; - private _stepDelay = 50; + private _stepDelay = 100; private _formattedValidUnitOptions: string[] = []; constructor(props?: IStepperProps) { super(props); - let value = props.value || props.defaultValue || props.min; + let value = props.value || props.defaultValue || String(props.min); + this._lastValidValue = value; + this.state = { value: value, spinning: false, - currentUnitIndex: -1 }; this._labelId = getId('Label'); - this._stepperId = getId('Stepper'); + this._inputId = getId('input'); + + if (props.onBlur) { + this._onBlur = props.onBlur; + } else { + this._onBlur = this._defaultOnBlur; + } + this._onBlur = this._onBlur.bind(this); + + if (props.onIncrement) { + this._onIncrement = props.onIncrement; + this._onIncrement = this._onIncrement.bind(this); + } else { + this._onIncrement = this._defaultOnIncrement; + } - if (props.onGetErrorMessage) { - this._onGetErrorMessage = props.onGetErrorMessage; - this._onGetErrorMessage = this._onGetErrorMessage.bind(this); + if (props.onDecrement) { + this._onDecrement = props.onDecrement; + this._onDecrement = this._onDecrement.bind(this); + } else { + this._onDecrement = this._defaultOnDecrement; } // bind this (for this class) to all the methods - this._getErrorMessage = this._getErrorMessage.bind(this); - this._parseNumIfValidSuffix = this._parseNumIfValidSuffix.bind(this); + this._blur = this._blur.bind(this); this._increment = this._increment.bind(this); this._decrement = this._decrement.bind(this); this._stop = this._stop.bind(this); this.focus = this.focus.bind(this); this._handleKeyDown = this._handleKeyDown.bind(this); this._handleKeyUp = this._handleKeyUp.bind(this); - this._getCurrentUnit = this._getCurrentUnit.bind(this); - this._normalizeUnit = this._normalizeUnit.bind(this); - this._formatUnitOptions = this._formatUnitOptions.bind(this); - this._updateValueByStep = this._updateValueByStep.bind(this); - - // Format all the unit options passed in to - // simplify the work we have to do when rendering the full value - this._formatUnitOptions(); + this._onInputChange = this._onInputChange.bind(this); } /** @@ -97,10 +118,10 @@ export class Stepper extends BaseComponent impleme */ public componentWillReceiveProps(newProps: IStepperProps): void { if (newProps.value !== undefined) { - let value = Math.max(newProps.min, Math.min(newProps.max, newProps.value)); + let value = Math.max(newProps.min, Math.min(newProps.max, +newProps.value)); this.setState({ - value: value + value: String(value) }); } } @@ -109,8 +130,6 @@ export class Stepper extends BaseComponent impleme const { className, disabled, - min, - max, label } = this.props; @@ -120,20 +139,21 @@ export class Stepper extends BaseComponent impleme return (
- { label && } - { label } } + impleme ) as React.ReactElement<{}>; } + private _onChange() { + /** + * A noop input change handler. + * https://github.com/facebook/react/issues/7027. + * Using the native onInput handler fixes the issue but onChange + * still need to be wired to avoid React console errors + * TODO: Check if issue is resolved when React 16 is available. + */ + } + /** - * OnFocus select the contents of the textField + * OnFocus select the contents of the input */ public focus() { if (this.state.spinning) { this._stop(); } - this._textField.focus(); - this._textField.select(); - } - - /** - * Format the unit options passed in so we do not have - * to think about it when rendering the unit with the value - */ - private _formatUnitOptions() { - if (this.props.validUnitOptions == null || this.props.validUnitOptions.length == 0) { - return; - } - - this.props.validUnitOptions.forEach(element => { - this._formattedValidUnitOptions = this._formattedValidUnitOptions.concat(this._normalizeUnit(element)); - }); - } - - /** - * Normalize the given unit (e.g. if it is only alpha characters - * and does not start with a space, add a space otherwise - * do not alter the value) - * @param unitValue - the unit to normalize - * @returns the normalized unit - */ - private _normalizeUnit(unitValue: string): string { - - if (unitValue == null || unitValue.length == 0) { - return ''; - } - - let formattedUnit: string = unitValue; - - // if our unitValue only contains alpha chars and does not - // already start with a space, add one - if (formattedUnit.match('^([a-zA-Z]+)$') && formattedUnit.indexOf(' ') != 0) { - formattedUnit = String(' ').concat(formattedUnit); - } - - return formattedUnit; - } - - /** - * Returns the currently formatted unit (based off of state) - */ - private _getCurrentUnit(): string { - const currentUnitIndex = this.state.currentUnitIndex; - - if (this._formattedValidUnitOptions == null || - this._formattedValidUnitOptions.length == 0 || - currentUnitIndex >= this._formattedValidUnitOptions.length) { - return ''; - } - else if (currentUnitIndex <= -1) { - return this._formattedValidUnitOptions[0]; - } - - return this._formattedValidUnitOptions[currentUnitIndex]; + this._input.focus(); + this._input.select(); } /** @@ -242,148 +216,23 @@ export class Stepper extends BaseComponent impleme * @param newValue - the pending value to check if it is valid * @returns an error message to display to the user, empty string if no error */ - private _getErrorMessage(newValue: string): string { - let errorMessage: string = ''; - - if (this._onGetErrorMessage) { - errorMessage = this._onGetErrorMessage(newValue, this.state, this.props); - - if (errorMessage == '' && Number(newValue) != this.state.value) { - this.setState({ value: Number(newValue) }); - } - - return errorMessage; - } - - const { - value, - currentUnitIndex, - } = this.state; - - let valueToSet: number = null; - let unitIndexToSet: number = null; - - // Attempt to parse the number from the new value, - // checking against the valid unit options. It returns - // a tuple of [ , ] - let parsedNumberInfo: number[] = this._parseNumIfValidSuffix(newValue); - - // Did we get back a well formatted response from the parse? - if (parsedNumberInfo != null && parsedNumberInfo.length == 2) { - let parsedNumberValue: number = parsedNumberInfo[0]; - let parsedNumberUnitIndex: number = parsedNumberInfo[1]; - - // We have a perspective number value, make sure it is valid - // before going any further - if (parsedNumberValue != null && - (parsedNumberValue >= this.props.min && parsedNumberValue <= this.props.max)) { - valueToSet = parsedNumberValue; - } - - // We have a perspective index into the unit options array, make sure - // it is valid before going any further - if (parsedNumberUnitIndex != null && parsedNumberUnitIndex < this._formattedValidUnitOptions.length) { - unitIndexToSet = parsedNumberUnitIndex; - } + private _blur(event: React.FocusEvent) { + const element: HTMLInputElement = event.target as HTMLInputElement; + const value: string = element.value; + if (this.state.value) { + var newValue = this._onBlur(value, this.state, this.props); + this._lastValidValue = newValue; + this.setState({ value: newValue }); } - - // At this point, if parsing the new value was successful, - // the value and unit index to set should not be null - if (valueToSet == null || unitIndexToSet == null) { - errorMessage = `${newValue} is not a valid value`; - } - - // If the value to set is null, fall back - // to a known valid value - if (valueToSet == null) { - if (value == null) { - valueToSet = this.props.min; - } - else { - valueToSet = value; - } - } - - // If the unit index is null, fall back - // to a known valid unit index - if (unitIndexToSet == null) { - if (currentUnitIndex == null) { - unitIndexToSet = -1; - } - else { - unitIndexToSet = currentUnitIndex; - } - } - - let stateToSet: any = {}; - - if (value != valueToSet) { - assign(stateToSet, { value: valueToSet }); - } - - if (currentUnitIndex != unitIndexToSet) { - assign(stateToSet, { currentUnitIndex: unitIndexToSet }); - } - - if (stateToSet != {}) { - this.setState({ ...stateToSet }); - } - - return errorMessage; } - /** - * Attempt to parse the number and units that - * the user entered - * @param value - the value to process - * @returns a number array of the format: - * null if we fail to parse the value, or [, ] - */ - private _parseNumIfValidSuffix(value: string): number[] { - if (value == null) { - return null; - } - - let valToConvert = value.trim(); - if (valToConvert.length == 0) { - return null; - } - - // Check to see if the value is alreay just a number, - // if so, convert it and return (keeping the current unit) - if (!isNaN(Number(valToConvert))) { - return [Number(valToConvert), this.state.currentUnitIndex]; - } - - // if we got a value that is not a number, we better have - // some valid unit options, otherwise we know this is invalid and we are done - if (this.props.validUnitOptions == null || this.props.validUnitOptions.length == 0) { - return null - } - - let index = -1; - let optionIndex = 0; + private _onInputChange(event: React.FormEvent): void { + const element: HTMLInputElement = event.target as HTMLInputElement; + const value: string = element.value; - // loop over the valid unit options to see if we've got a match - // at the end of the string (after the being trimmed) - do { - index = valToConvert.indexOf(this.props.validUnitOptions[optionIndex]); - } while (index === -1 && ++optionIndex < this.props.validUnitOptions.length); - - // Did we find a valid unit match? If not, return null - if (index === -1 || index + this.props.validUnitOptions[optionIndex].length != valToConvert.length) { - return null; - } - - // If we mad it here we found a matching unit, now - // parse the number from the value - let parsedNumbers: string = valToConvert.substring(0, index); - let numberValue: number = Number(parsedNumbers); - - // Is what we parsed a valid number? - numberValue = isNaN(numberValue) ? null : numberValue; - - return [numberValue, optionIndex]; + this.setState({ + value: value, + }); } /** @@ -392,8 +241,10 @@ export class Stepper extends BaseComponent impleme * True by default and useful if we get here from a mousedown event * on one of the buttons. False if this fired from a keydown event */ - private _increment(shouldSpin: boolean = true) { - this._updateValueByStep(shouldSpin, true /* increase */); + _increment(shouldSpin: boolean = true) { + var newValue = this._onIncrement(this.state.value); + this._lastValidValue = newValue; + this.setState({ value: newValue }); } /** @@ -402,50 +253,10 @@ export class Stepper extends BaseComponent impleme * True by default and useful if we get here from a mousedown event * on one of the buttons. False if this fired from a keydown event */ - private _decrement(shouldSpin: boolean = true) { - this._updateValueByStep(shouldSpin, false /* increase */); - } - - /** - * Update (increment or decrement) the current value by the step value (from props) - * @param shouldSpin - When finished updating, should we spin up another update? - * (for example, if responding to mousedown should be true so that the value will spin - * (since only one mousedown fires); but if responding to a keydown this should be false - * (since keydowns continue to fire as long as the key is depressed)) - * @param increase - Are we increasing the value? - */ - private _updateValueByStep(shouldSpin: boolean, increase: boolean) { - if (this.props.disabled) { - this._stop(); - return; - } - - let direction: number = increase ? 1 : -1; - let updatedValue: number = this.state.value + (direction * this.props.step); - let isnewValueWithinRange: boolean = updatedValue <= this.props.max && updatedValue >= this.props.min; - - // Only update the value if it is valid - if (isnewValueWithinRange) { - let stateToSet: any = {}; - - assign(stateToSet, { value: updatedValue }); - - if (this.state.spinning != shouldSpin) { - assign(stateToSet, { spinning: shouldSpin }) - } - - if (stateToSet != {}) { - this.setState({ ...stateToSet }); - } - - // spin up the next update if should spin is true - if (shouldSpin) { - this._currentStepFunctionHandle = window.setTimeout(() => { this._updateValueByStep(shouldSpin, increase); }, this._stepDelay); - return; - } - } - - this._stop(); + _decrement(shouldSpin: boolean = true) { + var newValue = this._onDecrement(this.state.value); + this._lastValidValue = newValue; + this.setState({ value: newValue }); } /** @@ -484,7 +295,6 @@ export class Stepper extends BaseComponent impleme event.currentTarget.blur(); this.focus(); } - } /** From 8e28b993fe2f2420601ba99f9b019065b7781c1a Mon Sep 17 00:00:00 2001 From: Boris Emorine Date: Thu, 13 Apr 2017 11:15:42 -0700 Subject: [PATCH 08/64] Modified example implementations. --- .../examples/Stepper.Basic.Example.tsx | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx b/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx index 4b32fdb7b58df..d7083a3022635 100644 --- a/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx +++ b/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx @@ -11,20 +11,28 @@ export class StepperBasicExample extends React.Component {
- { - return '2'; + if (isNaN(+value)) { + return '0' + } + + const newValue = Math.min(100, Math.max(0, +value)); + return String(newValue); } } onIncrement={ (value: string) => { - return String(+value + 1); + return String(+value + 2); } } onDecrement={ (value: string) => { - return String(+value - 1); + return String(+value - 2); } } /> From 4ccaf1d13bd6170691980794acc905ba35fc3e9c Mon Sep 17 00:00:00 2001 From: Boris Emorine Date: Thu, 13 Apr 2017 11:15:59 -0700 Subject: [PATCH 09/64] Add aria-valuemax. --- .../office-ui-fabric-react/src/components/Stepper/Stepper.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx index b2cd9ea92a084..f7bdf0dc2040d 100644 --- a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx +++ b/packages/office-ui-fabric-react/src/components/Stepper/Stepper.tsx @@ -86,7 +86,6 @@ export class Stepper extends BaseComponent impleme } else { this._onBlur = this._defaultOnBlur; } - this._onBlur = this._onBlur.bind(this); if (props.onIncrement) { this._onIncrement = props.onIncrement; @@ -139,7 +138,8 @@ export class Stepper extends BaseComponent impleme return (
- aria-valuemin={ this.props.min } + aria-valuemin={ String(this.props.min) } + aria-valuemax={ String(this.props.max) } { label && } Date: Thu, 13 Apr 2017 11:32:48 -0700 Subject: [PATCH 10/64] Change Stepper to SpinButton. --- apps/fabric-examples/src/AppDefinition.tsx | 8 +- .../pages/SpinButtonPage/SpinButtonPage.tsx | 67 +++++++++++++ .../examples/SpinButton.Basic.Example.scss | 3 + .../examples/SpinButton.Basic.Example.tsx} | 18 ++-- .../src/pages/StepperPage/StepperPage.tsx | 67 ------------- .../examples/Stepper.Basic.Example.scss | 3 - .../src/components/App/AppState.ts | 6 +- .../office-ui-fabric-react/src/Stepper.ts | 2 +- .../SpinButton.Props.ts} | 24 ++--- .../SpinButton.scss} | 4 +- .../SpinButton.test.tsx} | 94 +++++++++---------- .../Stepper.tsx => SpinButton/SpinButton.tsx} | 26 ++--- .../src/components/SpinButton/index.ts | 2 + .../src/components/Stepper/index.ts | 2 - 14 files changed, 163 insertions(+), 163 deletions(-) create mode 100644 apps/fabric-examples/src/pages/SpinButtonPage/SpinButtonPage.tsx create mode 100644 apps/fabric-examples/src/pages/SpinButtonPage/examples/SpinButton.Basic.Example.scss rename apps/fabric-examples/src/pages/{StepperPage/examples/Stepper.Basic.Example.tsx => SpinButtonPage/examples/SpinButton.Basic.Example.tsx} (59%) delete mode 100644 apps/fabric-examples/src/pages/StepperPage/StepperPage.tsx delete mode 100644 apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.scss rename packages/office-ui-fabric-react/src/components/{Stepper/Stepper.Props.ts => SpinButton/SpinButton.Props.ts} (60%) rename packages/office-ui-fabric-react/src/components/{Stepper/Stepper.scss => SpinButton/SpinButton.scss} (96%) rename packages/office-ui-fabric-react/src/components/{Stepper/Stepper.test.tsx => SpinButton/SpinButton.test.tsx} (87%) rename packages/office-ui-fabric-react/src/components/{Stepper/Stepper.tsx => SpinButton/SpinButton.tsx} (92%) create mode 100644 packages/office-ui-fabric-react/src/components/SpinButton/index.ts delete mode 100644 packages/office-ui-fabric-react/src/components/Stepper/index.ts diff --git a/apps/fabric-examples/src/AppDefinition.tsx b/apps/fabric-examples/src/AppDefinition.tsx index 86652034b4be0..33ee8bf8f9658 100644 --- a/apps/fabric-examples/src/AppDefinition.tsx +++ b/apps/fabric-examples/src/AppDefinition.tsx @@ -220,11 +220,11 @@ export const AppDefinition: IAppDefinition = { url: '#/examples/spinner' }, { - getComponent: cb => require.ensure([], () => cb(require('./pages/StepperPage/StepperPage').StepperPage)), - key: 'Stepper', - name: 'Stepper', + getComponent: cb => require.ensure([], () => cb(require('./pages/SpinButtonPage/SpinButtonPage').SpinButtonPage)), + key: 'SpinButton', + name: 'SpinButton', status: ExampleStatus.beta, - url: '#/examples/stepper' + url: '#/examples/SpinButton' }, { getComponent: cb => require.ensure([], () => cb(require('./pages/TeachingBubblePage/TeachingBubblePage').TeachingBubblePage)), diff --git a/apps/fabric-examples/src/pages/SpinButtonPage/SpinButtonPage.tsx b/apps/fabric-examples/src/pages/SpinButtonPage/SpinButtonPage.tsx new file mode 100644 index 0000000000000..92777b863c8bf --- /dev/null +++ b/apps/fabric-examples/src/pages/SpinButtonPage/SpinButtonPage.tsx @@ -0,0 +1,67 @@ +import * as React from 'react'; +import { + ExampleCard, + IComponentDemoPageProps, + ComponentPage, + PropertiesTableSet +} from '@uifabric/example-app-base'; +import { SpinButtonBasicExample } from './examples/SpinButton.Basic.Example'; + +const SpinButtonBasicExampleCode = require('./examples/SpinButton.Basic.Example.tsx') as string; + +export class SpinButtonPage extends React.Component { + public render() { + return ( + + + + } + propertiesTables={ + ('office-ui-fabric-react/lib/components/SpinButton/SpinButton.Props.ts') + ] } + /> + } + overview={ +
+

+ A SpinButton allows the user to incrementaly adjust a value in small steps. It is mainly used for numeric values, but other values are supported too. +

+
+ } + bestPractices={ +
+ } + dos={ +
+
    +
  • Use a SpinButton when changing a value with precise control.
  • +
  • Use a SpinButton when values are tied to a unit.
  • +
  • Include a label indicating what value the SpinButton changes.
  • +
+
+ } + donts={ +
+
    +
  • Don’t use a SpinButton if the range of values is large.
  • +
  • Don’t use a SpinButton for binary settings.
  • +
  • Don't use a SpinButton for a range of three values or less.
  • +
+
+ } + related={ + Fabric JS + } + isHeaderVisible={ this.props.isHeaderVisible }> +
+ ); + } +} diff --git a/apps/fabric-examples/src/pages/SpinButtonPage/examples/SpinButton.Basic.Example.scss b/apps/fabric-examples/src/pages/SpinButtonPage/examples/SpinButton.Basic.Example.scss new file mode 100644 index 0000000000000..c57b8b652bf5d --- /dev/null +++ b/apps/fabric-examples/src/pages/SpinButtonPage/examples/SpinButton.Basic.Example.scss @@ -0,0 +1,3 @@ +.ms-BasicSpinButtonsExample { + max-width: 300px; +} \ No newline at end of file diff --git a/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx b/apps/fabric-examples/src/pages/SpinButtonPage/examples/SpinButton.Basic.Example.tsx similarity index 59% rename from apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx rename to apps/fabric-examples/src/pages/SpinButtonPage/examples/SpinButton.Basic.Example.tsx index d7083a3022635..4b9b6910f93be 100644 --- a/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.tsx +++ b/apps/fabric-examples/src/pages/SpinButtonPage/examples/SpinButton.Basic.Example.tsx @@ -1,26 +1,26 @@ import * as React from 'react'; -import { Stepper, IStepperState, IStepperProps } from 'office-ui-fabric-react/lib/Stepper'; +import { SpinButton, ISpinButtonState, ISpinButtonProps } from 'office-ui-fabric-react/lib/SpinButton'; import { Label, assign } from 'office-ui-fabric-react/lib'; //import { assign } from 'office-ui-fabric-react/lib/Utilities'; -import './Stepper.Basic.Example.scss'; +import './SpinButton.Basic.Example.scss'; -export class StepperBasicExample extends React.Component { +export class SpinButtonBasicExample extends React.Component { public render() { return ( -
+
- - < Stepper - label='Stepper with custom implementation' + < SpinButton + label='SpinButton with custom implementation' defaultValue={ '7' } - onBlur={ (value: string, state: IStepperState, props: IStepperProps) => { + onBlur={ (value: string, state: ISpinButtonState, props: ISpinButtonProps) => { if (isNaN(+value)) { return '0' } diff --git a/apps/fabric-examples/src/pages/StepperPage/StepperPage.tsx b/apps/fabric-examples/src/pages/StepperPage/StepperPage.tsx deleted file mode 100644 index 32d11adf508f5..0000000000000 --- a/apps/fabric-examples/src/pages/StepperPage/StepperPage.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import * as React from 'react'; -import { - ExampleCard, - IComponentDemoPageProps, - ComponentPage, - PropertiesTableSet -} from '@uifabric/example-app-base'; -import { StepperBasicExample } from './examples/Stepper.Basic.Example'; - -const StepperBasicExampleCode = require('./examples/Stepper.Basic.Example.tsx') as string; - -export class StepperPage extends React.Component { - public render() { - return ( - - - - } - propertiesTables={ - ('office-ui-fabric-react/lib/components/Stepper/Stepper.Props.ts') - ] } - /> - } - overview={ -
-

- A Stepper allows the user to incrementaly adjust a value in small steps. It is mainly used for numeric values, but other values are supported too. -

-
- } - bestPractices={ -
- } - dos={ -
-
    -
  • Use a Stepper when changing a value with precise control.
  • -
  • Use a Stepper when values are tied to a unit.
  • -
  • Include a label indicating what value the Stepper changes.
  • -
-
- } - donts={ -
-
    -
  • Don’t use a Stepper if the range of values is large.
  • -
  • Don’t use a Slider for binary settings.
  • -
  • Don't use a Stepper for a range of three values or less.
  • -
-
- } - related={ - Fabric JS - } - isHeaderVisible={ this.props.isHeaderVisible }> -
- ); - } -} diff --git a/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.scss b/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.scss deleted file mode 100644 index c38144d4a29a3..0000000000000 --- a/apps/fabric-examples/src/pages/StepperPage/examples/Stepper.Basic.Example.scss +++ /dev/null @@ -1,3 +0,0 @@ -.ms-BasicSteppersExample { - max-width: 300px; -} \ No newline at end of file diff --git a/apps/fabric-website/src/components/App/AppState.ts b/apps/fabric-website/src/components/App/AppState.ts index 55cf14d8a5e2c..572aa1f90eef2 100644 --- a/apps/fabric-website/src/components/App/AppState.ts +++ b/apps/fabric-website/src/components/App/AppState.ts @@ -246,9 +246,9 @@ export const AppState: IAppState = { getComponent: cb => require.ensure([], (require) => cb(require('../../pages/Components/SpinnerComponentPage').SpinnerComponentPage)) }, { - title: 'Stepper', - url: '#/components/stepper', - getComponent: cb => require.ensure([], (require) => cb(require('../../pages/Components/StepperComponentPage').StepperComponentPage)) + title: 'SpinButton', + url: '#/components/SpinButton', + getComponent: cb => require.ensure([], (require) => cb(require('../../pages/Components/SpinButtonComponentPage').SpinButtonComponentPage)) }, { title: 'TextField', diff --git a/packages/office-ui-fabric-react/src/Stepper.ts b/packages/office-ui-fabric-react/src/Stepper.ts index bd5b02d4f0135..25568549f09b5 100644 --- a/packages/office-ui-fabric-react/src/Stepper.ts +++ b/packages/office-ui-fabric-react/src/Stepper.ts @@ -1 +1 @@ -export * from './components/Stepper/index'; +export * from './components/SpinButton/index'; diff --git a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts similarity index 60% rename from packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts rename to packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts index 4e507783fa580..f9e79caf839d4 100644 --- a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.Props.ts +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts @@ -1,35 +1,35 @@ import * as React from 'react'; -import { IStepperState } from './Stepper'; +import { ISpinButtonState } from './SpinButton'; -export interface IStepperProps { +export interface ISpinButtonProps { /** - * The initial value of the Stepper. Use this if you intend for the Stepper to be an uncontrolled component. + * The initial value of the SpinButton. Use this if you intend for the SpinButton to be an uncontrolled component. * This value is mutually exclusive to value. Use one or the other. * @default 0 */ defaultValue?: string; /** - * The initial value of the Stepper. Use this if you intend to pass in a new value as a result of onChange events. + * The initial value of the SpinButton. Use this if you intend to pass in a new value as a result of onChange events. * This value is mutually exclusive to defaultValue. Use one or the other. */ value?: string; /** - * The min value of the Stepper. + * The min value of the SpinButton. * @default 0 */ min?: number; /** - * The max value of the Stepper. + * The max value of the SpinButton. * @default 10 */ max?: number; /** - * The diffrrence between the two adjacent values of the Stepper. + * The diffrrence between the two adjacent values of the SpinButton. * @default 1 */ step?: number; @@ -40,17 +40,17 @@ export interface IStepperProps { onChange?: (value: number) => void; /** - * A description of the Stepper for the benefit of screen readers. + * A description of the SpinButton for the benefit of screen readers. */ ariaLabel?: string; /** - * Whether or not the Stepper is disabled. + * Whether or not the SpinButton is disabled. */ disabled?: boolean; /** - * Optional className for Stepper. + * Optional className for SpinButton. */ className?: string; @@ -68,13 +68,13 @@ export interface IStepperProps { * show a red border and show an error message below the text field. * */ - onBlur?: (value: string, state: IStepperState, props: IStepperProps) => string; + onBlur?: (value: string, state: ISpinButtonState, props: ISpinButtonProps) => string; onIncrement?: (value: string) => string; onDecrement?: (value: string) => string; } -export interface IStepper { +export interface ISpinButton { value?: number } \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.scss b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.scss similarity index 96% rename from packages/office-ui-fabric-react/src/components/Stepper/Stepper.scss rename to packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.scss index fbae71f09b25b..5cf3422cb8908 100644 --- a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.scss +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.scss @@ -4,12 +4,12 @@ :global { .arrowBox, .textField, -.upButton, .downButton, .stepperContainer { +.upButton, .downButton, .spinButtonContainer { outline: none; font-size: 12px; } -.stepperContainer { +.spinButtonContainer { width: 100px; height: 32px; min-width: 100px; diff --git a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.test.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.test.tsx similarity index 87% rename from packages/office-ui-fabric-react/src/components/Stepper/Stepper.test.tsx rename to packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.test.tsx index 49c75db28715f..c559b117cc892 100644 --- a/packages/office-ui-fabric-react/src/components/Stepper/Stepper.test.tsx +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.test.tsx @@ -3,15 +3,15 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import * as ReactTestUtils from 'react-addons-test-utils'; import { - Stepper, - IStepperState -} from './Stepper'; -import { IStepperProps } from './Stepper.Props'; + SpinButton, + ISpinButtonState +} from './SpinButton'; +import { ISpinButtonProps } from './SpinButton.Props'; import { KeyCodes } from '../../Utilities'; const expect: Chai.ExpectStatic = chai.expect; -describe('Stepper', () => { +describe('SpinButton', () => { function renderIntoDocument(element: React.ReactElement): HTMLElement { const component = ReactTestUtils.renderIntoDocument(element); const renderedDOM: Element = ReactDOM.findDOMNode(component as React.ReactInstance); @@ -25,13 +25,13 @@ describe('Stepper', () => { } it('should render a spinner with the default value on the input element', () => { - const exampleLabelValue: string = 'Stepper'; + const exampleLabelValue: string = 'SpinButton'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; const exampleDefaultValue: string = '12'; const renderedDOM: HTMLElement = renderIntoDocument( - { expect(labelDOM.htmlFor).to.equal(inputDOM.id); }); - it('should increment the value in the stepper via the up button', () => { - const exampleLabelValue: string = 'Stepper'; + it('should increment the value in the spin button via the up button', () => { + const exampleLabelValue: string = 'SpinButton'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; const exampleDefaultValue: string = '12'; const renderedDOM: HTMLElement = renderIntoDocument( - { }); - it('should decrement the value in the stepper by the down button', () => { - const exampleLabelValue: string = 'Stepper'; + it('should decrement the value in the spin button by the down button', () => { + const exampleLabelValue: string = 'SpinButton'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; const exampleDefaultValue: string = '12'; const renderedDOM: HTMLElement = renderIntoDocument( - { }); - it('should increment the value in the stepper by the up arrow', () => { - const exampleLabelValue: string = 'Stepper'; + it('should increment the value in the spin button by the up arrow', () => { + const exampleLabelValue: string = 'SpinButton'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; const exampleDefaultValue: string = '12'; const renderedDOM: HTMLElement = renderIntoDocument( - { }); - it('should decrement the value in the stepper by the down arrow', () => { - const exampleLabelValue: string = 'Stepper'; + it('should decrement the value in the spin button by the down arrow', () => { + const exampleLabelValue: string = 'SpinButton'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; const exampleDefaultValue: string = '12'; const renderedDOM: HTMLElement = renderIntoDocument( - { }); - it('should increment the value in the stepper by a step value of 2', () => { - const exampleLabelValue: string = 'Stepper'; + it('should increment the value in the spin button by a step value of 2', () => { + const exampleLabelValue: string = 'SpinButton'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; const exampleDefaultValue: string = '12'; const renderedDOM: HTMLElement = renderIntoDocument( - { }); - it('should decrement the value in the stepper by a step value of 2', () => { - const exampleLabelValue: string = 'Stepper'; + it('should decrement the value in the spin button by a step value of 2', () => { + const exampleLabelValue: string = 'SpinButton'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; const exampleDefaultValue: string = '12'; const renderedDOM: HTMLElement = renderIntoDocument( - { }); - it('should set the value of the stepper by manual entry', () => { - const exampleLabelValue: string = 'Stepper'; + it('should set the value of the spin button by manual entry', () => { + const exampleLabelValue: string = 'SpinButton'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; const exampleDefaultValue: string = '12'; const exampleNewValue: string = '21'; const renderedDOM: HTMLElement = renderIntoDocument( - { expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleNewValue)); }); - it('should reset the value of the stepper with invalid manual entry', () => { - const exampleLabelValue: string = 'Stepper'; + it('should reset the value of the spin button with invalid manual entry', () => { + const exampleLabelValue: string = 'SpinButton'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; const exampleDefaultValue: string = '12'; const exampleNewValue: string = 'garbage'; const renderedDOM: HTMLElement = renderIntoDocument( - { expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleDefaultValue)); }); - it('should revert to max value when input value is higher than the max of the stepper', () => { - const exampleLabelValue: string = 'Stepper'; + it('should revert to max value when input value is higher than the max of the spin button', () => { + const exampleLabelValue: string = 'SpinButton'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; const exampleDefaultValue: string = '12'; const exampleNewValue: string = '23'; const renderedDOM: HTMLElement = renderIntoDocument( - { expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleMaxValue)); }); - it('should revert existing value when input value is lower than the min of the stepper', () => { - const exampleLabelValue: string = 'Stepper'; + it('should revert existing value when input value is lower than the min of the spin button', () => { + const exampleLabelValue: string = 'SpinButton'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; const exampleDefaultValue: string = '12'; const exampleNewValue: string = '0'; const renderedDOM: HTMLElement = renderIntoDocument( - { expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleMinValue)); }); - it('should use onBlur passed to the stepper (with valid input)', () => { + it('should use onBlur passed to the spin button (with valid input)', () => { const errorMessage: string = 'The value is invalid'; - const exampleLabelValue: string = 'Stepper'; + const exampleLabelValue: string = 'SpinButton'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; const exampleDefaultValue: string = '12'; const exampleNewValue: string = '21'; - function validator(newValue: string, state: IStepperState, props: IStepperProps): string { + function validator(newValue: string, state: ISpinButtonState, props: ISpinButtonProps): string { let numberValue: number = +newValue; return (!isNaN(numberValue) && numberValue >= props.min && numberValue <= props.max) ? newValue : errorMessage; } const renderedDOM: HTMLElement = renderIntoDocument( - { expect(inputDOM.getAttribute('aria-valuenow')).to.equal(String(exampleNewValue)); }); - it('should use onBlur passed to the stepper (with invalid input)', () => { + it('should use onBlur passed to the spin button (with invalid input)', () => { const errorMessage: string = 'The value is invalid'; - const exampleLabelValue: string = 'Stepper'; + const exampleLabelValue: string = 'SpinButton'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; const exampleDefaultValue: string = '12'; const exampleNewValue: string = '100'; - function validator(newValue: string, state: IStepperState, props: IStepperProps): string { + function validator(newValue: string, state: ISpinButtonState, props: ISpinButtonProps): string { let numberValue: number = Number(newValue); return (!isNaN(numberValue) && numberValue >= props.min && numberValue <= props.max) ? newValue : errorMessage; } const renderedDOM: HTMLElement = renderIntoDocument( - { }); it('should stop spinning if text field is focused while actively spinning', () => { - const exampleLabelValue: string = 'Stepper'; + const exampleLabelValue: string = 'SpinButton'; const exampleMinValue: number = 2; const exampleMaxValue: number = 22; const exampleDefaultValue: string = '12'; @@ -468,7 +468,7 @@ describe('Stepper', () => { } const renderedDOM: HTMLElement = renderIntoDocument( - implements IStepper { +export class SpinButton extends BaseComponent implements ISpinButton { private _input: HTMLInputElement; private _inputId: string; private _labelId: string; private _lastValidValue: string; - private _onBlur?: (value: string, state: IStepperState, props: IStepperProps) => string; + private _onBlur?: (value: string, state: ISpinButtonState, props: ISpinButtonProps) => string; private _onIncrement?: (value: string) => string; private _onDecrement?: (value: string) => string; - private _defaultOnBlur = (value: string, state: IStepperState, props: IStepperProps) => { + private _defaultOnBlur = (value: string, state: ISpinButtonState, props: ISpinButtonProps) => { if (isNaN(+value)) { return this._lastValidValue; } @@ -54,7 +54,7 @@ export class Stepper extends BaseComponent impleme return String(newValue); }; - public static defaultProps: IStepperProps = { + public static defaultProps: ISpinButtonProps = { step: 1, min: 0, max: 100, @@ -67,7 +67,7 @@ export class Stepper extends BaseComponent impleme private _formattedValidUnitOptions: string[] = []; - constructor(props?: IStepperProps) { + constructor(props?: ISpinButtonProps) { super(props); let value = props.value || props.defaultValue || String(props.min); @@ -115,7 +115,7 @@ export class Stepper extends BaseComponent impleme /** * Invoked when a component is receiving new props. This method is not called for the initial render. */ - public componentWillReceiveProps(newProps: IStepperProps): void { + public componentWillReceiveProps(newProps: ISpinButtonProps): void { if (newProps.value !== undefined) { let value = Math.max(newProps.min, Math.min(newProps.max, +newProps.value)); @@ -137,7 +137,7 @@ export class Stepper extends BaseComponent impleme } = this.state; return ( -
+
aria-valuemin={ String(this.props.min) } aria-valuemax={ String(this.props.max) } { label && } diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/index.ts b/packages/office-ui-fabric-react/src/components/SpinButton/index.ts new file mode 100644 index 0000000000000..08acda35da3bd --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/SpinButton/index.ts @@ -0,0 +1,2 @@ +export * from './SpinButton'; +export * from './SpinButton.Props'; diff --git a/packages/office-ui-fabric-react/src/components/Stepper/index.ts b/packages/office-ui-fabric-react/src/components/Stepper/index.ts deleted file mode 100644 index 0786fff919084..0000000000000 --- a/packages/office-ui-fabric-react/src/components/Stepper/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './Stepper'; -export * from './Stepper.Props'; From d699fc05703fb7873d6ee48b6fa09066db95666e Mon Sep 17 00:00:00 2001 From: Boris Emorine Date: Thu, 13 Apr 2017 12:48:34 -0700 Subject: [PATCH 11/64] Add example with unit. --- .../examples/SpinButton.Basic.Example.tsx | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/apps/fabric-examples/src/pages/SpinButtonPage/examples/SpinButton.Basic.Example.tsx b/apps/fabric-examples/src/pages/SpinButtonPage/examples/SpinButton.Basic.Example.tsx index 4b9b6910f93be..f360ee880d87e 100644 --- a/apps/fabric-examples/src/pages/SpinButtonPage/examples/SpinButton.Basic.Example.tsx +++ b/apps/fabric-examples/src/pages/SpinButtonPage/examples/SpinButton.Basic.Example.tsx @@ -6,7 +6,23 @@ import './SpinButton.Basic.Example.scss'; export class SpinButtonBasicExample extends React.Component { + private hasSuffix(string: string, suffix: string): Boolean { + var subString = string.substr(string.length - suffix.length); + return subString == suffix; + } + + private removeSuffix(string: string, suffix: string): string { + if (!this.hasSuffix(string, suffix)) { + return string; + } + + return string.substr(0, string.length - suffix.length); + } + public render() { + + var suffix = " cm"; + return (
@@ -19,20 +35,23 @@ export class SpinButtonBasicExample extends React.Component { < SpinButton label='SpinButton with custom implementation' - defaultValue={ '7' } + defaultValue={ '7' + suffix } onBlur={ (value: string, state: ISpinButtonState, props: ISpinButtonProps) => { + value = this.removeSuffix(value, suffix); if (isNaN(+value)) { - return '0' + return '0' + suffix } const newValue = Math.min(100, Math.max(0, +value)); - return String(newValue); + return String(newValue) + suffix; } } onIncrement={ (value: string) => { - return String(+value + 2); + value = this.removeSuffix(value, suffix); + return String(+value + 2) + suffix; } } onDecrement={ (value: string) => { - return String(+value - 2); + value = this.removeSuffix(value, suffix); + return String(+value - 2) + suffix; } } /> From 4f830cd8c55269368e73bfc6d0cd69430776b977 Mon Sep 17 00:00:00 2001 From: Derek Sun Date: Thu, 13 Apr 2017 14:03:19 -0700 Subject: [PATCH 12/64] Implement color scheme in the ContextualMenu control to enable alternative theming. --- .../ContextualMenu/ContextualMenu.Props.ts | 28 +++++ .../ContextualMenu/ContextualMenu.scss | 53 ++++++-- .../ContextualMenu/ContextualMenu.tsx | 104 +++++++++++++--- .../ContextualMenu/ContextualMenuPage.tsx | 5 + .../ContextualMenu.ColorScheme.Example.tsx | 117 ++++++++++++++++++ 5 files changed, 281 insertions(+), 26 deletions(-) create mode 100644 packages/office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.ColorScheme.Example.tsx diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.Props.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.Props.ts index 52c05de36f339..38b74d0a22a39 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.Props.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.Props.ts @@ -19,6 +19,24 @@ export interface IContextualMenu { } +export enum ContextualMenuColorScheme { + /** + * Icon color: themePrimary, + * Highlighted menu item background color: neutralLighter, + * Expanded menu item background color: neutralQuaternaryAlt, + * Header text color: themePrimary + */ + Default = 0, + + /** + * Icon color: #a6a6a6, + * Highlighted menu item background color: themeDark, + * Expanded menu item background color: themeDarker, + * Header text color: #a6a6a6 + */ + Themed = 1, +} + export interface IContextualMenuProps extends React.Props { /** * Optional callback to access the IContextualMenu interface. Use this instead of ref for accessing @@ -39,6 +57,16 @@ export interface IContextualMenuProps extends React.Props { */ targetElement?: HTMLElement; + /** + * The color scheme determines the following colors: + * - the color of a header menu item + * - the background color of a menu item when it is hovered + * - the colors of a menu item's text + * - the colors of a menu item's icon + * @default ContextualMenuColorScheme.Neutral + */ + colorScheme?: ContextualMenuColorScheme; + /** * How the element should be positioned * @default DirectionalHint.bottomAutoEdge diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.scss b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.scss index 900c080d0fd38..d6f2c697d1c25 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.scss +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.scss @@ -1,8 +1,10 @@ @import '../../common/common'; $ContextualMenu-background: $ms-color-white; -$ContextualMenu-itemHover: $ms-color-neutralLighter; -$ContextualMenu-expandedItemBackground: $ms-color-neutralQuaternaryAlt; +$ContextualMenu-itemHover-Default: $ms-color-neutralLighter; +$ContextualMenu-itemHover-Themed: $ms-color-themeDark; +$ContextualMenu-expandedItemBackground-Default: $ms-color-neutralQuaternaryAlt; +$ContextualMenu-expandedItemBackground-Themed: $ms-color-themeDarker; $ContextualMenu-itemHeight: 36px; $ContextualMenu-iconWidth: 14px; @@ -41,9 +43,6 @@ $ContextualMenu-iconWidth: 14px; padding: 0px 6px; @include text-align(left); - &:hover:not([disabled]) { - background: $ContextualMenu-itemHover; - } &.isDisabled, &[disabled] { color: $ms-color-neutralTertiaryAlt; @@ -53,24 +52,52 @@ $ContextualMenu-iconWidth: 14px; color: $ms-color-neutralTertiaryAlt; } } +} + +.linkDefault { + @extend .link; + + &:hover:not([disabled]) { + background: $ContextualMenu-itemHover-Default; + } :global(.is-focusVisible) &:focus { - background: $ContextualMenu-itemHover; + background: $ContextualMenu-itemHover-Default; } &.isExpanded, &.isExpanded:hover { - background: $ContextualMenu-expandedItemBackground; + background: $ContextualMenu-expandedItemBackground-Default; color: $ms-color-black; font-weight: $ms-font-weight-semibold; } } +.linkThemed { + @extend .link; + + &:hover:not([disabled]) { + background: $ContextualMenu-itemHover-Themed; + color: white; + } + + :global(.is-focusVisible) &:focus { + background: $ContextualMenu-itemHover-Themed; + color: white; + } + + &.isExpanded, + &.isExpanded:hover { + background: $ContextualMenu-expandedItemBackground-Themed; + color: white; + font-weight: $ms-font-weight-semibold; + } +} + .header { @include focus-border(); @include ms-font-s; font-weight: $ms-font-weight-semibold; - color: $ms-color-themePrimary; background: none; border: none; height: $ContextualMenu-itemHeight; @@ -81,6 +108,16 @@ $ContextualMenu-iconWidth: 14px; @include text-align(left); } +.headerDefault { + @extend .header; + color: $ms-color-themePrimary; +} + +.headerThemed { + @extend .header; + color: #a6a6a6; +} + a.link { padding: 0px 6px; text-rendering: auto; diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx index db96ccc56612a..51ac934d10319 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx @@ -1,5 +1,10 @@ import * as React from 'react'; -import { IContextualMenuProps, IContextualMenuItem, ContextualMenuItemType } from './ContextualMenu.Props'; +import { + IContextualMenuProps, + IContextualMenuItem, + ContextualMenuItemType, + ContextualMenuColorScheme +} from './ContextualMenu.Props'; import { DirectionalHint } from '../../common/DirectionalHint'; import { FocusZone, FocusZoneDirection } from '../../FocusZone'; import { @@ -21,7 +26,7 @@ import { Icon, IIconProps } from '../../Icon'; -import styles = require('./ContextualMenu.scss'); +import * as styles from './ContextualMenu.scss'; export interface IContextualMenuState { expandedMenuItemKey?: string; @@ -86,6 +91,8 @@ export function getSubmenuItems(item: IContextualMenuItem) { return item.subMenuProps ? item.subMenuProps.items : item.items; } +const ContextualMenuThemedAltColor = '#a6a6a6'; + export class ContextualMenu extends BaseComponent { // The default ContextualMenu properities have no items and beak, the default submenu direction is right and top. public static defaultProps = { @@ -96,11 +103,13 @@ export class ContextualMenu extends BaseComponent ) : (null) } { submenuProps ? ( // If a submenu properities exists, the submenu will be rendered. - + ) : (null) }
@@ -239,6 +249,14 @@ export class ContextualMenu extends BaseComponent - { this._renderMenuItemChildren(item, index, hasCheckmarks, hasIcons) } +
+ { this._renderMenuItemChildren(item, index, hasCheckmarks, hasIcons, true) }
); } @@ -309,11 +327,11 @@ export class ContextualMenu extends BaseComponent - { this._renderMenuItemChildren(item, index, hasCheckmarks, hasIcons) } + { this._renderMenuItemChildren(item, index, hasCheckmarks, hasIcons, false) }
); } @@ -333,14 +351,16 @@ export class ContextualMenu extends BaseComponent this._onItemMouseDown(item, ev), + onFocus: (ev: any) => this._onItemFocus(item, ev), + onBlur: (ev: any) => this._onItemBlur(item, ev), disabled: item.isDisabled || item.disabled, href: item.href, title: item.title, @@ -353,20 +373,24 @@ export class ContextualMenu extends BaseComponent { (hasCheckmarks) ? ( + onClick={ this._onItemClick.bind(this, item) } + style={ { color: iconColor } } /> ) : (null) } - { (hasIcons) ? ( + { (hasIcons && !isHeader) ? ( this._renderIcon(item) ) : (null) } { item.name } @@ -374,7 +398,8 @@ export class ContextualMenu extends BaseComponent + className={ css('ms-ContextualMenu-submenuIcon', styles.submenuIcon, item.submenuIconProps ? item.submenuIconProps.className : '') } + style={ { color: iconColor } } /> ) : (null) }
); @@ -388,10 +413,15 @@ export class ContextualMenu extends BaseComponent; + // If the current color scheme is Themed, then use the fixed icon color. + if (this.props.colorScheme === ContextualMenuColorScheme.Themed) { + return ; + } else { + return ; + } } @autobind @@ -409,9 +439,45 @@ export class ContextualMenu extends BaseComponent, isHighlighted: boolean) { + const iconElement = ev.currentTarget.getElementsByTagName('i')[0]; + if (this.props.colorScheme === ContextualMenuColorScheme.Themed && iconElement !== undefined) { + iconElement.style.color = this._iconColorForThemedItem(menuItem, isHighlighted); + } + } + + private _onItemFocus(item: any, ev: React.FocusEvent) { + // We automatically focus on the first menu item when we open a context menu, + // but the initial focus is not supposed to be visible, so we need to avoid + // changing the color in that case. + if (this._hasInitialFocusEventHappend) { + this._updateIconColorForMenuItemIfNeeded(item, ev, true); + } else { + this._hasInitialFocusEventHappend = true; + } + } + + private _onItemBlur(item: any, ev: React.FocusEvent) { + this._updateIconColorForMenuItemIfNeeded(item, ev, false); + } + private _onItemMouseEnter(item: any, ev: React.MouseEvent) { let targetElement = ev.currentTarget as HTMLElement; - if (item.key !== this.state.expandedMenuItemKey) { if (hasSubmenuItems(item)) { this._enterTimerId = this._async.setTimeout(() => this._onItemSubMenuExpand(item, targetElement), 500); @@ -419,11 +485,13 @@ export class ContextualMenu extends BaseComponent this._onSubMenuDismiss(ev), 500); } } + this._updateIconColorForMenuItemIfNeeded(item, ev, true); } @autobind - private _onMouseLeave(ev: React.MouseEvent) { + private _onMouseLeave(item: any, ev: React.MouseEvent) { this._async.clearTimeout(this._enterTimerId); + this._updateIconColorForMenuItemIfNeeded(item, ev, false); } private _onItemMouseDown(item: IContextualMenuItem, ev: React.MouseEvent) { diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuPage.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuPage.tsx index 140fd2a9e553a..f94f14478e1a0 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuPage.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuPage.tsx @@ -10,12 +10,14 @@ import { ContextualMenuCheckmarksExample } from './examples/ContextualMenu.Check import { ContextualMenuDirectionalExample } from './examples/ContextualMenu.Directional.Example'; import { ContextualMenuCustomizationExample } from './examples/ContextualMenu.Customization.Example'; import { ContextualMenuHeaderExample } from './examples/ContextualMenu.Header.Example'; +import { ContextualMenuColorSchemeExample } from './examples/ContextualMenu.ColorScheme.Example'; const ContextualMenuBasicExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.Basic.Example.tsx') as string; const ContextualMenuCheckmarksExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.Checkmarks.Example.tsx') as string; const ContextualMenuDirectionalExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.Directional.Example.tsx') as string; const ContextualMenuCustomizationExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.Customization.Example.tsx') as string; const ContextualMenuHeaderExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.Header.Example.tsx') as string; +const ContextualMenuColorSchemeExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.ColorScheme.Example.tsx') as string; export class ContextualMenuPage extends React.Component { public render() { @@ -40,6 +42,9 @@ export class ContextualMenuPage extends React.Component + + +
} propertiesTables={ diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.ColorScheme.Example.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.ColorScheme.Example.tsx new file mode 100644 index 0000000000000..7004793db1081 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.ColorScheme.Example.tsx @@ -0,0 +1,117 @@ +import * as React from 'react'; +import { Button } from 'office-ui-fabric-react/lib/Button'; +import { ContextualMenu, DirectionalHint, ContextualMenuColorScheme } from 'office-ui-fabric-react/lib/ContextualMenu'; +import { getRTL } from 'office-ui-fabric-react/lib/Utilities'; +import './ContextualMenuExample.scss'; + +export class ContextualMenuColorSchemeExample extends React.Component { + + constructor() { + super(); + this.state = { + isContextMenuVisible: false + }; + this._onClick = this._onClick.bind(this); + this._onDismiss = this._onDismiss.bind(this); + } + + public render() { + + return ( +
+ + { this.state.isContextMenuVisible ? ( + ) : (null) } +
+ ); + } + + private _onClick(event: React.MouseEvent) { + this.setState({ target: event.target, isContextMenuVisible: true }); + } + + private _onDismiss(event: any) { + this.setState({ isContextMenuVisible: false }); + } +} From ffa948df1658c041a66c15e40d327ac3bbe87c2c Mon Sep 17 00:00:00 2001 From: Boris Emorine Date: Thu, 13 Apr 2017 18:29:53 -0700 Subject: [PATCH 13/64] Improvements to SpinButton. --- .../pages/SpinButtonPage/SpinButtonPage.tsx | 19 ++- .../examples/SpinButton.Basic.Example.scss | 3 - .../examples/SpinButton.Basic.Example.tsx | 56 +------- .../examples/SpinButton.Stateful.Example.tsx | 48 +++++++ .../components/SpinButton/SpinButton.Props.ts | 2 +- .../src/components/SpinButton/SpinButton.scss | 76 ++++++++--- .../components/SpinButton/SpinButton.test.tsx | 14 +- .../src/components/SpinButton/SpinButton.tsx | 127 ++++++++---------- 8 files changed, 193 insertions(+), 152 deletions(-) delete mode 100644 apps/fabric-examples/src/pages/SpinButtonPage/examples/SpinButton.Basic.Example.scss create mode 100644 apps/fabric-examples/src/pages/SpinButtonPage/examples/SpinButton.Stateful.Example.tsx diff --git a/apps/fabric-examples/src/pages/SpinButtonPage/SpinButtonPage.tsx b/apps/fabric-examples/src/pages/SpinButtonPage/SpinButtonPage.tsx index 92777b863c8bf..8b9185d231a1b 100644 --- a/apps/fabric-examples/src/pages/SpinButtonPage/SpinButtonPage.tsx +++ b/apps/fabric-examples/src/pages/SpinButtonPage/SpinButtonPage.tsx @@ -6,8 +6,10 @@ import { PropertiesTableSet } from '@uifabric/example-app-base'; import { SpinButtonBasicExample } from './examples/SpinButton.Basic.Example'; +import { SpinButtonStatefulExample } from './examples/SpinButton.Stateful.Example'; const SpinButtonBasicExampleCode = require('./examples/SpinButton.Basic.Example.tsx') as string; +const SpinButtonStatefulExampleCode = require('./examples/SpinButton.Stateful.Example.tsx') as string; export class SpinButtonPage extends React.Component { public render() { @@ -16,11 +18,18 @@ export class SpinButtonPage extends React.Component title='SpinButton' componentName='SpinButtonExample' exampleCards={ - - - +
+ + + + + + +
} propertiesTables={ { - - private hasSuffix(string: string, suffix: string): Boolean { - var subString = string.substr(string.length - suffix.length); - return subString == suffix; - } - - private removeSuffix(string: string, suffix: string): string { - if (!this.hasSuffix(string, suffix)) { - return string; - } - - return string.substr(0, string.length - suffix.length); - } - public render() { - - var suffix = " cm"; - return ( -
- - - - < SpinButton - label='SpinButton with custom implementation' - defaultValue={ '7' + suffix } - onBlur={ (value: string, state: ISpinButtonState, props: ISpinButtonProps) => { - value = this.removeSuffix(value, suffix); - if (isNaN(+value)) { - return '0' + suffix - } - - const newValue = Math.min(100, Math.max(0, +value)); - return String(newValue) + suffix; - } } - onIncrement={ (value: string) => { - value = this.removeSuffix(value, suffix); - return String(+value + 2) + suffix; - } } - onDecrement={ (value: string) => { - value = this.removeSuffix(value, suffix); - return String(+value - 2) + suffix; - } } - /> - -
+ ); } } diff --git a/apps/fabric-examples/src/pages/SpinButtonPage/examples/SpinButton.Stateful.Example.tsx b/apps/fabric-examples/src/pages/SpinButtonPage/examples/SpinButton.Stateful.Example.tsx new file mode 100644 index 0000000000000..28d89acd0c15f --- /dev/null +++ b/apps/fabric-examples/src/pages/SpinButtonPage/examples/SpinButton.Stateful.Example.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import { SpinButton, ISpinButtonState, ISpinButtonProps } from 'office-ui-fabric-react/lib/SpinButton'; +import { Label, assign } from 'office-ui-fabric-react/lib'; +//import { assign } from 'office-ui-fabric-react/lib/Utilities'; + +export class SpinButtonStatefulExample extends React.Component { + + private hasSuffix(string: string, suffix: string): Boolean { + var subString = string.substr(string.length - suffix.length); + return subString == suffix; + } + + private removeSuffix(string: string, suffix: string): string { + if (!this.hasSuffix(string, suffix)) { + return string; + } + + return string.substr(0, string.length - suffix.length); + } + + public render() { + + var suffix = " cm"; + + return ( + < SpinButton + label='SpinButton with custom implementation' + defaultValue={ '7' + suffix } + onBlur={ (value: string) => { + value = this.removeSuffix(value, suffix); + if (isNaN(+value)) { + return '0' + suffix + } + + return String(value) + suffix; + } } + onIncrement={ (value: string) => { + value = this.removeSuffix(value, suffix); + return String(+value + 2) + suffix; + } } + onDecrement={ (value: string) => { + value = this.removeSuffix(value, suffix); + return String(+value - 2) + suffix; + } } + /> + ); + } +} diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts index f9e79caf839d4..eb24e409acd36 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts @@ -68,7 +68,7 @@ export interface ISpinButtonProps { * show a red border and show an error message below the text field. * */ - onBlur?: (value: string, state: ISpinButtonState, props: ISpinButtonProps) => string; + onBlur?: (value: string) => string; onIncrement?: (value: string) => string; diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.scss b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.scss index 5cf3422cb8908..cdd90dff7d9ef 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.scss +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.scss @@ -3,29 +3,66 @@ :global { -.arrowBox, .textField, -.upButton, .downButton, .spinButtonContainer { +.ms-ArrowBox, .ms-SpinButton-Input, +.ms-UpButton, .ms-DownButton, .ms-SpinButtonContainer { outline: none; font-size: 12px; } -.spinButtonContainer { +.ms-Label { + pointer-events: none; +} + +.ms-SpinButtonContainer { width: 100px; - height: 32px; min-width: 100px; + overflow: hidden; } - .arrowBox { + .ms-ArrowBox { display: block; float: left; height: 30px; - border: 1px solid #e1e1e1; + border: 1px solid $ms-color-neutralTertiaryAlt; border-left-width: 0px; cursor: default; padding: 0px; } - .spinButton-input { + .ms-SpinButtonWrapper:hover .ms-ArrowBox, .ms-SpinButtonWrapper:hover .ms-SpinButton-Input { + border-color: $ms-color-neutralSecondaryAlt; + @media screen and (-ms-high-contrast: active) { + border-color: $ms-color-contrastBlackSelected; + } + + @media screen and (-ms-high-contrast: black-on-white) { + border-color: $ms-color-contrastWhiteSelected; + } + } + + .ms-SpinButtonWrapper { + overflow: hidden; + display: inline-block; + } + + .ms-SpinButton-Input:focus + .ms-ArrowBox { + border-color: $ms-color-themePrimary; + } + + .ms-SpinButton-Input:disabled + .ms-ArrowBox { + background-color: $ms-color-neutralLighter; + border-color: $ms-color-neutralLighter; + pointer-events: none; + opacity: 0.3; + cursor: default; + } + + + .ms-SpinButtonWrapper:hover .ms-SpinButton-Input:focus { + border-color: $ms-color-themePrimary; + } + + .ms-SpinButton-Input { @include ms-u-normalize; @include ms-baseFont; border: 1px solid $ms-color-neutralTertiaryAlt; @@ -45,16 +82,12 @@ cursor: text; user-select: text; - &:hover { - border-color: $ms-color-neutralSecondaryAlt; - } + // &:hover { + // border-color: $ms-color-neutralSecondaryAlt; + // } &:focus { border-color: $ms-color-themePrimary; - } - - &:hover, - &:focus { @media screen and (-ms-high-contrast: active) { border-color: $ms-color-contrastBlackSelected; } @@ -68,6 +101,7 @@ background-color: $ms-color-neutralLighter; border-color: $ms-color-neutralLighter; pointer-events: none; + opacity: 0.3; cursor: default; } @@ -76,18 +110,26 @@ } } - .upButton, .downButton { + .ms-SpinButtonWrapper .ms-UpButton, .ms-SpinButtonWrapper .ms-DownButton { display: block; - height: 16px; + height: 15px; width: 26px; //border: 1px solid transparent; padding-top: 0px; background-color: transparent; text-align: center; cursor: default; + + &:hover { + background-color: $ms-color-neutralLight; + } + + &:active { + background-color: $ms-color-themePrimary; + } } - .upButton span, .downButton span { + .ms-UpButton span, .ms-DownButton span { line-height: 6px !important; font-size: 14px; padding-top: 4px; diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.test.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.test.tsx index c559b117cc892..061f3805f3b1c 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.test.tsx +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.test.tsx @@ -71,7 +71,7 @@ describe('SpinButton', () => { // Assert on the input element. const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; - const buttonDOM: Element = renderedDOM.getElementsByClassName('upButton')[0]; + const buttonDOM: Element = renderedDOM.getElementsByClassName('ms-UpButton')[0]; expect(buttonDOM.tagName).to.equal('BUTTON'); @@ -113,7 +113,7 @@ describe('SpinButton', () => { // Assert on the input element. const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; - const buttonDOM: Element = renderedDOM.getElementsByClassName('downButton')[0]; + const buttonDOM: Element = renderedDOM.getElementsByClassName('ms-DownButton')[0]; expect(buttonDOM.tagName).to.equal('BUTTON'); @@ -396,9 +396,9 @@ describe('SpinButton', () => { const exampleDefaultValue: string = '12'; const exampleNewValue: string = '21'; - function validator(newValue: string, state: ISpinButtonState, props: ISpinButtonProps): string { + function validator(newValue: string): string { let numberValue: number = +newValue; - return (!isNaN(numberValue) && numberValue >= props.min && numberValue <= props.max) ? newValue : errorMessage; + return (!isNaN(numberValue) && numberValue >= exampleMinValue && numberValue <= exampleMaxValue) ? newValue : errorMessage; } const renderedDOM: HTMLElement = renderIntoDocument( @@ -430,10 +430,10 @@ describe('SpinButton', () => { const exampleDefaultValue: string = '12'; const exampleNewValue: string = '100'; - function validator(newValue: string, state: ISpinButtonState, props: ISpinButtonProps): string { + function validator(newValue: string): string { let numberValue: number = Number(newValue); - return (!isNaN(numberValue) && numberValue >= props.min && numberValue <= props.max) ? newValue : errorMessage; + return (!isNaN(numberValue) && numberValue >= exampleMinValue && numberValue <= exampleMaxValue) ? newValue : errorMessage; } const renderedDOM: HTMLElement = renderIntoDocument( @@ -478,7 +478,7 @@ describe('SpinButton', () => { // Assert on the input element. const inputDOM: HTMLInputElement = renderedDOM.getElementsByTagName('input')[0]; - const buttonDOM: Element = renderedDOM.getElementsByClassName('upButton')[0]; + const buttonDOM: Element = renderedDOM.getElementsByClassName('ms-UpButton')[0]; expect(buttonDOM.tagName).to.equal('BUTTON'); diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx index 75040a8a7a673..69ece6e91cbe0 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx @@ -35,10 +35,10 @@ export class SpinButton extends BaseComponent string; + private _onBlur?: (value: string) => string; private _onIncrement?: (value: string) => string; private _onDecrement?: (value: string) => string; - private _defaultOnBlur = (value: string, state: ISpinButtonState, props: ISpinButtonProps) => { + private _defaultOnBlur = (value: string) => { if (isNaN(+value)) { return this._lastValidValue; } @@ -103,8 +103,7 @@ export class SpinButton extends BaseComponent - aria-valuemin={ String(this.props.min) } - aria-valuemax={ String(this.props.max) } +
{ label && } - - -
+ { (label && labelDirection == RectangleEdge.bottom) && } ) as React.ReactElement<{}>; } + private _labelDirectionHelper(): any { + let direction: any = {}; + + switch (this.props.labelDirection) { + case RectangleEdge.left: + direction = { float: 'left' }; + break; + case RectangleEdge.right: + direction = { float: 'right' }; + break; + default: + break; + } + + return direction; + } + private _onChange() { /** * A noop input change handler. From 68e4c1f0f5c1a956bd03c711f6779c0fdc95675b Mon Sep 17 00:00:00 2001 From: Boris Emorine Date: Tue, 18 Apr 2017 11:48:22 -0700 Subject: [PATCH 17/64] Fix border. --- .../src/components/SpinButton/SpinButton.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.scss b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.scss index de2bb1bd22268..87baccc0ef07a 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.scss +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.scss @@ -22,11 +22,12 @@ .ms-ArrowBox { display: block; float: left; - height: 30px; + height: 100%; border: 1px solid $ms-color-neutralTertiaryAlt; border-left-width: 0px; cursor: default; padding: 0px; + box-sizing: border-box; } .ms-SpinButtonWrapper:hover .ms-ArrowBox, .ms-SpinButtonWrapper:hover .ms-SpinButton-Input { @@ -43,6 +44,7 @@ .ms-SpinButtonWrapper { overflow: hidden; display: inline-block; + height: 32px; } .ms-SpinButtonWrapper.topBottom { @@ -73,7 +75,7 @@ font-weight: $ms-font-weight-regular; font-size: $ms-font-size-m; color: $ms-color-neutralPrimary; - height: 32px; + height: 100%; @include padding(0, 12px, 0, 12px); outline: 0; text-overflow: ellipsis; From 25cf2cf67abc42f6c217a2f9ded42fa6aaef36e8 Mon Sep 17 00:00:00 2001 From: Boris Emorine Date: Wed, 19 Apr 2017 14:22:09 -0700 Subject: [PATCH 18/64] Add Position enum. --- .../components/SpinButton/SpinButton.Props.ts | 4 ++-- .../src/components/SpinButton/SpinButton.tsx | 18 +++++++++--------- .../src/utilities/positioning.ts | 7 +++++++ 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts index ef72085d1a50d..efb455c99a80f 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import { ISpinButtonState } from './SpinButton'; -import { RectangleEdge } from '../../utilities/positioning' +import { Position } from '../../utilities/positioning' export interface ISpinButtonProps { @@ -69,7 +69,7 @@ export interface ISpinButtonProps { /** * @default: Left */ - labelDirection?: RectangleEdge; + labelPosition?: Position; labelGapSpace?: number; diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx index 9e1d9ed86feda..829937c543702 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx @@ -13,7 +13,7 @@ import { ISpinButtonProps } from './SpinButton.Props'; import './SpinButton.scss'; -import { RectangleEdge } from '../../utilities/positioning' +import { Position } from '../../utilities/positioning' export interface ISpinButtonState { /** @@ -59,7 +59,7 @@ export class SpinButton extends BaseComponent - { (label && labelDirection != RectangleEdge.bottom) && + { (label && labelPosition != Position.bottom) && < Label id={ this._labelId } style={ this._labelDirectionHelper() } htmlFor={ this._inputId }>{ label } } - < div className={ 'ms-SpinButtonWrapper' + ((this.props.labelDirection == RectangleEdge.top || this.props.labelDirection == RectangleEdge.bottom) ? ' topBottom' : '') }> + < div className={ 'ms-SpinButtonWrapper' + ((this.props.labelPosition == Position.top || this.props.labelPosition == Position.bottom) ? ' topBottom' : '') }>
- { (label && labelDirection == RectangleEdge.bottom) && } + { (label && labelPosition == Position.bottom) && } ) as React.ReactElement<{}>; } @@ -200,11 +200,11 @@ export class SpinButton extends BaseComponent Date: Wed, 19 Apr 2017 14:38:48 -0700 Subject: [PATCH 19/64] `defaultValue` is now the deciding prop for using the default implementation or not. --- .../src/components/SpinButton/SpinButton.tsx | 19 +++++-------------- .../examples/SpinButton.Basic.Example.tsx | 1 + .../examples/SpinButton.Stateful.Example.tsx | 2 +- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx index 829937c543702..809ba363d9e73 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx @@ -58,7 +58,6 @@ export class SpinButton extends BaseComponent { public render() { return ( { < SpinButton width={ '120px' } label='SpinButton with custom implementation:' - defaultValue={ '7' + suffix } + value={ '7' + suffix } onBlur={ (value: string) => { value = this.removeSuffix(value, suffix); if (isNaN(+value)) { From 9241663595f692397196b63623416dd37c06fed9 Mon Sep 17 00:00:00 2001 From: Boris Emorine Date: Wed, 19 Apr 2017 14:47:24 -0700 Subject: [PATCH 20/64] onBlur is now onValidate. --- .../components/SpinButton/SpinButton.Props.ts | 9 ++------- .../src/components/SpinButton/SpinButton.tsx | 18 +++++++++--------- .../examples/SpinButton.Stateful.Example.tsx | 2 +- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts index efb455c99a80f..61608286bf99a 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts @@ -74,15 +74,10 @@ export interface ISpinButtonProps { labelGapSpace?: number; /** - * The method is used to get the validation error message and determine whether the input value is valid or not. - * - * When it returns string: - * - If valid, it returns empty string. - * - If invalid, it returns the error message string and the text field will - * show a red border and show an error message below the text field. + * The method is triggered when the value inside the SpinButton should be validated. * */ - onBlur?: (value: string) => string; + onValidate?: (value: string) => string; /** * TODO diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx index 809ba363d9e73..b0806a74b7548 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx @@ -34,10 +34,10 @@ export class SpinButton extends BaseComponent string; + private _onValidate?: (value: string) => string; private _onIncrement?: (value: string) => string; private _onDecrement?: (value: string) => string; - private _defaultOnBlur = (value: string) => { + private _defaultOnValidate = (value: string) => { if (isNaN(+value)) { return this._lastValidValue; } @@ -82,11 +82,11 @@ export class SpinButton extends BaseComponent) { + private _validate(event: React.FocusEvent) { const element: HTMLInputElement = event.target as HTMLInputElement; const value: string = element.value; if (this.state.value) { - var newValue = this._onBlur(value); + var newValue = this._onValidate(value); this._lastValidValue = newValue; this.setState({ value: newValue }); } diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.Stateful.Example.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.Stateful.Example.tsx index 088694a91b93e..cfd63799e6c10 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.Stateful.Example.tsx +++ b/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.Stateful.Example.tsx @@ -25,7 +25,7 @@ export class SpinButtonStatefulExample extends React.Component { width={ '120px' } label='SpinButton with custom implementation:' value={ '7' + suffix } - onBlur={ (value: string) => { + onValidate={ (value: string) => { value = this.removeSuffix(value, suffix); if (isNaN(+value)) { return '0' + suffix From 21453e34a92a14320df6d279e810538b19900193 Mon Sep 17 00:00:00 2001 From: Boris Emorine Date: Wed, 19 Apr 2017 17:32:09 -0700 Subject: [PATCH 21/64] Fix tests. --- .../src/components/SpinButton/SpinButton.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.test.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.test.tsx index e0659cb9340d9..0063412ef784f 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.test.tsx +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.test.tsx @@ -407,7 +407,7 @@ describe('SpinButton', () => { min={ exampleMinValue } max={ exampleMaxValue } defaultValue={ exampleDefaultValue } - onBlur={ validator } + onValidate={ validator } /> ); @@ -442,7 +442,7 @@ describe('SpinButton', () => { min={ exampleMinValue } max={ exampleMaxValue } defaultValue={ exampleDefaultValue } - onBlur={ validator } + onValidate={ validator } /> ); From 629d21db5c026696301b520db8a04cbb1d6870d3 Mon Sep 17 00:00:00 2001 From: Boris Emorine Date: Thu, 20 Apr 2017 12:54:43 -0700 Subject: [PATCH 22/64] Fix warnings. --- .../components/SpinButton/SpinButton.Props.ts | 6 +- .../components/SpinButton/SpinButton.test.tsx | 6 +- .../src/components/SpinButton/SpinButton.tsx | 108 +++++++++--------- .../examples/SpinButton.Stateful.Example.tsx | 31 +++-- 4 files changed, 70 insertions(+), 81 deletions(-) diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts index 61608286bf99a..29ba06f38ba9a 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts @@ -1,6 +1,4 @@ -import * as React from 'react'; -import { ISpinButtonState } from './SpinButton'; -import { Position } from '../../utilities/positioning' +import { Position } from '../../utilities/positioning'; export interface ISpinButtonProps { @@ -91,5 +89,5 @@ export interface ISpinButtonProps { } export interface ISpinButton { - value?: number + value?: number; } \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.test.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.test.tsx index 0063412ef784f..e3063a0523c47 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.test.tsx +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.test.tsx @@ -2,11 +2,7 @@ import 'es6-promise'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import * as ReactTestUtils from 'react-addons-test-utils'; -import { - SpinButton, - ISpinButtonState -} from './SpinButton'; -import { ISpinButtonProps } from './SpinButton.Props'; +import { SpinButton } from './SpinButton'; import { KeyCodes } from '../../Utilities'; const expect: Chai.ExpectStatic = chai.expect; diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx index b0806a74b7548..dedad1760de3d 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx @@ -3,17 +3,15 @@ import { IconButton } from '../../Button'; import { Label } from '../../Label'; import { BaseComponent, - css, getId, - KeyCodes, - assign + KeyCodes } from '../../Utilities'; import { ISpinButton, ISpinButtonProps } from './SpinButton.Props'; import './SpinButton.scss'; -import { Position } from '../../utilities/positioning' +import { Position } from '../../utilities/positioning'; export interface ISpinButtonState { /** @@ -29,29 +27,6 @@ export interface ISpinButtonState { } export class SpinButton extends BaseComponent implements ISpinButton { - private _input: HTMLInputElement; - private _inputId: string; - private _labelId: string; - private _lastValidValue: string; - - private _onValidate?: (value: string) => string; - private _onIncrement?: (value: string) => string; - private _onDecrement?: (value: string) => string; - private _defaultOnValidate = (value: string) => { - if (isNaN(+value)) { - return this._lastValidValue; - } - const newValue = Math.min(this.props.max, Math.max(this.props.min, +value)); - return String(newValue); - } - private _defaultOnIncrement = (value: string) => { - let newValue = Math.min(+value + this.props.step, this.props.max); - return String(newValue); - }; - private _defaultOnDecrement = (value: string) => { - let newValue = Math.max(+value - this.props.step, this.props.min); - return String(newValue); - }; public static defaultProps: ISpinButtonProps = { step: 1, @@ -62,9 +37,17 @@ export class SpinButton extends BaseComponent string; + private _onIncrement?: (value: string) => string; + private _onDecrement?: (value: string) => string; + private _currentStepFunctionHandle: number; private _stepDelay = 100; - private _formattedValidUnitOptions: string[] = []; constructor(props?: ISpinButtonProps) { @@ -132,13 +115,13 @@ export class SpinButton extends BaseComponent - { (label && labelPosition != Position.bottom) && + { (label && labelPosition !== Position.bottom) && < Label id={ this._labelId } style={ this._labelDirectionHelper() } htmlFor={ this._inputId }>{ label } } - < div className={ 'ms-SpinButtonWrapper' + ((this.props.labelPosition == Position.top || this.props.labelPosition == Position.bottom) ? ' topBottom' : '') }> + < div className={ 'ms-SpinButtonWrapper' + ((this.props.labelPosition === Position.top || this.props.labelPosition === Position.bottom) ? ' topBottom' : '') }> { this._updateValue(true /* shouldSpin */, this._onIncrement) } } + onMouseDown={ () => { this._updateValue(true /* shouldSpin */, this._onIncrement); } } onMouseLeave={ this._stop } onMouseUp={ this._stop } onBlur={ this._stop } @@ -175,7 +158,7 @@ export class SpinButton extends BaseComponent
- { (label && labelPosition == Position.bottom) && } + { (label && labelPosition === Position.bottom) && } ) as React.ReactElement<{}>; } + /** + * OnFocus select the contents of the input + */ + public focus() { + if (this.state.spinning) { + this._stop(); + } + + this._input.focus(); + this._input.select(); + } + + private _defaultOnValidate = (value: string) => { + if (isNaN(+value)) { + return this._lastValidValue; + } + const newValue = Math.min(this.props.max, Math.max(this.props.min, +value)); + return String(newValue); + } + private _defaultOnIncrement = (value: string) => { + let newValue = Math.min(+value + this.props.step, this.props.max); + return String(newValue); + } + private _defaultOnDecrement = (value: string) => { + let newValue = Math.max(+value - this.props.step, this.props.min); + return String(newValue); + } + private _labelDirectionHelper(): any { let direction: any = {}; @@ -215,18 +226,6 @@ export class SpinButton extends BaseComponent string) { - var newValue = stepFunction(this.state.value); + const newValue = stepFunction(this.state.value); this._lastValidValue = newValue; this.setState({ value: newValue }); - if (this.state.spinning != shouldSpin) { + if (this.state.spinning !== shouldSpin) { this.setState({ spinning: shouldSpin }); } if (shouldSpin) { - this._currentStepFunctionHandle = window.setTimeout(() => { this._updateValue(shouldSpin, stepFunction); }, this._stepDelay) + this._currentStepFunctionHandle = window.setTimeout(() => { this._updateValue(shouldSpin, stepFunction); }, this._stepDelay); } } @@ -270,9 +269,9 @@ export class SpinButton extends BaseComponent { - private hasSuffix(string: string, suffix: string): Boolean { - var subString = string.substr(string.length - suffix.length); - return subString == suffix; - } - - private removeSuffix(string: string, suffix: string): string { - if (!this.hasSuffix(string, suffix)) { - return string; - } - - return string.substr(0, string.length - suffix.length); - } - public render() { - - var suffix = " cm"; + let suffix = ' cm'; return ( < SpinButton @@ -28,7 +14,7 @@ export class SpinButtonStatefulExample extends React.Component { onValidate={ (value: string) => { value = this.removeSuffix(value, suffix); if (isNaN(+value)) { - return '0' + suffix + return '0' + suffix; } return String(value) + suffix; @@ -44,4 +30,17 @@ export class SpinButtonStatefulExample extends React.Component { /> ); } + + private hasSuffix(string: string, suffix: string): Boolean { + let subString = string.substr(string.length - suffix.length); + return subString === suffix; + } + + private removeSuffix(string: string, suffix: string): string { + if (!this.hasSuffix(string, suffix)) { + return string; + } + + return string.substr(0, string.length - suffix.length); + } } From 8475fc40a6be795ca44b73210283df4d963bc11e Mon Sep 17 00:00:00 2001 From: Boris Emorine Date: Thu, 20 Apr 2017 17:27:31 -0700 Subject: [PATCH 23/64] Add implementation for labelGap. --- .../components/SpinButton/SpinButton.Props.ts | 5 +++++ .../src/components/SpinButton/SpinButton.tsx | 21 +++++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts index 29ba06f38ba9a..24b715499443d 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts @@ -69,8 +69,13 @@ export interface ISpinButtonProps { */ labelPosition?: Position; + /** + * @default: 10 + */ labelGapSpace?: number; + icon?: string; + /** * The method is triggered when the value inside the SpinButton should be validated. * diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx index dedad1760de3d..dcee83f07ef41 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx @@ -34,6 +34,7 @@ export class SpinButton extends BaseComponent
- { (label && labelPosition === Position.bottom) && } + { (label && labelPosition === Position.bottom) && + } ) as React.ReactElement<{}>; } @@ -200,20 +206,23 @@ export class SpinButton extends BaseComponent Date: Fri, 21 Apr 2017 17:34:51 -0700 Subject: [PATCH 24/64] Put some polish on the styling, added some icon support, and added some more example spinButtons --- .../components/SpinButton/SpinButton.Props.ts | 20 ++++-- .../src/components/SpinButton/SpinButton.scss | 29 ++++---- .../src/components/SpinButton/SpinButton.tsx | 68 ++++++++++++------- .../components/SpinButton/SpinButtonPage.tsx | 21 ++++++ .../examples/SpinButton.Basic.Example.tsx | 2 +- .../SpinButton.BasicDisabled.Example.tsx | 18 +++++ ...pinButton.BasicWithEndPosition.Example.tsx | 20 ++++++ .../SpinButton.BasicWithIcon.Example.tsx | 18 +++++ 8 files changed, 156 insertions(+), 40 deletions(-) create mode 100644 packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.BasicDisabled.Example.tsx create mode 100644 packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.BasicWithEndPosition.Example.tsx create mode 100644 packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.BasicWithIcon.Example.tsx diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts index 24b715499443d..6afc2247f7b4c 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts @@ -74,23 +74,35 @@ export interface ISpinButtonProps { */ labelGapSpace?: number; + /** + * Icon that goes along with the label for the whole splitButton + */ icon?: string; /** - * The method is triggered when the value inside the SpinButton should be validated. - * + * This callback is triggered when the value inside the SpinButton should be validated. */ onValidate?: (value: string) => string; /** - * TODO + * This callback is triggered when the increment button or if the user presses up arrow with focus on the input of the spinButton */ onIncrement?: (value: string) => string; /** - * TODO + * This callback is triggered when the decrement button or if the user presses down arrow with focus on the input of the spinButton */ onDecrement?: (value: string) => string; + + /** + * Icon for the increment button of the spinButton + */ + incrementButtonIcon?: string; + + /** + * Icon for the decrement button of the spinButton + */ + decrementButtonIcon?: string; } export interface ISpinButton { diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.scss b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.scss index 87baccc0ef07a..eed3caa919f6d 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.scss +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.scss @@ -41,10 +41,20 @@ } } + .ms-labelWrapper { + display: inline-flex; + } + + .ms-SpinButtonIcon { + padding: 5px; + font-size: 20px; + } + .ms-SpinButtonWrapper { overflow: hidden; - display: inline-block; + display: flex; height: 32px; + min-width: 90px; } .ms-SpinButtonWrapper.topBottom { @@ -81,17 +91,13 @@ text-overflow: ellipsis; display: block; float: left; - width: 70%; + width: calc(100% - 27px); min-width: 63px; border-right-width: 0px; overflow: hidden; cursor: text; user-select: text; - // &:hover { - // border-color: $ms-color-neutralSecondaryAlt; - // } - &:focus { border-color: $ms-color-themePrimary; @media screen and (-ms-high-contrast: active) { @@ -120,11 +126,11 @@ display: block; height: 15px; width: 26px; - //border: 1px solid transparent; padding-top: 0px; background-color: transparent; text-align: center; cursor: default; + font-size: 14px; &:hover { background-color: $ms-color-neutralLight; @@ -132,14 +138,13 @@ &:active { background-color: $ms-color-themePrimary; + color: $ms-color-white; } } - .ms-UpButton span, .ms-DownButton span { - line-height: 6px !important; - font-size: 14px; - padding-top: 4px; - margin: 0px; + .ms-SpinButtonWrapper .ms-UpButton.active, .ms-SpinButtonWrapper .ms-DownButton.active { + background-color: $ms-color-themePrimary; + color: $ms-color-white; } } \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx index dcee83f07ef41..c5e4f2e2ad47a 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx @@ -35,7 +35,9 @@ export class SpinButton extends BaseComponent - { (label && labelPosition !== Position.bottom) && - < Label - id={ this._labelId } - style={ this._labelDirectionHelper() } - htmlFor={ this._inputId }>{ label } - } - < div className={ 'ms-SpinButtonWrapper' + ((this.props.labelPosition === Position.top || this.props.labelPosition === Position.bottom) ? ' topBottom' : '') }> + { labelPosition !== Position.bottom &&
+ { icon && } + { label && + < Label + id={ this._labelId } + //style={ this._labelDirectionHelper() } + htmlFor={ this._inputId }>{ label } + } +
} +
- { (label && labelPosition === Position.bottom) && - } + { labelPosition === Position.bottom &&
+ { icon && } + { label && + } +
} ) as React.ReactElement<{}>; } @@ -210,16 +225,16 @@ export class SpinButton extends BaseComponent) { if (this.props.disabled) { this._stop(); + + // eat the up and down arrow keys to keep the page from scrolling + if (event.which === KeyCodes.up || event.which === KeyCodes.down) { + event.preventDefault(); + event.stopPropagation(); + } + return; } diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButtonPage.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButtonPage.tsx index 8b9185d231a1b..8f637292fce69 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButtonPage.tsx +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButtonPage.tsx @@ -6,10 +6,16 @@ import { PropertiesTableSet } from '@uifabric/example-app-base'; import { SpinButtonBasicExample } from './examples/SpinButton.Basic.Example'; +import { SpinButtonBasicDisabledExample } from './examples/SpinButton.BasicDisabled.Example'; import { SpinButtonStatefulExample } from './examples/SpinButton.Stateful.Example'; +import { SpinButtonBasicWithIconExample } from './examples/SpinButton.BasicWithIcon.Example'; +import { SpinButtonBasicWithEndPositionExample } from './examples/SpinButton.BasicWithEndPosition.Example'; const SpinButtonBasicExampleCode = require('./examples/SpinButton.Basic.Example.tsx') as string; +const SpinButtonBasicDisabledExampleCode = require('./examples/SpinButton.BasicDisabled.Example.tsx') as string; const SpinButtonStatefulExampleCode = require('./examples/SpinButton.Stateful.Example.tsx') as string; +const SpinButtonBasicWithIconExampleCode = require('./examples/SpinButton.BasicWithIcon.Example.tsx') as string; +const SpinButtonBasicWithEndPositionExampleCode = require('./examples/SpinButton.BasicWithEndPosition.Example.tsx') as string; export class SpinButtonPage extends React.Component { public render() { @@ -24,11 +30,26 @@ export class SpinButtonPage extends React.Component code={ SpinButtonBasicExampleCode }> + + + + + + + + +
} propertiesTables={ diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.Basic.Example.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.Basic.Example.tsx index 1e2d753d8b290..e62928eb96a90 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.Basic.Example.tsx +++ b/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.Basic.Example.tsx @@ -6,7 +6,7 @@ export class SpinButtonBasicExample extends React.Component { return ( { + public render() { + return ( + + ); + } +} diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.BasicWithEndPosition.Example.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.BasicWithEndPosition.Example.tsx new file mode 100644 index 0000000000000..b1bdb2c9a721e --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.BasicWithEndPosition.Example.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { SpinButton } from 'office-ui-fabric-react/lib/SpinButton'; +import { Position } from 'office-ui-fabric-react/lib/utilities/positioning'; + +export class SpinButtonBasicWithEndPositionExample extends React.Component { + public render() { + return ( + + ); + } +} diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.BasicWithIcon.Example.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.BasicWithIcon.Example.tsx new file mode 100644 index 0000000000000..aeff8aa6e3aa8 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.BasicWithIcon.Example.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { SpinButton } from 'office-ui-fabric-react/lib/SpinButton'; + +export class SpinButtonBasicWithIconExample extends React.Component { + public render() { + return ( + + ); + } +} From fdaadbd30ac083f223246181a4e01d76c350aeef Mon Sep 17 00:00:00 2001 From: Derek Sun Date: Fri, 21 Apr 2017 17:51:36 -0700 Subject: [PATCH 25/64] Implement the bar and unit tests and component page --- .../src/components/App/AppState.tsx | 6 + .../DocumentTitleBarComponentPage.tsx | 29 +++ .../src/DocumentTitleBar.ts | 1 + .../DocumentTitleBar.Props.ts | 45 ++++ .../DocumentTitleBar/DocumentTitleBar.scss | 54 ++++ .../DocumentTitleBar.test.tsx | 231 ++++++++++++++++++ .../DocumentTitleBar/DocumentTitleBar.tsx | 180 ++++++++++++++ .../DocumentTitleBar/DocumentTitleBarPage.tsx | 64 +++++ .../DocumentTitleBar.Basic.Example.tsx | 38 +++ .../src/components/DocumentTitleBar/index.ts | 2 + .../src/demo/AppDefinition.tsx | 6 + packages/office-ui-fabric-react/src/index.ts | 1 + 12 files changed, 657 insertions(+) create mode 100644 apps/fabric-website/src/pages/Components/DocumentTitleBarComponentPage.tsx create mode 100644 packages/office-ui-fabric-react/src/DocumentTitleBar.ts create mode 100644 packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.Props.ts create mode 100644 packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.scss create mode 100644 packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.test.tsx create mode 100644 packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.tsx create mode 100644 packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBarPage.tsx create mode 100644 packages/office-ui-fabric-react/src/components/DocumentTitleBar/examples/DocumentTitleBar.Basic.Example.tsx create mode 100644 packages/office-ui-fabric-react/src/components/DocumentTitleBar/index.ts diff --git a/apps/fabric-website/src/components/App/AppState.tsx b/apps/fabric-website/src/components/App/AppState.tsx index 6b589a7872b9c..ae0f0ed2dc752 100644 --- a/apps/fabric-website/src/components/App/AppState.tsx +++ b/apps/fabric-website/src/components/App/AppState.tsx @@ -166,6 +166,12 @@ export const AppState: IAppState = { component: () => , getComponent: cb => require.ensure([], (require) => cb(require('../../pages/Components/DocumentCardComponentPage').DocumentCardComponentPage)) }, + { + title: 'DocumentTitleBar', + url: '#/components/documenttitlebar', + component: () => , + getComponent: cb => require.ensure([], (require) => cb(require('../../pages/Components/DocumentTitleBarComponentPage').DocumentTitleBarComponentPage)) + }, { title: 'Dropdown', url: '#/components/dropdown', diff --git a/apps/fabric-website/src/pages/Components/DocumentTitleBarComponentPage.tsx b/apps/fabric-website/src/pages/Components/DocumentTitleBarComponentPage.tsx new file mode 100644 index 0000000000000..88cfd50fd0c76 --- /dev/null +++ b/apps/fabric-website/src/pages/Components/DocumentTitleBarComponentPage.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { DocumentTitleBarPage } from 'office-ui-fabric-react/lib/components/DocumentTitleBar/DocumentTitleBarPage'; +import { PageHeader } from '../../components/PageHeader/PageHeader'; +import { ComponentPage } from '../../components/ComponentPage/ComponentPage'; + +export class DocumentTitleBarComponentPage extends React.Component { + public render() { + return ( +
+ + + + +
+ ); + } +} diff --git a/packages/office-ui-fabric-react/src/DocumentTitleBar.ts b/packages/office-ui-fabric-react/src/DocumentTitleBar.ts new file mode 100644 index 0000000000000..c9eb2a677ca3f --- /dev/null +++ b/packages/office-ui-fabric-react/src/DocumentTitleBar.ts @@ -0,0 +1 @@ +export * from './components/DocumentTitleBar/index'; diff --git a/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.Props.ts b/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.Props.ts new file mode 100644 index 0000000000000..daef0a3171d23 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.Props.ts @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { DocumentTitleBar } from './DocumentTitleBar'; + +export interface IDocumentTitleBarProps extends React.Props { + /** + * Additional className to append to the root Document Title Bar element. + */ + className?: string; + + /** + * The document's title. + */ + title: string; + + /** + * Optional text to indicate the status of the current document (e.g. Saved) + */ + statusText?: string; + + /** + * File path of the document's saved location. + */ + filePath: string; + + /** + * Whether or not the current document has older versions. + * If set to false, the contextual menu will not show the Versions menu item. + */ + hasVersions: boolean; + + /** + * Callback that is invoked when the user renames the document. + */ + onRenameDocument?: (newName: string) => void; + + /** + * Callback that is invoked when the user clicks the Saved Location menu item. + */ + onClickSavedLocationMenuItem?: (event: React.MouseEvent) => void; + + /** + * Callback that is invoked when the user clicks the Versions menu item. + */ + onClickVersionsMenuItem?: (event: React.MouseEvent) => void; +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.scss b/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.scss new file mode 100644 index 0000000000000..120bfafda8b64 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.scss @@ -0,0 +1,54 @@ +@import '../../common/common'; + +:global { + .ms-DocumentTitleBar-button { + background-color: $ms-color-themeDark; + color: white; + display: inline-block; + font-size: 20px; + padding: 10px 15px 10px 15px; + vertical-align: middle; + border-radius: 3px; + border: 0px; + cursor: default; + + &:hover { + background-color: $ms-color-themeDarkAlt; + cursor: pointer; + } + } + + .isHighlighted { + background-color: $ms-color-themeDarkAlt; + } + + .ms-DocumentTitleBar-button .ms-Icon { + color: rgba(255, 255, 255, .4); + padding-left: 10px; + display: inline; + vertical-align: middle; + font-size: 11px; + } + + .ms-DocumentTitleBar-button .statusText { + user-select: none; + color: rgba(255, 255, 255, .4); + font-size: 13px; + } + + .ms-DocumentTitleBar-renameDiv { + background-color: $ms-color-themeDark; + display: inline-block; + border-radius: 3px; + } + + .ms-DocumentTitleBar-renameDiv .textField { + font-size: 20px; + padding: 10px 15px 10px 15px; + vertical-align: middle; + background-color: rgba(0, 0, 0, .4); + color: white; + border: 0px; + text-align: center; + } +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.test.tsx b/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.test.tsx new file mode 100644 index 0000000000000..f999b06f05906 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.test.tsx @@ -0,0 +1,231 @@ +/* tslint:disable:no-unused-variable */ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +/* tslint:enable:no-unused-variable */ + +import * as ReactTestUtils from 'react-addons-test-utils'; +import { KeyCodes } from '../../Utilities'; +import { DocumentTitleBar } from './DocumentTitleBar'; + +const { expect } = chai; +const DocumentTitleBarButtonClassSelector = '.ms-DocumentTitleBar-button'; +const DocumentTitleBarStatusTextClassSelector = '.statusText'; +const MenuItemIndexRename = 0; +const MenuItemIndexSavedLocation = 3; +const MenuItemIndexVersions = 5; + +describe('DocumentTitleBar', () => { + + afterEach(() => { + for (let i = 0; i < document.body.children.length; i++) { + if (document.body.children[i].tagName === 'DIV') { + document.body.removeChild(document.body.children[i]); + i--; + } + } + }); + + it('should display title in the bar', () => { + const container = document.createElement('div'); + + ReactDOM.render( + , + container + ); + + const docTitleBarButton = container.querySelector(DocumentTitleBarButtonClassSelector); + const statusTextElement = docTitleBarButton.querySelector(DocumentTitleBarStatusTextClassSelector); + const titleNode = docTitleBarButton.childNodes[1]; + + expect(titleNode.nodeValue).to.equal('hello'); + expect(statusTextElement).to.not.exist; + }); + + it('should display title and status text in the bar', () => { + const container = document.createElement('div'); + + ReactDOM.render( + , + container + ); + + const docTitleBarButton = container.querySelector(DocumentTitleBarButtonClassSelector); + const titleNode = docTitleBarButton.childNodes[1]; + const statusTextElement = docTitleBarButton.querySelector(DocumentTitleBarStatusTextClassSelector); + const statusTextNode = statusTextElement.childNodes[4]; + + expect(titleNode.nodeValue).to.equal('hello'); + expect(statusTextNode.nodeValue).to.equal('world'); + }); + + it('should populate contextual menu items correctly', () => { + const container = document.createElement('div'); + + ReactDOM.render( + , + container + ); + + // Open the contextual menu by clicking the bar + const docTitleBarButton = container.querySelector(DocumentTitleBarButtonClassSelector); + ReactTestUtils.Simulate.click(docTitleBarButton); + const contextualMenuList = document.querySelector('.ms-ContextualMenu-list'); + expect(contextualMenuList).to.exist; + + const getMenuItemNameForIndex = (index: number) => { + return contextualMenuList.childNodes[index].firstChild.attributes.getNamedItem('name').value; + }; + + // Verify each menu item + expect(getMenuItemNameForIndex(MenuItemIndexRename)).to.equal('Rename'); + expect(getMenuItemNameForIndex(MenuItemIndexSavedLocation)).to.equal('OneDrive > Documents'); + expect(getMenuItemNameForIndex(MenuItemIndexVersions)).to.equal('Versions'); + }); + + it('should populate current title text in the Rename text field and rename properly', () => { + const container = document.createElement('div'); + const initialDocumentName = 'Document'; + const newDocumentName = 'Nice'; + let newDocumentNameToTest; + + const onRename = (newName: string) => { + newDocumentNameToTest = newName; + }; + + ReactDOM.render( + , + container + ); + + // Open the contextual menu by clicking the bar + const docTitleBarButton = container.querySelector(DocumentTitleBarButtonClassSelector); + ReactTestUtils.Simulate.click(docTitleBarButton); + const contextualMenuList = document.querySelector('.ms-ContextualMenu-list'); + expect(contextualMenuList).to.exist; + + // Find the Rename menu item and click it + const renameItem = contextualMenuList.childNodes[MenuItemIndexRename].firstChild as HTMLButtonElement; + ReactTestUtils.Simulate.click(renameItem); + const docTitleRenameTextField = container.querySelector('.ms-DocumentTitleBar-renameDiv .textField') as HTMLInputElement; + + // Verify that the input field prepopulates the current document title + expect(docTitleRenameTextField.value).to.equal(initialDocumentName); + + // Now try to rename + docTitleRenameTextField.value = newDocumentName; + ReactTestUtils.Simulate.blur(docTitleRenameTextField); + expect(newDocumentNameToTest).to.equal(newDocumentName); + }); + + it('should invoke Saved Location callback', () => { + const container = document.createElement('div'); + let hasInvokedSavedLocationCallback = false; + + const onClickSavedLocation = () => { + hasInvokedSavedLocationCallback = true; + }; + + ReactDOM.render( + , + container + ); + + // Open the contextual menu by clicking the bar + const docTitleBarButton = container.querySelector(DocumentTitleBarButtonClassSelector); + ReactTestUtils.Simulate.click(docTitleBarButton); + const contextualMenuList = document.querySelector('.ms-ContextualMenu-list'); + expect(contextualMenuList).to.exist; + + // Verify clicking Saved Location + const savedLocationItem = contextualMenuList.childNodes[MenuItemIndexSavedLocation].firstChild as HTMLButtonElement; + ReactTestUtils.Simulate.click(savedLocationItem); + expect(hasInvokedSavedLocationCallback).is.true; + }); + + it('should invoke Versions callback', () => { + const container = document.createElement('div'); + let hasInvokedVersionsCallback = false; + + const onClickVersions = () => { + hasInvokedVersionsCallback = true; + }; + + ReactDOM.render( + , + container + ); + + // Open the contextual menu by clicking the bar + const docTitleBarButton = container.querySelector(DocumentTitleBarButtonClassSelector); + ReactTestUtils.Simulate.click(docTitleBarButton); + const contextualMenuList = document.querySelector('.ms-ContextualMenu-list'); + expect(contextualMenuList).to.exist; + + // Verify clicking Versions + const versionsItem = contextualMenuList.childNodes[MenuItemIndexVersions].firstChild as HTMLButtonElement; + ReactTestUtils.Simulate.click(versionsItem); + expect(hasInvokedVersionsCallback).is.true; + }); + + it('should not rename if the user presses the Escape key', () => { + const container = document.createElement('div'); + let isRenameCallbackInvoked = false; + + const onRename = (newName: string) => { + isRenameCallbackInvoked = true; + }; + + ReactDOM.render( + , + container + ); + + // Open the contextual menu by clicking the bar + const docTitleBarButton = container.querySelector(DocumentTitleBarButtonClassSelector); + ReactTestUtils.Simulate.click(docTitleBarButton); + const contextualMenuList = document.querySelector('.ms-ContextualMenu-list'); + expect(contextualMenuList).to.exist; + + // Find the Rename menu item and click it + const renameItem = contextualMenuList.childNodes[MenuItemIndexRename].firstChild as HTMLButtonElement; + ReactTestUtils.Simulate.click(renameItem); + const docTitleRenameTextField = container.querySelector('.ms-DocumentTitleBar-renameDiv .textField') as HTMLInputElement; + + // Cancel out Rename by pressing the Escape key + ReactTestUtils.Simulate.keyDown(docTitleRenameTextField, { which: KeyCodes.escape }); + expect(isRenameCallbackInvoked).is.false; + }); + +}); \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.tsx b/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.tsx new file mode 100644 index 0000000000000..fc4ad45af5b6b --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.tsx @@ -0,0 +1,180 @@ +import * as React from 'react'; +import { BaseComponent, KeyCodes } from '../../Utilities'; +import { + ContextualMenu, + ContextualMenuItemType, + IContextualMenuItem +} from '../../ContextualMenu'; +import { DirectionalHint } from '../../common/DirectionalHint'; +import { Icon } from '../../Icon'; +import { IDocumentTitleBarProps } from './DocumentTitleBar.Props'; +import './DocumentTitleBar.scss'; + +const documentTitleButtonClassName = 'ms-DocumentTitleBar-button'; + +export interface IDocumentTitleBarState { + isContextualMenuVisible: boolean; + isRenaming: boolean; +} + +export class DocumentTitleBar extends BaseComponent { + private _renameTextField: HTMLInputElement; + + constructor(props: IDocumentTitleBarProps) { + super(props); + + this._onClickTitleButton = this._onClickTitleButton.bind(this); + this._onDismissContextualMenu = this._onDismissContextualMenu.bind(this); + this._onClickRenameMenuItem = this._onClickRenameMenuItem.bind(this); + this._onClickSavedLocationMenuItem = this._onClickSavedLocationMenuItem.bind(this); + this._onClickVersionsMenuItem = this._onClickVersionsMenuItem.bind(this); + this._onBlurTitleTextField = this._onBlurTitleTextField.bind(this); + this._onKeyDown = this._onKeyDown.bind(this); + + this.state = { + isContextualMenuVisible: false, + isRenaming: false, + }; + } + + public render() { + const { className, hasVersions, filePath } = this.props; + return ( +
+ { this.state.isRenaming ? this._renderEditableTextField() : this._renderClickableButton() } + { this.state.isContextualMenuVisible && + + } +
+ ); + } + + private _onDismissContextualMenu() { + this.setState({ + isContextualMenuVisible: !this.state.isContextualMenuVisible + }); + } + + private _onClickTitleButton() { + this.setState({ + isContextualMenuVisible: !this.state.isContextualMenuVisible + }); + } + + private _onKeyDown(event: React.KeyboardEvent) { + if (this.state.isRenaming) { + switch (event.which) { + case KeyCodes.enter: + if (this.props.onRenameDocument !== undefined) { + this.props.onRenameDocument(this._renameTextField.value); + } + this.setState({ isRenaming: false }); + break; + case KeyCodes.escape: + this.setState({ isRenaming: false }); + break; + } + } + } + + private _onClickRenameMenuItem() { + this.setState({ + isRenaming: true + }, () => this._renameTextField.select()); + } + + private _onClickSavedLocationMenuItem(event: React.MouseEvent) { + if (this.props.onClickSavedLocationMenuItem !== undefined) { + this.props.onClickSavedLocationMenuItem(event); + } + } + + private _onClickVersionsMenuItem(event: React.MouseEvent) { + if (this.props.onClickVersionsMenuItem !== undefined) { + this.props.onClickVersionsMenuItem(event); + } + } + + private _onBlurTitleTextField(event: React.FocusEvent) { + if (this.props.onRenameDocument !== undefined) { + this.props.onRenameDocument((event.target as HTMLInputElement).value); + } + this.setState({ isRenaming: false }); + } + + private _renderClickableButton(): React.ReactNode { + const { title, statusText } = this.props; + return ( + + ); + } + + private _renderEditableTextField(): React.ReactNode { + return ( +
+ this._renameTextField = textField } /> +
+ ); + } + + private _getVersionsMenuItems(): IContextualMenuItem[] { + return [ + { + key: '-', + name: '-' + }, + { + key: 'Versions', + name: 'Versions', + iconProps: { + iconName: 'History' + }, + onClick: this._onClickVersionsMenuItem + } + ]; + } +} + +export default DocumentTitleBar; diff --git a/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBarPage.tsx b/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBarPage.tsx new file mode 100644 index 0000000000000..546503ffabb4a --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBarPage.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import { + ExampleCard, + IComponentDemoPageProps, + ComponentPage, + PropertiesTableSet +} from '@uifabric/example-app-base'; +import { DocumentTitleBarExample } from './examples/DocumentTitleBar.Basic.Example'; + +const DocumentTitleBarExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/DocumentTitleBar/examples/DocumentTitleBar.Basic.Example.tsx') as string; + +export class DocumentTitleBarPage extends React.Component { + public render() { + return ( + + + + +
+ } + propertiesTables={ + ('!raw-loader!office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.Props.ts') + ] } + /> + } + overview={ +
+

+ A Document Title Bar allows users to perform various actions related to the current document's metadata, such as renaming, viewing saved location and version history. +

+

+ A consumer of this component may customize the appearance of the Document Title Bar by setting the corresponding colors in the Theme object. +

+
+ } + bestPractices={ +
+ } + dos={ +
+
    +
  • Pass true to the "hasVersions" property only if the current document has version history.
  • +
  • Provide callback for "onRenameDocument".
  • +
+
+ } + donts={ +
+
    +
  • Don't pass a "filePath" that is too long.
  • +
+
+ } + isHeaderVisible={ this.props.isHeaderVisible }> + + ); + } +} diff --git a/packages/office-ui-fabric-react/src/components/DocumentTitleBar/examples/DocumentTitleBar.Basic.Example.tsx b/packages/office-ui-fabric-react/src/components/DocumentTitleBar/examples/DocumentTitleBar.Basic.Example.tsx new file mode 100644 index 0000000000000..45335b17f5eb5 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/DocumentTitleBar/examples/DocumentTitleBar.Basic.Example.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { DocumentTitleBar } from '../DocumentTitleBar'; + +export interface IDocumentTitleBarExampleState { + documentTitle: string; + statusText: string; +} + +export class DocumentTitleBarExample extends React.Component { + constructor() { + super(); + this.state = { + documentTitle: 'Climate Change', + statusText: undefined + }; + this._onRenameDocument = this._onRenameDocument.bind(this); + } + + public render() { + return ( + console.log('Clicked Saved Location') } + onClickVersionsMenuItem={ () => console.log('Clicked Versions') } + onRenameDocument={ this._onRenameDocument } /> + ); + } + + private _onRenameDocument(newName: string) { + this.setState({ + documentTitle: newName, + statusText: 'Saved' + }); + } +} diff --git a/packages/office-ui-fabric-react/src/components/DocumentTitleBar/index.ts b/packages/office-ui-fabric-react/src/components/DocumentTitleBar/index.ts new file mode 100644 index 0000000000000..a6eb9e57656e7 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/DocumentTitleBar/index.ts @@ -0,0 +1,2 @@ +export * from './DocumentTitleBar'; +export * from './DocumentTitleBar.Props'; diff --git a/packages/office-ui-fabric-react/src/demo/AppDefinition.tsx b/packages/office-ui-fabric-react/src/demo/AppDefinition.tsx index 9313881244f2f..ace45df6e6214 100644 --- a/packages/office-ui-fabric-react/src/demo/AppDefinition.tsx +++ b/packages/office-ui-fabric-react/src/demo/AppDefinition.tsx @@ -88,6 +88,12 @@ export const AppDefinition: IAppDefinition = { name: 'DocumentCard', url: '#/examples/documentcard' }, + { + getComponent: cb => cb(require('../components/DocumentTitleBar/DocumentTitleBarPage').DocumentTitleBarPage), + key: 'DocumentTitleBar', + name: 'DocumentTitleBar', + url: '#/examples/documenttitlebar' + }, { getComponent: cb => cb(require('../components/Dropdown/DropdownPage').DropdownPage), key: 'Dropdown', diff --git a/packages/office-ui-fabric-react/src/index.ts b/packages/office-ui-fabric-react/src/index.ts index c96aa1d436290..fd8248dbde7bc 100644 --- a/packages/office-ui-fabric-react/src/index.ts +++ b/packages/office-ui-fabric-react/src/index.ts @@ -16,6 +16,7 @@ export * from './DatePicker'; export * from './DetailsList'; export * from './Dialog'; export * from './DocumentCard'; +export * from './DocumentTitleBar'; export * from './Dropdown'; export * from './Fabric'; export * from './Facepile'; From 6b9716df640ffd68428257b71eb1e02ca49f1939 Mon Sep 17 00:00:00 2001 From: "REDMOND\\jspurlin" Date: Mon, 24 Apr 2017 10:26:28 -0700 Subject: [PATCH 26/64] Add the ability for the spinButton buttons to look pressed when spinning via keyboard --- .../src/components/SpinButton/SpinButton.tsx | 71 +++++++++++++++---- 1 file changed, 59 insertions(+), 12 deletions(-) diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx index c5e4f2e2ad47a..c5d01fc740678 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx @@ -24,6 +24,16 @@ export interface ISpinButtonState { * and the text field gets focus (we should stop spinning) */ spinning?: boolean; + + /** + * keyboard spin direction, used to style the up or down button + * as active when up/down arrow is pressed + * + * -1 == spinning down + * 0 == not spinning + * 1 == spinning up + */ + keyboardSpinDirection?: number; } export class SpinButton extends BaseComponent implements ISpinButton { @@ -62,6 +72,7 @@ export class SpinButton extends BaseComponent{ label } }
} @@ -154,7 +165,7 @@ export class SpinButton extends BaseComponent { this._updateValue(true /* shouldSpin */, this._onIncrement); } } onMouseLeave={ this._stop } onMouseUp={ this._stop } - onBlur={ this._stop } tabIndex={ -1 } /> { this._updateValue(true /* shouldSpin */, this._onDecrement); } } onMouseLeave={ this._stop } onMouseUp={ this._stop } - onBlur={ this._stop } tabIndex={ -1 } /> @@ -184,7 +193,6 @@ export class SpinButton extends BaseComponent{ label } }
} @@ -196,7 +204,7 @@ export class SpinButton extends BaseComponent { if (isNaN(+value)) { return this._lastValidValue; @@ -211,15 +222,26 @@ export class SpinButton extends BaseComponent { let newValue = Math.min(+value + this.props.step, this.props.max); return String(newValue); } + + /** + * Increment function to use if one is not passed in + */ private _defaultOnDecrement = (value: string) => { let newValue = Math.max(+value - this.props.step, this.props.min); return String(newValue); } + /** + * Set the label direction with the gap on the correct side + */ private _labelDirectionHelper(): any { let style: any = {}; @@ -253,8 +275,7 @@ export class SpinButton extends BaseComponent) { const element: HTMLInputElement = event.target as HTMLInputElement; @@ -266,6 +287,11 @@ export class SpinButton extends BaseComponent): void { const element: HTMLInputElement = event.target as HTMLInputElement; const value: string = element.value; @@ -275,6 +301,12 @@ export class SpinButton extends BaseComponent string) { const newValue = stepFunction(this.state.value); this._lastValidValue = newValue; @@ -298,8 +330,8 @@ export class SpinButton extends BaseComponent) { + if (this.props.disabled) { this._stop(); return; From 238ad26f83e8bc521954a01a20ab6039a7463704 Mon Sep 17 00:00:00 2001 From: Derek Sun Date: Mon, 24 Apr 2017 13:24:06 -0700 Subject: [PATCH 27/64] Revert "Implement color scheme in the ContextualMenu control to enable alternative theming." This reverts commit 4f830cd8c55269368e73bfc6d0cd69430776b977. --- .../ContextualMenu/ContextualMenu.Props.ts | 28 ----- .../ContextualMenu/ContextualMenu.scss | 53 ++------ .../ContextualMenu/ContextualMenu.tsx | 104 +++------------- .../ContextualMenu/ContextualMenuPage.tsx | 5 - .../ContextualMenu.ColorScheme.Example.tsx | 117 ------------------ 5 files changed, 26 insertions(+), 281 deletions(-) delete mode 100644 packages/office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.ColorScheme.Example.tsx diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.Props.ts b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.Props.ts index 38b74d0a22a39..52c05de36f339 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.Props.ts +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.Props.ts @@ -19,24 +19,6 @@ export interface IContextualMenu { } -export enum ContextualMenuColorScheme { - /** - * Icon color: themePrimary, - * Highlighted menu item background color: neutralLighter, - * Expanded menu item background color: neutralQuaternaryAlt, - * Header text color: themePrimary - */ - Default = 0, - - /** - * Icon color: #a6a6a6, - * Highlighted menu item background color: themeDark, - * Expanded menu item background color: themeDarker, - * Header text color: #a6a6a6 - */ - Themed = 1, -} - export interface IContextualMenuProps extends React.Props { /** * Optional callback to access the IContextualMenu interface. Use this instead of ref for accessing @@ -57,16 +39,6 @@ export interface IContextualMenuProps extends React.Props { */ targetElement?: HTMLElement; - /** - * The color scheme determines the following colors: - * - the color of a header menu item - * - the background color of a menu item when it is hovered - * - the colors of a menu item's text - * - the colors of a menu item's icon - * @default ContextualMenuColorScheme.Neutral - */ - colorScheme?: ContextualMenuColorScheme; - /** * How the element should be positioned * @default DirectionalHint.bottomAutoEdge diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.scss b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.scss index d6f2c697d1c25..900c080d0fd38 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.scss +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.scss @@ -1,10 +1,8 @@ @import '../../common/common'; $ContextualMenu-background: $ms-color-white; -$ContextualMenu-itemHover-Default: $ms-color-neutralLighter; -$ContextualMenu-itemHover-Themed: $ms-color-themeDark; -$ContextualMenu-expandedItemBackground-Default: $ms-color-neutralQuaternaryAlt; -$ContextualMenu-expandedItemBackground-Themed: $ms-color-themeDarker; +$ContextualMenu-itemHover: $ms-color-neutralLighter; +$ContextualMenu-expandedItemBackground: $ms-color-neutralQuaternaryAlt; $ContextualMenu-itemHeight: 36px; $ContextualMenu-iconWidth: 14px; @@ -43,6 +41,9 @@ $ContextualMenu-iconWidth: 14px; padding: 0px 6px; @include text-align(left); + &:hover:not([disabled]) { + background: $ContextualMenu-itemHover; + } &.isDisabled, &[disabled] { color: $ms-color-neutralTertiaryAlt; @@ -52,52 +53,24 @@ $ContextualMenu-iconWidth: 14px; color: $ms-color-neutralTertiaryAlt; } } -} - -.linkDefault { - @extend .link; - - &:hover:not([disabled]) { - background: $ContextualMenu-itemHover-Default; - } :global(.is-focusVisible) &:focus { - background: $ContextualMenu-itemHover-Default; + background: $ContextualMenu-itemHover; } &.isExpanded, &.isExpanded:hover { - background: $ContextualMenu-expandedItemBackground-Default; + background: $ContextualMenu-expandedItemBackground; color: $ms-color-black; font-weight: $ms-font-weight-semibold; } } -.linkThemed { - @extend .link; - - &:hover:not([disabled]) { - background: $ContextualMenu-itemHover-Themed; - color: white; - } - - :global(.is-focusVisible) &:focus { - background: $ContextualMenu-itemHover-Themed; - color: white; - } - - &.isExpanded, - &.isExpanded:hover { - background: $ContextualMenu-expandedItemBackground-Themed; - color: white; - font-weight: $ms-font-weight-semibold; - } -} - .header { @include focus-border(); @include ms-font-s; font-weight: $ms-font-weight-semibold; + color: $ms-color-themePrimary; background: none; border: none; height: $ContextualMenu-itemHeight; @@ -108,16 +81,6 @@ $ContextualMenu-iconWidth: 14px; @include text-align(left); } -.headerDefault { - @extend .header; - color: $ms-color-themePrimary; -} - -.headerThemed { - @extend .header; - color: #a6a6a6; -} - a.link { padding: 0px 6px; text-rendering: auto; diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx index 51ac934d10319..db96ccc56612a 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx @@ -1,10 +1,5 @@ import * as React from 'react'; -import { - IContextualMenuProps, - IContextualMenuItem, - ContextualMenuItemType, - ContextualMenuColorScheme -} from './ContextualMenu.Props'; +import { IContextualMenuProps, IContextualMenuItem, ContextualMenuItemType } from './ContextualMenu.Props'; import { DirectionalHint } from '../../common/DirectionalHint'; import { FocusZone, FocusZoneDirection } from '../../FocusZone'; import { @@ -26,7 +21,7 @@ import { Icon, IIconProps } from '../../Icon'; -import * as styles from './ContextualMenu.scss'; +import styles = require('./ContextualMenu.scss'); export interface IContextualMenuState { expandedMenuItemKey?: string; @@ -91,8 +86,6 @@ export function getSubmenuItems(item: IContextualMenuItem) { return item.subMenuProps ? item.subMenuProps.items : item.items; } -const ContextualMenuThemedAltColor = '#a6a6a6'; - export class ContextualMenu extends BaseComponent { // The default ContextualMenu properities have no items and beak, the default submenu direction is right and top. public static defaultProps = { @@ -103,13 +96,11 @@ export class ContextualMenu extends BaseComponent ) : (null) } { submenuProps ? ( // If a submenu properities exists, the submenu will be rendered. - + ) : (null) } @@ -249,14 +239,6 @@ export class ContextualMenu extends BaseComponent - { this._renderMenuItemChildren(item, index, hasCheckmarks, hasIcons, true) } +
+ { this._renderMenuItemChildren(item, index, hasCheckmarks, hasIcons) }
); } @@ -327,11 +309,11 @@ export class ContextualMenu extends BaseComponent - { this._renderMenuItemChildren(item, index, hasCheckmarks, hasIcons, false) } + { this._renderMenuItemChildren(item, index, hasCheckmarks, hasIcons) } ); } @@ -351,16 +333,14 @@ export class ContextualMenu extends BaseComponent this._onItemMouseDown(item, ev), - onFocus: (ev: any) => this._onItemFocus(item, ev), - onBlur: (ev: any) => this._onItemBlur(item, ev), disabled: item.isDisabled || item.disabled, href: item.href, title: item.title, @@ -373,24 +353,20 @@ export class ContextualMenu extends BaseComponent { (hasCheckmarks) ? ( + onClick={ this._onItemClick.bind(this, item) } /> ) : (null) } - { (hasIcons && !isHeader) ? ( + { (hasIcons) ? ( this._renderIcon(item) ) : (null) } { item.name } @@ -398,8 +374,7 @@ export class ContextualMenu extends BaseComponent + className={ css('ms-ContextualMenu-submenuIcon', styles.submenuIcon, item.submenuIconProps ? item.submenuIconProps.className : '') } /> ) : (null) } ); @@ -413,15 +388,10 @@ export class ContextualMenu extends BaseComponent; - } else { - return ; - } + return ; } @autobind @@ -439,45 +409,9 @@ export class ContextualMenu extends BaseComponent, isHighlighted: boolean) { - const iconElement = ev.currentTarget.getElementsByTagName('i')[0]; - if (this.props.colorScheme === ContextualMenuColorScheme.Themed && iconElement !== undefined) { - iconElement.style.color = this._iconColorForThemedItem(menuItem, isHighlighted); - } - } - - private _onItemFocus(item: any, ev: React.FocusEvent) { - // We automatically focus on the first menu item when we open a context menu, - // but the initial focus is not supposed to be visible, so we need to avoid - // changing the color in that case. - if (this._hasInitialFocusEventHappend) { - this._updateIconColorForMenuItemIfNeeded(item, ev, true); - } else { - this._hasInitialFocusEventHappend = true; - } - } - - private _onItemBlur(item: any, ev: React.FocusEvent) { - this._updateIconColorForMenuItemIfNeeded(item, ev, false); - } - private _onItemMouseEnter(item: any, ev: React.MouseEvent) { let targetElement = ev.currentTarget as HTMLElement; + if (item.key !== this.state.expandedMenuItemKey) { if (hasSubmenuItems(item)) { this._enterTimerId = this._async.setTimeout(() => this._onItemSubMenuExpand(item, targetElement), 500); @@ -485,13 +419,11 @@ export class ContextualMenu extends BaseComponent this._onSubMenuDismiss(ev), 500); } } - this._updateIconColorForMenuItemIfNeeded(item, ev, true); } @autobind - private _onMouseLeave(item: any, ev: React.MouseEvent) { + private _onMouseLeave(ev: React.MouseEvent) { this._async.clearTimeout(this._enterTimerId); - this._updateIconColorForMenuItemIfNeeded(item, ev, false); } private _onItemMouseDown(item: IContextualMenuItem, ev: React.MouseEvent) { diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuPage.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuPage.tsx index f94f14478e1a0..140fd2a9e553a 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuPage.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenuPage.tsx @@ -10,14 +10,12 @@ import { ContextualMenuCheckmarksExample } from './examples/ContextualMenu.Check import { ContextualMenuDirectionalExample } from './examples/ContextualMenu.Directional.Example'; import { ContextualMenuCustomizationExample } from './examples/ContextualMenu.Customization.Example'; import { ContextualMenuHeaderExample } from './examples/ContextualMenu.Header.Example'; -import { ContextualMenuColorSchemeExample } from './examples/ContextualMenu.ColorScheme.Example'; const ContextualMenuBasicExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.Basic.Example.tsx') as string; const ContextualMenuCheckmarksExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.Checkmarks.Example.tsx') as string; const ContextualMenuDirectionalExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.Directional.Example.tsx') as string; const ContextualMenuCustomizationExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.Customization.Example.tsx') as string; const ContextualMenuHeaderExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.Header.Example.tsx') as string; -const ContextualMenuColorSchemeExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.ColorScheme.Example.tsx') as string; export class ContextualMenuPage extends React.Component { public render() { @@ -42,9 +40,6 @@ export class ContextualMenuPage extends React.Component - - - } propertiesTables={ diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.ColorScheme.Example.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.ColorScheme.Example.tsx deleted file mode 100644 index 7004793db1081..0000000000000 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/examples/ContextualMenu.ColorScheme.Example.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import * as React from 'react'; -import { Button } from 'office-ui-fabric-react/lib/Button'; -import { ContextualMenu, DirectionalHint, ContextualMenuColorScheme } from 'office-ui-fabric-react/lib/ContextualMenu'; -import { getRTL } from 'office-ui-fabric-react/lib/Utilities'; -import './ContextualMenuExample.scss'; - -export class ContextualMenuColorSchemeExample extends React.Component { - - constructor() { - super(); - this.state = { - isContextMenuVisible: false - }; - this._onClick = this._onClick.bind(this); - this._onDismiss = this._onDismiss.bind(this); - } - - public render() { - - return ( -
- - { this.state.isContextMenuVisible ? ( - ) : (null) } -
- ); - } - - private _onClick(event: React.MouseEvent) { - this.setState({ target: event.target, isContextMenuVisible: true }); - } - - private _onDismiss(event: any) { - this.setState({ isContextMenuVisible: false }); - } -} From 0436c114deee1b4147a11a1d6a27d9a930d19838 Mon Sep 17 00:00:00 2001 From: Derek Sun Date: Mon, 24 Apr 2017 13:30:10 -0700 Subject: [PATCH 28/64] Don't render an empty icon for an icon-less header menu item. --- .../src/components/ContextualMenu/ContextualMenu.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx index db96ccc56612a..197030d729d47 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx @@ -297,7 +297,7 @@ export class ContextualMenu extends BaseComponent - { this._renderMenuItemChildren(item, index, hasCheckmarks, hasIcons) } + { this._renderMenuItemChildren(item, index, hasCheckmarks, hasIcons, true) } ); } @@ -313,7 +313,7 @@ export class ContextualMenu extends BaseComponent - { this._renderMenuItemChildren(item, index, hasCheckmarks, hasIcons) } + { this._renderMenuItemChildren(item, index, hasCheckmarks, hasIcons, false) } ); } @@ -353,10 +353,10 @@ export class ContextualMenu extends BaseComponent @@ -366,7 +366,7 @@ export class ContextualMenu extends BaseComponent ) : (null) } - { (hasIcons) ? ( + { (hasIcons && !isHeader) ? ( this._renderIcon(item) ) : (null) } { item.name } From ee4b3d0fff7658d693cfc1ecd3fd22130fdd351c Mon Sep 17 00:00:00 2001 From: Derek Sun Date: Mon, 24 Apr 2017 13:48:09 -0700 Subject: [PATCH 29/64] Revert "Implement Document Title Bar" --- .../src/components/App/AppState.tsx | 6 - .../DocumentTitleBarComponentPage.tsx | 29 --- .../src/DocumentTitleBar.ts | 1 - .../ContextualMenu/ContextualMenu.tsx | 10 +- .../DocumentTitleBar.Props.ts | 45 ---- .../DocumentTitleBar/DocumentTitleBar.scss | 54 ---- .../DocumentTitleBar.test.tsx | 231 ------------------ .../DocumentTitleBar/DocumentTitleBar.tsx | 180 -------------- .../DocumentTitleBar/DocumentTitleBarPage.tsx | 64 ----- .../DocumentTitleBar.Basic.Example.tsx | 38 --- .../src/components/DocumentTitleBar/index.ts | 2 - .../src/demo/AppDefinition.tsx | 6 - packages/office-ui-fabric-react/src/index.ts | 1 - 13 files changed, 5 insertions(+), 662 deletions(-) delete mode 100644 apps/fabric-website/src/pages/Components/DocumentTitleBarComponentPage.tsx delete mode 100644 packages/office-ui-fabric-react/src/DocumentTitleBar.ts delete mode 100644 packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.Props.ts delete mode 100644 packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.scss delete mode 100644 packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.test.tsx delete mode 100644 packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.tsx delete mode 100644 packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBarPage.tsx delete mode 100644 packages/office-ui-fabric-react/src/components/DocumentTitleBar/examples/DocumentTitleBar.Basic.Example.tsx delete mode 100644 packages/office-ui-fabric-react/src/components/DocumentTitleBar/index.ts diff --git a/apps/fabric-website/src/components/App/AppState.tsx b/apps/fabric-website/src/components/App/AppState.tsx index ae0f0ed2dc752..6b589a7872b9c 100644 --- a/apps/fabric-website/src/components/App/AppState.tsx +++ b/apps/fabric-website/src/components/App/AppState.tsx @@ -166,12 +166,6 @@ export const AppState: IAppState = { component: () => , getComponent: cb => require.ensure([], (require) => cb(require('../../pages/Components/DocumentCardComponentPage').DocumentCardComponentPage)) }, - { - title: 'DocumentTitleBar', - url: '#/components/documenttitlebar', - component: () => , - getComponent: cb => require.ensure([], (require) => cb(require('../../pages/Components/DocumentTitleBarComponentPage').DocumentTitleBarComponentPage)) - }, { title: 'Dropdown', url: '#/components/dropdown', diff --git a/apps/fabric-website/src/pages/Components/DocumentTitleBarComponentPage.tsx b/apps/fabric-website/src/pages/Components/DocumentTitleBarComponentPage.tsx deleted file mode 100644 index 88cfd50fd0c76..0000000000000 --- a/apps/fabric-website/src/pages/Components/DocumentTitleBarComponentPage.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import * as React from 'react'; -import { DocumentTitleBarPage } from 'office-ui-fabric-react/lib/components/DocumentTitleBar/DocumentTitleBarPage'; -import { PageHeader } from '../../components/PageHeader/PageHeader'; -import { ComponentPage } from '../../components/ComponentPage/ComponentPage'; - -export class DocumentTitleBarComponentPage extends React.Component { - public render() { - return ( -
- - - - -
- ); - } -} diff --git a/packages/office-ui-fabric-react/src/DocumentTitleBar.ts b/packages/office-ui-fabric-react/src/DocumentTitleBar.ts deleted file mode 100644 index c9eb2a677ca3f..0000000000000 --- a/packages/office-ui-fabric-react/src/DocumentTitleBar.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './components/DocumentTitleBar/index'; diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx index 197030d729d47..db96ccc56612a 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.tsx @@ -297,7 +297,7 @@ export class ContextualMenu extends BaseComponent - { this._renderMenuItemChildren(item, index, hasCheckmarks, hasIcons, true) } + { this._renderMenuItemChildren(item, index, hasCheckmarks, hasIcons) } ); } @@ -313,7 +313,7 @@ export class ContextualMenu extends BaseComponent - { this._renderMenuItemChildren(item, index, hasCheckmarks, hasIcons, false) } + { this._renderMenuItemChildren(item, index, hasCheckmarks, hasIcons) } ); } @@ -353,10 +353,10 @@ export class ContextualMenu extends BaseComponent @@ -366,7 +366,7 @@ export class ContextualMenu extends BaseComponent ) : (null) } - { (hasIcons && !isHeader) ? ( + { (hasIcons) ? ( this._renderIcon(item) ) : (null) } { item.name } diff --git a/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.Props.ts b/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.Props.ts deleted file mode 100644 index daef0a3171d23..0000000000000 --- a/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.Props.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as React from 'react'; -import { DocumentTitleBar } from './DocumentTitleBar'; - -export interface IDocumentTitleBarProps extends React.Props { - /** - * Additional className to append to the root Document Title Bar element. - */ - className?: string; - - /** - * The document's title. - */ - title: string; - - /** - * Optional text to indicate the status of the current document (e.g. Saved) - */ - statusText?: string; - - /** - * File path of the document's saved location. - */ - filePath: string; - - /** - * Whether or not the current document has older versions. - * If set to false, the contextual menu will not show the Versions menu item. - */ - hasVersions: boolean; - - /** - * Callback that is invoked when the user renames the document. - */ - onRenameDocument?: (newName: string) => void; - - /** - * Callback that is invoked when the user clicks the Saved Location menu item. - */ - onClickSavedLocationMenuItem?: (event: React.MouseEvent) => void; - - /** - * Callback that is invoked when the user clicks the Versions menu item. - */ - onClickVersionsMenuItem?: (event: React.MouseEvent) => void; -} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.scss b/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.scss deleted file mode 100644 index 120bfafda8b64..0000000000000 --- a/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.scss +++ /dev/null @@ -1,54 +0,0 @@ -@import '../../common/common'; - -:global { - .ms-DocumentTitleBar-button { - background-color: $ms-color-themeDark; - color: white; - display: inline-block; - font-size: 20px; - padding: 10px 15px 10px 15px; - vertical-align: middle; - border-radius: 3px; - border: 0px; - cursor: default; - - &:hover { - background-color: $ms-color-themeDarkAlt; - cursor: pointer; - } - } - - .isHighlighted { - background-color: $ms-color-themeDarkAlt; - } - - .ms-DocumentTitleBar-button .ms-Icon { - color: rgba(255, 255, 255, .4); - padding-left: 10px; - display: inline; - vertical-align: middle; - font-size: 11px; - } - - .ms-DocumentTitleBar-button .statusText { - user-select: none; - color: rgba(255, 255, 255, .4); - font-size: 13px; - } - - .ms-DocumentTitleBar-renameDiv { - background-color: $ms-color-themeDark; - display: inline-block; - border-radius: 3px; - } - - .ms-DocumentTitleBar-renameDiv .textField { - font-size: 20px; - padding: 10px 15px 10px 15px; - vertical-align: middle; - background-color: rgba(0, 0, 0, .4); - color: white; - border: 0px; - text-align: center; - } -} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.test.tsx b/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.test.tsx deleted file mode 100644 index f999b06f05906..0000000000000 --- a/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.test.tsx +++ /dev/null @@ -1,231 +0,0 @@ -/* tslint:disable:no-unused-variable */ -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; -/* tslint:enable:no-unused-variable */ - -import * as ReactTestUtils from 'react-addons-test-utils'; -import { KeyCodes } from '../../Utilities'; -import { DocumentTitleBar } from './DocumentTitleBar'; - -const { expect } = chai; -const DocumentTitleBarButtonClassSelector = '.ms-DocumentTitleBar-button'; -const DocumentTitleBarStatusTextClassSelector = '.statusText'; -const MenuItemIndexRename = 0; -const MenuItemIndexSavedLocation = 3; -const MenuItemIndexVersions = 5; - -describe('DocumentTitleBar', () => { - - afterEach(() => { - for (let i = 0; i < document.body.children.length; i++) { - if (document.body.children[i].tagName === 'DIV') { - document.body.removeChild(document.body.children[i]); - i--; - } - } - }); - - it('should display title in the bar', () => { - const container = document.createElement('div'); - - ReactDOM.render( - , - container - ); - - const docTitleBarButton = container.querySelector(DocumentTitleBarButtonClassSelector); - const statusTextElement = docTitleBarButton.querySelector(DocumentTitleBarStatusTextClassSelector); - const titleNode = docTitleBarButton.childNodes[1]; - - expect(titleNode.nodeValue).to.equal('hello'); - expect(statusTextElement).to.not.exist; - }); - - it('should display title and status text in the bar', () => { - const container = document.createElement('div'); - - ReactDOM.render( - , - container - ); - - const docTitleBarButton = container.querySelector(DocumentTitleBarButtonClassSelector); - const titleNode = docTitleBarButton.childNodes[1]; - const statusTextElement = docTitleBarButton.querySelector(DocumentTitleBarStatusTextClassSelector); - const statusTextNode = statusTextElement.childNodes[4]; - - expect(titleNode.nodeValue).to.equal('hello'); - expect(statusTextNode.nodeValue).to.equal('world'); - }); - - it('should populate contextual menu items correctly', () => { - const container = document.createElement('div'); - - ReactDOM.render( - , - container - ); - - // Open the contextual menu by clicking the bar - const docTitleBarButton = container.querySelector(DocumentTitleBarButtonClassSelector); - ReactTestUtils.Simulate.click(docTitleBarButton); - const contextualMenuList = document.querySelector('.ms-ContextualMenu-list'); - expect(contextualMenuList).to.exist; - - const getMenuItemNameForIndex = (index: number) => { - return contextualMenuList.childNodes[index].firstChild.attributes.getNamedItem('name').value; - }; - - // Verify each menu item - expect(getMenuItemNameForIndex(MenuItemIndexRename)).to.equal('Rename'); - expect(getMenuItemNameForIndex(MenuItemIndexSavedLocation)).to.equal('OneDrive > Documents'); - expect(getMenuItemNameForIndex(MenuItemIndexVersions)).to.equal('Versions'); - }); - - it('should populate current title text in the Rename text field and rename properly', () => { - const container = document.createElement('div'); - const initialDocumentName = 'Document'; - const newDocumentName = 'Nice'; - let newDocumentNameToTest; - - const onRename = (newName: string) => { - newDocumentNameToTest = newName; - }; - - ReactDOM.render( - , - container - ); - - // Open the contextual menu by clicking the bar - const docTitleBarButton = container.querySelector(DocumentTitleBarButtonClassSelector); - ReactTestUtils.Simulate.click(docTitleBarButton); - const contextualMenuList = document.querySelector('.ms-ContextualMenu-list'); - expect(contextualMenuList).to.exist; - - // Find the Rename menu item and click it - const renameItem = contextualMenuList.childNodes[MenuItemIndexRename].firstChild as HTMLButtonElement; - ReactTestUtils.Simulate.click(renameItem); - const docTitleRenameTextField = container.querySelector('.ms-DocumentTitleBar-renameDiv .textField') as HTMLInputElement; - - // Verify that the input field prepopulates the current document title - expect(docTitleRenameTextField.value).to.equal(initialDocumentName); - - // Now try to rename - docTitleRenameTextField.value = newDocumentName; - ReactTestUtils.Simulate.blur(docTitleRenameTextField); - expect(newDocumentNameToTest).to.equal(newDocumentName); - }); - - it('should invoke Saved Location callback', () => { - const container = document.createElement('div'); - let hasInvokedSavedLocationCallback = false; - - const onClickSavedLocation = () => { - hasInvokedSavedLocationCallback = true; - }; - - ReactDOM.render( - , - container - ); - - // Open the contextual menu by clicking the bar - const docTitleBarButton = container.querySelector(DocumentTitleBarButtonClassSelector); - ReactTestUtils.Simulate.click(docTitleBarButton); - const contextualMenuList = document.querySelector('.ms-ContextualMenu-list'); - expect(contextualMenuList).to.exist; - - // Verify clicking Saved Location - const savedLocationItem = contextualMenuList.childNodes[MenuItemIndexSavedLocation].firstChild as HTMLButtonElement; - ReactTestUtils.Simulate.click(savedLocationItem); - expect(hasInvokedSavedLocationCallback).is.true; - }); - - it('should invoke Versions callback', () => { - const container = document.createElement('div'); - let hasInvokedVersionsCallback = false; - - const onClickVersions = () => { - hasInvokedVersionsCallback = true; - }; - - ReactDOM.render( - , - container - ); - - // Open the contextual menu by clicking the bar - const docTitleBarButton = container.querySelector(DocumentTitleBarButtonClassSelector); - ReactTestUtils.Simulate.click(docTitleBarButton); - const contextualMenuList = document.querySelector('.ms-ContextualMenu-list'); - expect(contextualMenuList).to.exist; - - // Verify clicking Versions - const versionsItem = contextualMenuList.childNodes[MenuItemIndexVersions].firstChild as HTMLButtonElement; - ReactTestUtils.Simulate.click(versionsItem); - expect(hasInvokedVersionsCallback).is.true; - }); - - it('should not rename if the user presses the Escape key', () => { - const container = document.createElement('div'); - let isRenameCallbackInvoked = false; - - const onRename = (newName: string) => { - isRenameCallbackInvoked = true; - }; - - ReactDOM.render( - , - container - ); - - // Open the contextual menu by clicking the bar - const docTitleBarButton = container.querySelector(DocumentTitleBarButtonClassSelector); - ReactTestUtils.Simulate.click(docTitleBarButton); - const contextualMenuList = document.querySelector('.ms-ContextualMenu-list'); - expect(contextualMenuList).to.exist; - - // Find the Rename menu item and click it - const renameItem = contextualMenuList.childNodes[MenuItemIndexRename].firstChild as HTMLButtonElement; - ReactTestUtils.Simulate.click(renameItem); - const docTitleRenameTextField = container.querySelector('.ms-DocumentTitleBar-renameDiv .textField') as HTMLInputElement; - - // Cancel out Rename by pressing the Escape key - ReactTestUtils.Simulate.keyDown(docTitleRenameTextField, { which: KeyCodes.escape }); - expect(isRenameCallbackInvoked).is.false; - }); - -}); \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.tsx b/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.tsx deleted file mode 100644 index fc4ad45af5b6b..0000000000000 --- a/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import * as React from 'react'; -import { BaseComponent, KeyCodes } from '../../Utilities'; -import { - ContextualMenu, - ContextualMenuItemType, - IContextualMenuItem -} from '../../ContextualMenu'; -import { DirectionalHint } from '../../common/DirectionalHint'; -import { Icon } from '../../Icon'; -import { IDocumentTitleBarProps } from './DocumentTitleBar.Props'; -import './DocumentTitleBar.scss'; - -const documentTitleButtonClassName = 'ms-DocumentTitleBar-button'; - -export interface IDocumentTitleBarState { - isContextualMenuVisible: boolean; - isRenaming: boolean; -} - -export class DocumentTitleBar extends BaseComponent { - private _renameTextField: HTMLInputElement; - - constructor(props: IDocumentTitleBarProps) { - super(props); - - this._onClickTitleButton = this._onClickTitleButton.bind(this); - this._onDismissContextualMenu = this._onDismissContextualMenu.bind(this); - this._onClickRenameMenuItem = this._onClickRenameMenuItem.bind(this); - this._onClickSavedLocationMenuItem = this._onClickSavedLocationMenuItem.bind(this); - this._onClickVersionsMenuItem = this._onClickVersionsMenuItem.bind(this); - this._onBlurTitleTextField = this._onBlurTitleTextField.bind(this); - this._onKeyDown = this._onKeyDown.bind(this); - - this.state = { - isContextualMenuVisible: false, - isRenaming: false, - }; - } - - public render() { - const { className, hasVersions, filePath } = this.props; - return ( -
- { this.state.isRenaming ? this._renderEditableTextField() : this._renderClickableButton() } - { this.state.isContextualMenuVisible && - - } -
- ); - } - - private _onDismissContextualMenu() { - this.setState({ - isContextualMenuVisible: !this.state.isContextualMenuVisible - }); - } - - private _onClickTitleButton() { - this.setState({ - isContextualMenuVisible: !this.state.isContextualMenuVisible - }); - } - - private _onKeyDown(event: React.KeyboardEvent) { - if (this.state.isRenaming) { - switch (event.which) { - case KeyCodes.enter: - if (this.props.onRenameDocument !== undefined) { - this.props.onRenameDocument(this._renameTextField.value); - } - this.setState({ isRenaming: false }); - break; - case KeyCodes.escape: - this.setState({ isRenaming: false }); - break; - } - } - } - - private _onClickRenameMenuItem() { - this.setState({ - isRenaming: true - }, () => this._renameTextField.select()); - } - - private _onClickSavedLocationMenuItem(event: React.MouseEvent) { - if (this.props.onClickSavedLocationMenuItem !== undefined) { - this.props.onClickSavedLocationMenuItem(event); - } - } - - private _onClickVersionsMenuItem(event: React.MouseEvent) { - if (this.props.onClickVersionsMenuItem !== undefined) { - this.props.onClickVersionsMenuItem(event); - } - } - - private _onBlurTitleTextField(event: React.FocusEvent) { - if (this.props.onRenameDocument !== undefined) { - this.props.onRenameDocument((event.target as HTMLInputElement).value); - } - this.setState({ isRenaming: false }); - } - - private _renderClickableButton(): React.ReactNode { - const { title, statusText } = this.props; - return ( - - ); - } - - private _renderEditableTextField(): React.ReactNode { - return ( -
- this._renameTextField = textField } /> -
- ); - } - - private _getVersionsMenuItems(): IContextualMenuItem[] { - return [ - { - key: '-', - name: '-' - }, - { - key: 'Versions', - name: 'Versions', - iconProps: { - iconName: 'History' - }, - onClick: this._onClickVersionsMenuItem - } - ]; - } -} - -export default DocumentTitleBar; diff --git a/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBarPage.tsx b/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBarPage.tsx deleted file mode 100644 index 546503ffabb4a..0000000000000 --- a/packages/office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBarPage.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import * as React from 'react'; -import { - ExampleCard, - IComponentDemoPageProps, - ComponentPage, - PropertiesTableSet -} from '@uifabric/example-app-base'; -import { DocumentTitleBarExample } from './examples/DocumentTitleBar.Basic.Example'; - -const DocumentTitleBarExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/DocumentTitleBar/examples/DocumentTitleBar.Basic.Example.tsx') as string; - -export class DocumentTitleBarPage extends React.Component { - public render() { - return ( - - - - - - } - propertiesTables={ - ('!raw-loader!office-ui-fabric-react/src/components/DocumentTitleBar/DocumentTitleBar.Props.ts') - ] } - /> - } - overview={ -
-

- A Document Title Bar allows users to perform various actions related to the current document's metadata, such as renaming, viewing saved location and version history. -

-

- A consumer of this component may customize the appearance of the Document Title Bar by setting the corresponding colors in the Theme object. -

-
- } - bestPractices={ -
- } - dos={ -
-
    -
  • Pass true to the "hasVersions" property only if the current document has version history.
  • -
  • Provide callback for "onRenameDocument".
  • -
-
- } - donts={ -
-
    -
  • Don't pass a "filePath" that is too long.
  • -
-
- } - isHeaderVisible={ this.props.isHeaderVisible }> -
- ); - } -} diff --git a/packages/office-ui-fabric-react/src/components/DocumentTitleBar/examples/DocumentTitleBar.Basic.Example.tsx b/packages/office-ui-fabric-react/src/components/DocumentTitleBar/examples/DocumentTitleBar.Basic.Example.tsx deleted file mode 100644 index 45335b17f5eb5..0000000000000 --- a/packages/office-ui-fabric-react/src/components/DocumentTitleBar/examples/DocumentTitleBar.Basic.Example.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from 'react'; -import { DocumentTitleBar } from '../DocumentTitleBar'; - -export interface IDocumentTitleBarExampleState { - documentTitle: string; - statusText: string; -} - -export class DocumentTitleBarExample extends React.Component { - constructor() { - super(); - this.state = { - documentTitle: 'Climate Change', - statusText: undefined - }; - this._onRenameDocument = this._onRenameDocument.bind(this); - } - - public render() { - return ( - console.log('Clicked Saved Location') } - onClickVersionsMenuItem={ () => console.log('Clicked Versions') } - onRenameDocument={ this._onRenameDocument } /> - ); - } - - private _onRenameDocument(newName: string) { - this.setState({ - documentTitle: newName, - statusText: 'Saved' - }); - } -} diff --git a/packages/office-ui-fabric-react/src/components/DocumentTitleBar/index.ts b/packages/office-ui-fabric-react/src/components/DocumentTitleBar/index.ts deleted file mode 100644 index a6eb9e57656e7..0000000000000 --- a/packages/office-ui-fabric-react/src/components/DocumentTitleBar/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './DocumentTitleBar'; -export * from './DocumentTitleBar.Props'; diff --git a/packages/office-ui-fabric-react/src/demo/AppDefinition.tsx b/packages/office-ui-fabric-react/src/demo/AppDefinition.tsx index ace45df6e6214..9313881244f2f 100644 --- a/packages/office-ui-fabric-react/src/demo/AppDefinition.tsx +++ b/packages/office-ui-fabric-react/src/demo/AppDefinition.tsx @@ -88,12 +88,6 @@ export const AppDefinition: IAppDefinition = { name: 'DocumentCard', url: '#/examples/documentcard' }, - { - getComponent: cb => cb(require('../components/DocumentTitleBar/DocumentTitleBarPage').DocumentTitleBarPage), - key: 'DocumentTitleBar', - name: 'DocumentTitleBar', - url: '#/examples/documenttitlebar' - }, { getComponent: cb => cb(require('../components/Dropdown/DropdownPage').DropdownPage), key: 'Dropdown', diff --git a/packages/office-ui-fabric-react/src/index.ts b/packages/office-ui-fabric-react/src/index.ts index fd8248dbde7bc..c96aa1d436290 100644 --- a/packages/office-ui-fabric-react/src/index.ts +++ b/packages/office-ui-fabric-react/src/index.ts @@ -16,7 +16,6 @@ export * from './DatePicker'; export * from './DetailsList'; export * from './Dialog'; export * from './DocumentCard'; -export * from './DocumentTitleBar'; export * from './Dropdown'; export * from './Fabric'; export * from './Facepile'; From f5c7bb3776c4a89444b1b442eee9de4ba9979d8c Mon Sep 17 00:00:00 2001 From: "REDMOND\\jspurlin" Date: Wed, 10 May 2017 12:52:52 -0700 Subject: [PATCH 30/64] update some CSS for high contrast in ff and use css utility instead of concatenating string classnames --- .../src/components/SpinButton/SpinButton.scss | 109 ++++++++++++++---- .../src/components/SpinButton/SpinButton.tsx | 29 +++-- .../examples/SpinButton.Basic.Example.tsx | 2 +- .../SpinButton.BasicDisabled.Example.tsx | 2 +- ...pinButton.BasicWithEndPosition.Example.tsx | 2 +- .../SpinButton.BasicWithIcon.Example.tsx | 2 +- 6 files changed, 110 insertions(+), 36 deletions(-) diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.scss b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.scss index eed3caa919f6d..a8d6c6893dbc1 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.scss +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.scss @@ -9,14 +9,15 @@ font-size: 12px; } -.ms-Label { +.ms-SpinButtonLabel { pointer-events: none; + padding: 2px 0px; } .ms-SpinButtonContainer { width: 100%; - min-width: 90px; - overflow: hidden; + min-width: 86px; + padding: 2px; } .ms-ArrowBox { @@ -32,6 +33,8 @@ .ms-SpinButtonWrapper:hover .ms-ArrowBox, .ms-SpinButtonWrapper:hover .ms-SpinButton-Input { border-color: $ms-color-neutralSecondaryAlt; + outline: 2px dashed transparent; + @media screen and (-ms-high-contrast: active) { border-color: $ms-color-contrastBlackSelected; } @@ -46,15 +49,14 @@ } .ms-SpinButtonIcon { - padding: 5px; + padding: 2px 5px 5px 5px; font-size: 20px; } .ms-SpinButtonWrapper { - overflow: hidden; display: flex; - height: 32px; - min-width: 90px; + height: 26px; + min-width: 86px; } .ms-SpinButtonWrapper.topBottom { @@ -62,15 +64,31 @@ } .ms-SpinButton-Input:focus + .ms-ArrowBox { - border-color: $ms-color-themePrimary; + border-color: $ms-color-themePrimary; + outline: 2px dashed transparent; + + @media screen and (-ms-high-contrast: active) { + border-color: $ms-color-contrastBlackSelected; + } + + @media screen and (-ms-high-contrast: black-on-white) { + border-color: $ms-color-contrastWhiteSelected; + } } - .ms-SpinButton-Input:disabled + .ms-ArrowBox { + .ms-SpinButton-Input.disabled + .ms-ArrowBox { background-color: $ms-color-neutralLighter; border-color: $ms-color-neutralLighter; pointer-events: none; - opacity: 0.3; cursor: default; + + @media screen and (-ms-high-contrast: active) { + color: $ms-color-contrastBlackDisabled; + } + + @media screen and (-ms-high-contrast: black-on-white) { + color: $ms-color-contrastWhiteDisabled; + } } .ms-SpinButtonWrapper:hover .ms-SpinButton-Input:focus { @@ -86,13 +104,13 @@ font-size: $ms-font-size-m; color: $ms-color-neutralPrimary; height: 100%; - @include padding(0, 12px, 0, 12px); + @include padding(3px, 3px, 4px, 4px); outline: 0; text-overflow: ellipsis; display: block; float: left; - width: calc(100% - 27px); - min-width: 63px; + width: calc(100% - 14px); + min-width: 72px; border-right-width: 0px; overflow: hidden; cursor: text; @@ -100,6 +118,8 @@ &:focus { border-color: $ms-color-themePrimary; + outline: 2px dashed transparent; + @media screen and (-ms-high-contrast: active) { border-color: $ms-color-contrastBlackSelected; } @@ -109,28 +129,47 @@ } } - &[disabled] { + &::selection { + background-color: $ms-color-themePrimary; + color: $ms-color-white; + } + + @include input-placeholder { + color: $ms-color-neutralSecondary; + } + } + + .ms-SpinButtonWrapper:hover .ms-SpinButton-Input.disabled, + .ms-SpinButton-Input.disabled { background-color: $ms-color-neutralLighter; border-color: $ms-color-neutralLighter; pointer-events: none; - opacity: 0.3; cursor: default; + color: $ms-color-neutralTertiaryAlt; + + @media screen and (-ms-high-contrast: active) { + color: $ms-color-contrastBlackDisabled; } - @include input-placeholder { - color: $ms-color-neutralSecondary; + @media screen and (-ms-high-contrast: black-on-white) { + color: $ms-color-contrastWhiteDisabled; } } + .ms-SpinButton-Input:hover + .ms-ArrowBox .ms-UpButton, .ms-SpinButton-Input:hover + .ms-ArrowBox .ms-DownButton { + background-color: $ms-color-neutralLighter; + } + .ms-SpinButtonWrapper .ms-UpButton, .ms-SpinButtonWrapper .ms-DownButton { display: block; - height: 15px; - width: 26px; - padding-top: 0px; + height: 12px; + width: 12px; + padding: 0px; background-color: transparent; text-align: center; cursor: default; - font-size: 14px; + font-size: 6px; + color: $ms-color-neutralPrimary; &:hover { background-color: $ms-color-neutralLight; @@ -139,12 +178,40 @@ &:active { background-color: $ms-color-themePrimary; color: $ms-color-white; + + @media screen and (-ms-high-contrast: active) { + background-color: $ms-color-contrastBlackSelected; + } + + @media screen and (-ms-high-contrast: black-on-white) { + background-color: $ms-color-contrastWhiteSelected; + } + } + } + + .ms-UpButton:disabled, .ms-DownButton:disabled { + opacity: 0; + + @media screen and (-ms-high-contrast: active) { + color: $ms-color-contrastBlackDisabled; + } + + @media screen and (-ms-high-contrast: black-on-white) { + color: $ms-color-contrastWhiteDisabled; } } .ms-SpinButtonWrapper .ms-UpButton.active, .ms-SpinButtonWrapper .ms-DownButton.active { background-color: $ms-color-themePrimary; color: $ms-color-white; + + @media screen and (-ms-high-contrast: active) { + background-color: $ms-color-contrastBlackSelected; + } + + @media screen and (-ms-high-contrast: black-on-white) { + background-color: $ms-color-contrastWhiteSelected; + } } } \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx index c5d01fc740678..8f14adbb07086 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx @@ -3,6 +3,7 @@ import { IconButton } from '../../Button'; import { Label } from '../../Label'; import { BaseComponent, + css, getId, KeyCodes } from '../../Utilities'; @@ -46,8 +47,8 @@ export class SpinButton extends BaseComponent { labelPosition !== Position.bottom &&
- { icon && } + { icon && } { label && < Label id={ this._labelId } - htmlFor={ this._inputId }>{ label } + htmlFor={ this._inputId } + className='ms-SpinButtonLabel'>{ label } }
} -
+
{ labelPosition === Position.bottom &&
- { icon && } + { icon && } { label && }
} @@ -368,6 +371,10 @@ export class SpinButton extends BaseComponent { return ( { return ( { return ( Date: Wed, 10 May 2017 17:17:10 -0700 Subject: [PATCH 31/64] Fix quotation issue --- .../src/components/SpinButton/SpinButton.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx index 8f14adbb07086..8f160d21ebe78 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx @@ -136,7 +136,7 @@ export class SpinButton extends BaseComponent { labelPosition !== Position.bottom &&
- { icon && } + { icon && } { label && < Label id={ this._labelId } @@ -191,13 +191,14 @@ export class SpinButton extends BaseComponent
{ labelPosition === Position.bottom &&
- { icon && } + { icon && } { label && } + + }
} ) as React.ReactElement<{}>; From d309a83b87cadb234a7d5889ca28c42611a85dcb Mon Sep 17 00:00:00 2001 From: Boris Emorine Date: Wed, 10 May 2017 17:22:05 -0700 Subject: [PATCH 32/64] Fix Spin Button properties table. --- .../src/components/SpinButton/SpinButtonPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButtonPage.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButtonPage.tsx index 8f637292fce69..7dbd022c9dffb 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButtonPage.tsx +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButtonPage.tsx @@ -55,7 +55,7 @@ export class SpinButtonPage extends React.Component propertiesTables={ ('office-ui-fabric-react/lib/components/SpinButton/SpinButton.Props.ts') + require('!raw-loader!office-ui-fabric-react/lib/components/SpinButton/SpinButton.Props.ts') ] } /> } From c9a3dc74b11b337c1bd459b4029f3e03509a3f23 Mon Sep 17 00:00:00 2001 From: Boris Emorine Date: Wed, 10 May 2017 17:38:45 -0700 Subject: [PATCH 33/64] Fix Spin Button example code --- .../src/components/SpinButton/SpinButtonPage.tsx | 10 +++++----- .../examples/SpinButton.Stateful.Example.tsx | 1 - 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButtonPage.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButtonPage.tsx index 7dbd022c9dffb..6659f9c00d760 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButtonPage.tsx +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButtonPage.tsx @@ -11,11 +11,11 @@ import { SpinButtonStatefulExample } from './examples/SpinButton.Stateful.Exampl import { SpinButtonBasicWithIconExample } from './examples/SpinButton.BasicWithIcon.Example'; import { SpinButtonBasicWithEndPositionExample } from './examples/SpinButton.BasicWithEndPosition.Example'; -const SpinButtonBasicExampleCode = require('./examples/SpinButton.Basic.Example.tsx') as string; -const SpinButtonBasicDisabledExampleCode = require('./examples/SpinButton.BasicDisabled.Example.tsx') as string; -const SpinButtonStatefulExampleCode = require('./examples/SpinButton.Stateful.Example.tsx') as string; -const SpinButtonBasicWithIconExampleCode = require('./examples/SpinButton.BasicWithIcon.Example.tsx') as string; -const SpinButtonBasicWithEndPositionExampleCode = require('./examples/SpinButton.BasicWithEndPosition.Example.tsx') as string; +const SpinButtonBasicExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.Basic.Example.tsx') as string; +const SpinButtonBasicDisabledExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.BasicDisabled.Example.tsx') as string; +const SpinButtonStatefulExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.Stateful.Example.tsx') as string; +const SpinButtonBasicWithIconExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.BasicWithIcon.Example.tsx') as string; +const SpinButtonBasicWithEndPositionExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.BasicWithEndPosition.Example.tsx') as string; export class SpinButtonPage extends React.Component { public render() { diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.Stateful.Example.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.Stateful.Example.tsx index 4274b0bca99c3..409f15e47f5f3 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.Stateful.Example.tsx +++ b/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.Stateful.Example.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { SpinButton, ISpinButtonState, ISpinButtonProps } from 'office-ui-fabric-react/lib/SpinButton'; export class SpinButtonStatefulExample extends React.Component { - public render() { let suffix = ' cm'; From 4e7b615e0dff54cc48712f5829c8ecf9df9facde Mon Sep 17 00:00:00 2001 From: Boris Emorine Date: Thu, 11 May 2017 15:43:22 -0700 Subject: [PATCH 34/64] Use iconProps instead of string --- .../src/components/SpinButton/SpinButton.Props.ts | 7 ++++--- .../src/components/SpinButton/SpinButton.tsx | 6 +++--- .../examples/SpinButton.BasicWithEndPosition.Example.tsx | 2 +- .../examples/SpinButton.BasicWithIcon.Example.tsx | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts index 6afc2247f7b4c..13c9aece05e9b 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.Props.ts @@ -1,4 +1,5 @@ import { Position } from '../../utilities/positioning'; +import { IIconProps } from '../../Icon'; export interface ISpinButtonProps { @@ -34,7 +35,7 @@ export interface ISpinButtonProps { max?: number; /** - * The diffrrence between the two adjacent values of the SpinButton. + * The difference between the two adjacent values of the SpinButton. * @default 1 */ step?: number; @@ -75,9 +76,9 @@ export interface ISpinButtonProps { labelGapSpace?: number; /** - * Icon that goes along with the label for the whole splitButton + * Icon that goes along with the label for the whole SpinButton */ - icon?: string; + iconProps?: IIconProps; /** * This callback is triggered when the value inside the SpinButton should be validated. diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx index 8f160d21ebe78..8cef63a60cd04 100644 --- a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx +++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButton.tsx @@ -123,7 +123,7 @@ export class SpinButton extends BaseComponent { labelPosition !== Position.bottom &&
- { icon && } + { iconProps && } { label && < Label id={ this._labelId } @@ -191,7 +191,7 @@ export class SpinButton extends BaseComponent
{ labelPosition === Position.bottom &&
- { icon && } + { iconProps && } { label &&