diff --git a/common/changes/office-ui-fabric-react/keco-ms-slider-pt1_2018-05-07-19-11.json b/common/changes/office-ui-fabric-react/keco-ms-slider-pt1_2018-05-07-19-11.json new file mode 100644 index 00000000000000..030a518696ef4d --- /dev/null +++ b/common/changes/office-ui-fabric-react/keco-ms-slider-pt1_2018-05-07-19-11.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "office-ui-fabric-react", + "comment": "Part 1 of converting Slider to mergeStyles", + "type": "minor" + } + ], + "packageName": "office-ui-fabric-react", + "email": "keco@microsoft.com" +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/Slider/Slider.base.tsx b/packages/office-ui-fabric-react/src/components/Slider/Slider.base.tsx new file mode 100644 index 00000000000000..7eb926c6b39b0e --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Slider/Slider.base.tsx @@ -0,0 +1,309 @@ +import * as React from 'react'; +import { + BaseComponent, + KeyCodes, + css, + getId, + getRTL, + getRTLSafeKeyCode, + createRef +} from '../../Utilities'; +import { ISliderProps, ISlider } from './Slider.types'; +import { Label } from '../../Label'; +import * as stylesImport from './Slider.scss'; +const styles: any = stylesImport; + +export interface ISliderState { + value?: number; + renderedValue?: number; +} + +/** + * @deprecated Unused. +*/ +export enum ValuePosition { + Previous = 0, + Next = 1 +} + +export class SliderBase extends BaseComponent implements ISlider { + public static defaultProps: ISliderProps = { + step: 1, + min: 0, + max: 10, + showValue: true, + disabled: false, + vertical: false, + buttonProps: {} + }; + + private _sliderLine = createRef(); + private _thumb = createRef(); + private _id: string; + + constructor(props: ISliderProps) { + super(props); + + this._warnMutuallyExclusive({ + 'value': 'defaultValue' + }); + + this._id = getId('Slider'); + + const value = props.value || props.defaultValue || props.min; + + this.state = { + value: value, + renderedValue: value + }; + } + + /** + * Invoked when a component is receiving new props. This method is not called for the initial render. + */ + public componentWillReceiveProps(newProps: ISliderProps): void { + + if (newProps.value !== undefined) { + const value = Math.max(newProps.min as number, Math.min(newProps.max as number, newProps.value)); + + this.setState({ + value: value, + renderedValue: value + }); + } + } + + public render(): React.ReactElement<{}> { + const { + ariaLabel, + className, + disabled, + label, + max, + min, + showValue, + buttonProps, + vertical + } = this.props; + const { value, renderedValue } = this.state; + const thumbOffsetPercent: number = (renderedValue! - min!) / (max! - min!) * 100; + const lengthString = vertical ? 'height' : 'width'; + const onMouseDownProp: {} = disabled ? {} : { onMouseDown: this._onMouseDownOrTouchStart }; + const onTouchStartProp: {} = disabled ? {} : { onTouchStart: this._onMouseDownOrTouchStart }; + const onKeyDownProp: {} = disabled ? {} : { onKeyDown: this._onKeyDown }; + + return ( +
+ { label && ( + + ) } +
+ + { showValue && } +
+
+ ) as React.ReactElement<{}>; + } + public focus(): void { + if (this._thumb.current) { + this._thumb.current.focus(); + } + } + + public get value(): number | undefined { + return this.state.value; + } + + private _getAriaValueText = (value: number | undefined): string | undefined => { + if (this.props.ariaValueText && value !== undefined) { + return this.props.ariaValueText(value); + } + } + + private _getThumbStyle(vertical: boolean | undefined, thumbOffsetPercent: number): any { + const direction: string = vertical ? 'bottom' : (getRTL() ? 'right' : 'left'); + return { + [direction]: thumbOffsetPercent + '%' + }; + } + + private _onMouseDownOrTouchStart = (event: MouseEvent | TouchEvent): void => { + if (event.type === 'mousedown') { + this._events.on(window, 'mousemove', this._onMouseMoveOrTouchMove, true); + this._events.on(window, 'mouseup', this._onMouseUpOrTouchEnd, true); + } else if (event.type === 'touchstart') { + this._events.on(window, 'touchmove', this._onMouseMoveOrTouchMove, true); + this._events.on(window, 'touchend', this._onMouseUpOrTouchEnd, true); + } + this._onMouseMoveOrTouchMove(event, true); + } + + private _onMouseMoveOrTouchMove = (event: MouseEvent | TouchEvent, suppressEventCancelation?: boolean): void => { + if (!this._sliderLine.current) { + return; + } + + const { max, min, step } = this.props; + const steps: number = (max! - min!) / step!; + const sliderPositionRect: ClientRect = this._sliderLine.current.getBoundingClientRect(); + const sliderLength: number = !this.props.vertical ? sliderPositionRect.width : sliderPositionRect.height; + const stepLength: number = sliderLength / steps; + let currentSteps: number | undefined; + let distance: number | undefined; + + if (!this.props.vertical) { + const left: number | undefined = this._getPosition(event, this.props.vertical); + distance = getRTL() ? sliderPositionRect.right - left! : left! - sliderPositionRect.left; + currentSteps = distance / stepLength; + } else { + const bottom: number | undefined = this._getPosition(event, this.props.vertical); + distance = sliderPositionRect.bottom - bottom!; + currentSteps = distance / stepLength; + } + + let currentValue: number | undefined; + let renderedValue: number | undefined; + + // The value shouldn't be bigger than max or be smaller than min. + if (currentSteps! > Math.floor(steps)) { + renderedValue = currentValue = max as number; + } else if (currentSteps! < 0) { + renderedValue = currentValue = min as number; + } else { + renderedValue = min! + step! * currentSteps!; + currentValue = min! + step! * Math.round(currentSteps!); + } + + this._updateValue(currentValue, renderedValue); + + if (!suppressEventCancelation) { + event.preventDefault(); + event.stopPropagation(); + } + } + + private _getPosition(event: MouseEvent | TouchEvent, vertical: boolean | undefined): number | undefined { + let currentPosition: number | undefined; + switch (event.type) { + case 'mousedown': + case 'mousemove': + currentPosition = !vertical ? (event as MouseEvent).clientX : (event as MouseEvent).clientY; + break; + case 'touchstart': + case 'touchmove': + currentPosition = !vertical ? (event as TouchEvent).touches[0].clientX : (event as TouchEvent).touches[0].clientY; + break; + } + return currentPosition; + } + private _updateValue(value: number, renderedValue: number): void { + const interval: number = 1.0 / this.props.step!; + // Make sure value has correct number of decimal places based on steps without JS's floating point issues + const roundedValue: number = Math.round(value * interval) / interval; + + const valueChanged = roundedValue !== this.state.value; + + this.setState({ + value: roundedValue, + renderedValue + }, () => { + if (valueChanged && this.props.onChange) { + this.props.onChange(this.state.value as number); + } + }); + } + + private _onMouseUpOrTouchEnd = (): void => { + // Synchronize the renderedValue to the actual value. + this.setState({ + renderedValue: this.state.value + }); + + this._events.off(); + } + + private _onKeyDown = (event: KeyboardEvent): void => { + let value: number | undefined = this.state.value; + const { max, min, step } = this.props; + + let diff: number | undefined = 0; + + switch (event.which) { + case getRTLSafeKeyCode(KeyCodes.left): + case KeyCodes.down: + diff = -(step as number); + break; + case getRTLSafeKeyCode(KeyCodes.right): + case KeyCodes.up: + diff = step; + break; + + case KeyCodes.home: + value = min; + break; + + case KeyCodes.end: + value = max; + break; + + default: + return; + } + + const newValue: number = Math.min(max as number, Math.max(min as number, value! + diff!)); + + this._updateValue(newValue, newValue); + + event.preventDefault(); + event.stopPropagation(); + } +} diff --git a/packages/office-ui-fabric-react/src/components/Slider/Slider.styles.ts b/packages/office-ui-fabric-react/src/components/Slider/Slider.styles.ts new file mode 100644 index 00000000000000..1022dd49e95133 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Slider/Slider.styles.ts @@ -0,0 +1,58 @@ +import { ISliderStyleProps, ISliderStyles } from './Slider.types'; +import { getGlobalClassNames } from '../..'; + +const GlobalClassNames = { + root: 'ms-Slider', + container: 'ms-Slider-container', + slideBox: 'ms-Slider-slideBox', + line: 'ms-Slider-line', + thumb: 'ms-Slider-thumb', + activeSection: 'ms-Slider-active', + inactiveSection: 'ms-Slider-inactive', + valueLabel: 'ms-Slider-value' +}; + +export const getStyles = (props: ISliderStyleProps): ISliderStyles => { + + const { className, theme } = props; + const classNames = getGlobalClassNames(GlobalClassNames, theme); + + return ({ + root: [ + classNames.root, + {}, + className + ], + container: [ + classNames.container, + {} + ], + slideBox: [ + classNames.slideBox, + {} + ], + line: [ + classNames.line, + {} + ], + thumb: [ + classNames.thumb, + {} + ], + lineContainer: [ + {} + ], + activeSection: [ + classNames.activeSection, + {} + ], + inactiveSection: [ + classNames.inactiveSection, + {} + ], + valueLabel: [ + classNames.valueLabel, + {} + ] + }); +}; diff --git a/packages/office-ui-fabric-react/src/components/Slider/Slider.test.tsx b/packages/office-ui-fabric-react/src/components/Slider/Slider.test.tsx index a0a09e9dc38d5c..0a9e1439470afb 100644 --- a/packages/office-ui-fabric-react/src/components/Slider/Slider.test.tsx +++ b/packages/office-ui-fabric-react/src/components/Slider/Slider.test.tsx @@ -5,6 +5,8 @@ import * as ReactTestUtils from 'react-dom/test-utils'; import * as renderer from 'react-test-renderer'; import { Slider } from './Slider'; +import { SliderBase } from './Slider.base'; +import { getStyles } from './Slider.styles'; import { ISlider } from './Slider.types'; describe('Slider', () => { @@ -16,8 +18,8 @@ describe('Slider', () => { }); it('renders a slider', () => { - const component = ReactTestUtils.renderIntoDocument( - + const component = ReactTestUtils.renderIntoDocument( + ); const renderedDOM = ReactDOM.findDOMNode(component as React.ReactInstance) as Element; const labelElement = renderedDOM.querySelector('.ms-Label') as HTMLElement; @@ -30,8 +32,9 @@ describe('Slider', () => { const onChange = (val: any) => { changedValue = val; }; - const component = ReactTestUtils.renderIntoDocument( - ( + ); @@ -69,8 +72,8 @@ describe('Slider', () => { }); it('has type=button on all buttons', () => { - const component = ReactTestUtils.renderIntoDocument( - + const component = ReactTestUtils.renderIntoDocument( + ); const renderedDOM = ReactDOM.findDOMNode(component as React.ReactInstance) as Element; @@ -86,16 +89,16 @@ describe('Slider', () => { it('can read the current value', () => { let slider: ISlider | null; - ReactTestUtils.renderIntoDocument( + ReactTestUtils.renderIntoDocument( // tslint:disable-next-line:jsx-no-lambda - slider = s } /> + slider = s } /> ); expect(slider!.value).toEqual(12); }); it('renders correct aria-valuetext', () => { - let component = ReactTestUtils.renderIntoDocument( - + let component = ReactTestUtils.renderIntoDocument( + ); let renderedDOM = ReactDOM.findDOMNode(component as React.ReactInstance) as Element; let button = renderedDOM.querySelector('.ms-Slider-slideBox') as HTMLElement; @@ -107,8 +110,9 @@ describe('Slider', () => { const selected = 1; const getTextValue = (value: number) => values[value]; - component = ReactTestUtils.renderIntoDocument( - ( + 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 680f95319a8f9d..3fb8ee1dd11461 100644 --- a/packages/office-ui-fabric-react/src/components/Slider/Slider.tsx +++ b/packages/office-ui-fabric-react/src/components/Slider/Slider.tsx @@ -1,309 +1,15 @@ -import * as React from 'react'; -import { - BaseComponent, - KeyCodes, - css, - getId, - getRTL, - getRTLSafeKeyCode, - createRef -} from '../../Utilities'; -import { ISliderProps, ISlider } from './Slider.types'; -import { Label } from '../../Label'; -import * as stylesImport from './Slider.scss'; -const styles: any = stylesImport; - -export interface ISliderState { - value?: number; - renderedValue?: number; -} - -/** - * @deprecated Unused. Do not use, will be removed in 6.0 -*/ -export enum ValuePosition { - Previous = 0, - Next = 1 -} - -export class Slider extends BaseComponent implements ISlider { - public static defaultProps: {} = { - step: 1, - min: 0, - max: 10, - showValue: true, - disabled: false, - vertical: false, - buttonProps: {} - }; - - private _sliderLine = createRef(); - private _thumb = createRef(); - private _id: string; - - constructor(props: ISliderProps) { - super(props); - - this._warnMutuallyExclusive({ - 'value': 'defaultValue' - }); - - this._id = getId('Slider'); - - const value = props.value || props.defaultValue || props.min; - - this.state = { - value: value, - renderedValue: value - }; - } - - /** - * Invoked when a component is receiving new props. This method is not called for the initial render. - */ - public componentWillReceiveProps(newProps: ISliderProps): void { - - if (newProps.value !== undefined) { - const value = Math.max(newProps.min as number, Math.min(newProps.max as number, newProps.value)); - - this.setState({ - value: value, - renderedValue: value - }); - } - } - - public render(): React.ReactElement<{}> { - const { - ariaLabel, - className, - disabled, - label, - max, - min, - showValue, - buttonProps, - vertical - } = this.props; - const { value, renderedValue } = this.state; - const thumbOffsetPercent: number = (renderedValue! - min!) / (max! - min!) * 100; - const lengthString = vertical ? 'height' : 'width'; - const onMouseDownProp: {} = disabled ? {} : { onMouseDown: this._onMouseDownOrTouchStart }; - const onTouchStartProp: {} = disabled ? {} : { onTouchStart: this._onMouseDownOrTouchStart }; - const onKeyDownProp: {} = disabled ? {} : { onKeyDown: this._onKeyDown }; - - return ( -
- { label && ( - - ) } -
- - { showValue && } -
-
- ) as React.ReactElement<{}>; - } - public focus(): void { - if (this._thumb.current) { - this._thumb.current.focus(); - } - } - - public get value(): number | undefined { - return this.state.value; - } - - private _getAriaValueText = (value: number | undefined): string | undefined => { - if (this.props.ariaValueText && value !== undefined) { - return this.props.ariaValueText(value); - } - } - - private _getThumbStyle(vertical: boolean | undefined, thumbOffsetPercent: number): any { - const direction: string = vertical ? 'bottom' : (getRTL() ? 'right' : 'left'); - return { - [direction]: thumbOffsetPercent + '%' - }; - } - - private _onMouseDownOrTouchStart = (event: MouseEvent | TouchEvent): void => { - if (event.type === 'mousedown') { - this._events.on(window, 'mousemove', this._onMouseMoveOrTouchMove, true); - this._events.on(window, 'mouseup', this._onMouseUpOrTouchEnd, true); - } else if (event.type === 'touchstart') { - this._events.on(window, 'touchmove', this._onMouseMoveOrTouchMove, true); - this._events.on(window, 'touchend', this._onMouseUpOrTouchEnd, true); - } - this._onMouseMoveOrTouchMove(event, true); - } - - private _onMouseMoveOrTouchMove = (event: MouseEvent | TouchEvent, suppressEventCancelation?: boolean): void => { - if (!this._sliderLine.current) { - return; - } +import { styled } from '../../Utilities'; - const { max, min, step } = this.props; - const steps: number = (max! - min!) / step!; - const sliderPositionRect: ClientRect = this._sliderLine.current.getBoundingClientRect(); - const sliderLength: number = !this.props.vertical ? sliderPositionRect.width : sliderPositionRect.height; - const stepLength: number = sliderLength / steps; - let currentSteps: number | undefined; - let distance: number | undefined; - - if (!this.props.vertical) { - const left: number | undefined = this._getPosition(event, this.props.vertical); - distance = getRTL() ? sliderPositionRect.right - left! : left! - sliderPositionRect.left; - currentSteps = distance / stepLength; - } else { - const bottom: number | undefined = this._getPosition(event, this.props.vertical); - distance = sliderPositionRect.bottom - bottom!; - currentSteps = distance / stepLength; - } - - let currentValue: number | undefined; - let renderedValue: number | undefined; - - // The value shouldn't be bigger than max or be smaller than min. - if (currentSteps! > Math.floor(steps)) { - renderedValue = currentValue = max as number; - } else if (currentSteps! < 0) { - renderedValue = currentValue = min as number; - } else { - renderedValue = min! + step! * currentSteps!; - currentValue = min! + step! * Math.round(currentSteps!); - } - - this._updateValue(currentValue, renderedValue); - - if (!suppressEventCancelation) { - event.preventDefault(); - event.stopPropagation(); - } - } - - private _getPosition(event: MouseEvent | TouchEvent, vertical: boolean | undefined): number | undefined { - let currentPosition: number | undefined; - switch (event.type) { - case 'mousedown': - case 'mousemove': - currentPosition = !vertical ? (event as MouseEvent).clientX : (event as MouseEvent).clientY; - break; - case 'touchstart': - case 'touchmove': - currentPosition = !vertical ? (event as TouchEvent).touches[0].clientX : (event as TouchEvent).touches[0].clientY; - break; - } - return currentPosition; - } - private _updateValue(value: number, renderedValue: number): void { - const interval: number = 1.0 / this.props.step!; - // Make sure value has correct number of decimal places based on steps without JS's floating point issues - const roundedValue: number = Math.round(value * interval) / interval; - - const valueChanged = roundedValue !== this.state.value; - - this.setState({ - value: roundedValue, - renderedValue - }, () => { - if (valueChanged && this.props.onChange) { - this.props.onChange(this.state.value as number); - } - }); - } - - private _onMouseUpOrTouchEnd = (): void => { - // Synchronize the renderedValue to the actual value. - this.setState({ - renderedValue: this.state.value - }); - - this._events.off(); - } - - private _onKeyDown = (event: KeyboardEvent): void => { - let value: number | undefined = this.state.value; - const { max, min, step } = this.props; - - let diff: number | undefined = 0; - - switch (event.which) { - case getRTLSafeKeyCode(KeyCodes.left): - case KeyCodes.down: - diff = -(step as number); - break; - case getRTLSafeKeyCode(KeyCodes.right): - case KeyCodes.up: - diff = step; - break; - - case KeyCodes.home: - value = min; - break; - - case KeyCodes.end: - value = max; - break; - - default: - return; - } - - const newValue: number = Math.min(max as number, Math.max(min as number, value! + diff!)); - - this._updateValue(newValue, newValue); - - event.preventDefault(); - event.stopPropagation(); - } -} +import { + ISliderProps, + ISliderStyleProps, + ISliderStyles +} from './Slider.types'; + +import { SliderBase } from './Slider.base'; +import { getStyles } from './Slider.styles'; + +export const Slider = styled( + SliderBase, + getStyles +); \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/Slider/Slider.types.ts b/packages/office-ui-fabric-react/src/components/Slider/Slider.types.ts index c87a716ba15f00..8e9b131fd1b997 100644 --- a/packages/office-ui-fabric-react/src/components/Slider/Slider.types.ts +++ b/packages/office-ui-fabric-react/src/components/Slider/Slider.types.ts @@ -1,4 +1,7 @@ import * as React from 'react'; +import { SliderBase } from './Slider.base'; +import { IStyle, ITheme } from '../../Styling'; +import { IStyleFunction } from '../../Utilities'; export interface ISlider { value: number | undefined; @@ -6,13 +9,23 @@ export interface ISlider { focus: () => void; } -export interface ISliderProps { +export interface ISliderProps extends React.Props { /** * Optional callback to access the ISlider interface. Use this instead of ref for accessing * the public methods and properties of the component. */ componentRef?: (component: ISlider | null) => void; + /** + * Call to provide customized styling that will layer on top of the variant rules. + */ + getStyles?: IStyleFunction; + + /** + * Theme provided by High-Order Component. + */ + theme?: ITheme; + /** * Description label of the Slider */ @@ -88,3 +101,32 @@ export interface ISliderProps { */ buttonProps?: React.HTMLAttributes; } + +export interface ISliderStyleProps { + /** + * Theme provided by High-Order Component. + */ + theme: ITheme; + /** + * Accept custom classNames. + */ + className?: string; + titleLabel?: string; + rootIsEnabled?: boolean; + rootIsDisabled?: boolean; + rootIsHorizontal?: boolean; + rootIsVertical?: boolean; + showTransitions?: boolean; +} + +export interface ISliderStyles { + root: IStyle; + container: IStyle; + slideBox: IStyle; + line: IStyle; + thumb: IStyle; + lineContainer: IStyle; + activeSection: IStyle; + inactiveSection: IStyle; + valueLabel: IStyle; +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/Slider/__snapshots__/Slider.test.tsx.snap b/packages/office-ui-fabric-react/src/components/Slider/__snapshots__/Slider.test.tsx.snap index a0a17a02b7dd28..b160919ae6f511 100644 --- a/packages/office-ui-fabric-react/src/components/Slider/__snapshots__/Slider.test.tsx.snap +++ b/packages/office-ui-fabric-react/src/components/Slider/__snapshots__/Slider.test.tsx.snap @@ -15,7 +15,7 @@ exports[`Slider renders Slider correctly 1`] = ` aria-valuetext={undefined} className="ms-Slider-slideBox ms-Slider-showValue ms-Slider-showTransitions undefined" disabled={false} - id="Slider0" + id="Slider1" onKeyDown={[Function]} onMouseDown={[Function]} onTouchStart={[Function]} diff --git a/packages/office-ui-fabric-react/src/components/Slider/index.ts b/packages/office-ui-fabric-react/src/components/Slider/index.ts index 1651b69e2cfb2e..fd07578506773a 100644 --- a/packages/office-ui-fabric-react/src/components/Slider/index.ts +++ b/packages/office-ui-fabric-react/src/components/Slider/index.ts @@ -1,2 +1,3 @@ export * from './Slider'; +export * from './Slider.base'; export * from './Slider.types'; \ No newline at end of file