+ );
+ }
+
+ /**
+ * OnFocus select the contents of the input
+ */
+ public focus() {
+ if (this._spinningByMouse || this.state.keyboardSpinDirection !== KeyboardSpinDirection.notSpinning) {
+ this._stop();
+ }
+
+ this._input.focus();
+ this._input.select();
+ }
+
+ /**
+ * Validate function to use if one is not passed in
+ */
+ private _defaultOnValidate = (value: string) => {
+ if (isNaN(Number(value))) {
+ return this._lastValidValue;
+ }
+ const newValue = Math.min(this.props.max, Math.max(this.props.min, Number(value)));
+ return String(newValue);
+ }
+
+ /**
+ * Increment function to use if one is not passed in
+ */
+ private _defaultOnIncrement = (value: string) => {
+ let newValue = Math.min(Number(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(Number(value) - this.props.step, this.props.min);
+ return String(newValue);
+ }
+
+ /**
+ * Returns the class name corresponding to the label position
+ */
+ private _getClassNameForLabelPosition(labelPosition: Position): string {
+ let className: string = '';
+
+ switch (labelPosition) {
+ case Position.start:
+ className = styles.start;
+ break;
+ case Position.end:
+ className = styles.end;
+ break;
+ case Position.top:
+ className = styles.top;
+ break;
+ case Position.bottom:
+ className = styles.bottom;
+ }
+
+ return className;
+ }
+
+ 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.
+ */
+ }
+
+ /**
+ * This is used when validating text entry
+ * in the input (not when changed via the buttons)
+ * @param event - the event that fired
+ */
+ @autobind
+ private _validate(event: React.FocusEvent) {
+ const element: HTMLInputElement = event.target as HTMLInputElement;
+ const value: string = element.value;
+ if (this.state.value) {
+ const newValue = this._onValidate(value);
+ if (newValue) {
+ this._lastValidValue = newValue;
+ this.setState({ value: newValue });
+ }
+ }
+ }
+
+ /**
+ * The method is needed to ensure we are updating the actual input value.
+ * without this our value will never change (and validation will not have the correct number)
+ * @param event - the event that was fired
+ */
+ @autobind
+ private _onInputChange(event: React.FormEvent): void {
+ const element: HTMLInputElement = event.target as HTMLInputElement;
+ const value: string = element.value;
+
+ this.setState({
+ value: value,
+ });
+ }
+
+ /**
+ * Update the value with the given stepFunction
+ * @param shouldSpin - should we fire off another updateValue when we are done here? This should be true
+ * when spinning in response to a mouseDown
+ * @param stepFunction - function to use to step by
+ */
+ @autobind
+ private _updateValue(shouldSpin: boolean, stepFunction: (string) => string | void) {
+ const newValue = stepFunction(this.state.value);
+ if (newValue) {
+ this._lastValidValue = newValue;
+ this.setState({ value: newValue });
+ }
+
+ if (this._spinningByMouse !== shouldSpin) {
+ this._spinningByMouse = shouldSpin;
+ }
+
+ if (shouldSpin) {
+ this._currentStepFunctionHandle = this._async.setTimeout(() => { this._updateValue(shouldSpin, stepFunction); }, this._stepDelay);
+ }
+ }
+
+ /**
+ * Stop spinning (clear any currently pending update and set spinning to false)
+ */
+ @autobind
+ private _stop() {
+ if (this._currentStepFunctionHandle >= 0) {
+ this._async.clearTimeout(this._currentStepFunctionHandle);
+ this._currentStepFunctionHandle = -1;
+ }
+
+ if (this._spinningByMouse || this.state.keyboardSpinDirection !== KeyboardSpinDirection.notSpinning) {
+ this._spinningByMouse = false;
+ this.setState({ keyboardSpinDirection: KeyboardSpinDirection.notSpinning });
+ }
+ }
+
+ /**
+ * 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
+ */
+ @autobind
+ private _handleKeyDown(event: React.KeyboardEvent) {
+ 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;
+ }
+
+ let spinDirection = KeyboardSpinDirection.notSpinning;
+
+ if (event.which === KeyCodes.up) {
+
+ spinDirection = KeyboardSpinDirection.up;
+ this._updateValue(false /* shouldSpin */, this._onIncrement);
+ } else if (event.which === KeyCodes.down) {
+
+ spinDirection = KeyboardSpinDirection.down;
+ this._updateValue(false /* shouldSpin */, this._onDecrement);
+ } else if (event.which === KeyCodes.enter) {
+ event.currentTarget.blur();
+ this.focus();
+ } else if (event.which === KeyCodes.escape) {
+ if (this.state.value !== this._lastValidValue) {
+ this.setState({ value: this._lastValidValue });
+ }
+ }
+
+ // style the increment/decrement button to look active
+ // when the corresponding up/down arrow keys trigger a step
+ if (this.state.keyboardSpinDirection !== spinDirection) {
+ this.setState({ keyboardSpinDirection: spinDirection });
+ }
+ }
+
+ /**
+ * Make sure that we have stopped spinning on keyUp
+ * if the up or down arrow fired this event
+ * @param event stop spinning if we
+ */
+ @autobind
+ private _handleKeyUp(event: React.KeyboardEvent) {
+
+ if (this.props.disabled || event.which === KeyCodes.up || event.which === KeyCodes.down) {
+ this._stop();
+ return;
+ }
+ }
+
+ @autobind
+ private _onIncrementMouseDown() {
+ this._updateValue(true /* shouldSpin */, this._onIncrement);
+ }
+
+ @autobind
+ private _onDecrementMouseDown() {
+ this._updateValue(true /* shouldSpin */, this._onDecrement);
+ }
+
+}
\ No newline at end of file
diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/SpinButtonPage.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButtonPage.tsx
new file mode 100644
index 0000000000000..37fd9c958c51a
--- /dev/null
+++ b/packages/office-ui-fabric-react/src/components/SpinButton/SpinButtonPage.tsx
@@ -0,0 +1,97 @@
+import * as React from 'react';
+import {
+ ExampleCard,
+ IComponentDemoPageProps,
+ ComponentPage,
+ 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('!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() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ propertiesTables={
+ ('!raw-loader!office-ui-fabric-react/src/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/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
new file mode 100644
index 0000000000000..4999ef440bc24
--- /dev/null
+++ b/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.Basic.Example.tsx
@@ -0,0 +1,18 @@
+import * as React from 'react';
+import { SpinButton } from 'office-ui-fabric-react/lib/SpinButton';
+
+export class SpinButtonBasicExample extends React.Component {
+ public render() {
+ return (
+
+
+
+ );
+ }
+}
diff --git a/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.BasicDisabled.Example.tsx b/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.BasicDisabled.Example.tsx
new file mode 100644
index 0000000000000..b57b1d432db08
--- /dev/null
+++ b/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.BasicDisabled.Example.tsx
@@ -0,0 +1,19 @@
+import * as React from 'react';
+import { SpinButton } from 'office-ui-fabric-react/lib/SpinButton';
+
+export class SpinButtonBasicDisabledExample extends React.Component {
+ 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..31d1d77b14ab5
--- /dev/null
+++ b/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.BasicWithEndPosition.Example.tsx
@@ -0,0 +1,21 @@
+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..056ba07096ba3
--- /dev/null
+++ b/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.BasicWithIcon.Example.tsx
@@ -0,0 +1,19 @@
+import * as React from 'react';
+import { SpinButton } from 'office-ui-fabric-react/lib/SpinButton';
+
+export class SpinButtonBasicWithIconExample extends React.Component {
+ public render() {
+ return (
+
+
+
+ );
+ }
+}
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
new file mode 100644
index 0000000000000..205ddc1370100
--- /dev/null
+++ b/packages/office-ui-fabric-react/src/components/SpinButton/examples/SpinButton.Stateful.Example.tsx
@@ -0,0 +1,46 @@
+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';
+
+ return (
+