Skip to content

Commit

Permalink
feat(slider): adds slider component
Browse files Browse the repository at this point in the history
  • Loading branch information
riyalohia committed Aug 21, 2020
1 parent 6ff117f commit 3a7b0bd
Show file tree
Hide file tree
Showing 27 changed files with 1,510 additions and 0 deletions.
190 changes: 190 additions & 0 deletions core/components/atoms/multiSlider/Handle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import classNames from 'classnames';
import { Tooltip } from '@/index';
import * as React from 'react';
import * as Keys from '@/utils/Keys';
import { formatPercentage, clamp } from './SliderUtils';

export interface HandleProps {
value: number;
fillAfter?: boolean;
fillBefore?: boolean;
onChange?: (newValue: number) => void;
onRelease?: (newValue: number) => void;
}

export interface InternalHandleProps extends HandleProps {
disabled?: boolean;
label: string;
max: number;
min: number;
stepSize: number;
tickSize: number;
tickSizeRatio: number;
zIndex?: number;
}

export interface HandleState {
isMoving?: boolean;
}

export class Handle extends React.Component<InternalHandleProps, HandleState> {
state = {
isMoving: false,
};

handleElement: HTMLElement | null = null;
refHandlers = {
handle: (el: HTMLDivElement) => (this.handleElement = el),
};

componentWillUnmount() {
this.removeDocumentEventListeners();
}

componentDidUpdate(_prevProps: InternalHandleProps, prevState: HandleState) {
if (prevState.isMoving !== this.state.isMoving) {
if (this.handleElement) this.handleElement.focus();
}
}

mouseEventClientOffset = (event: MouseEvent | React.MouseEvent<HTMLElement>) => {
return event.clientX;
}

clientToValue = (clientPixel: number) => {
const { stepSize, tickSize, value } = this.props;
if (this.handleElement == null) {
return value;
}

const clientPixelNormalized = clientPixel;
const { handleMidpoint, handleOffset } = this.getHandleMidpointAndOffset(this.handleElement);
const handleCenterPixel = handleMidpoint + handleOffset;
const pixelDelta = clientPixelNormalized - handleCenterPixel;

if (isNaN(pixelDelta)) {
return value;
}

return value + Math.round(pixelDelta / (tickSize * stepSize)) * stepSize;
}

changeValue = (newValue: number, callback = this.props.onChange) => {
const updatedValue = clamp(newValue, this.props.min, this.props.max);

if (!isNaN(updatedValue) && this.props.value !== updatedValue) {
if (callback) callback(updatedValue);
}
return updatedValue;
}

endHandleMovement = (event: MouseEvent) => {
const clientPixel = this.mouseEventClientOffset(event);
const { onRelease } = this.props;

this.removeDocumentEventListeners();
this.setState({ isMoving: false });

const finalValue = this.changeValue(this.clientToValue(clientPixel));
if (onRelease) onRelease(finalValue);
}

continueHandleMovement = (event: MouseEvent) => {
const clientPixel = this.mouseEventClientOffset(event);
if (this.state.isMoving && !this.props.disabled) {
const value = this.clientToValue(clientPixel);
this.changeValue(value);
}
}

beginHandleMovement = (event: MouseEvent | React.MouseEvent<HTMLElement>) => {
if (this.props.disabled) return;
document.addEventListener('mousemove', this.continueHandleMovement);
document.addEventListener('mouseup', this.endHandleMovement);

this.setState({ isMoving: true });

const value = this.clientToValue(event.clientX);
this.changeValue(value);
}

handleKeyDown = (event: React.KeyboardEvent<HTMLSpanElement>) => {
if (this.props.disabled) return;

const { stepSize, value } = this.props;
const { which } = event;

if (which === Keys.ARROW_LEFT) {
this.changeValue(value - stepSize);
event.preventDefault();
} else if (which === Keys.ARROW_RIGHT) {
this.changeValue(value + stepSize);
event.preventDefault();
}
}

handleKeyUp = (event: React.KeyboardEvent<HTMLSpanElement>) => {
if (this.props.disabled) return;

if ([Keys.ARROW_LEFT, Keys.ARROW_RIGHT].indexOf(event.which) >= 0) {
const { onRelease } = this.props;
if (onRelease) onRelease(this.props.value);
}
}

getHandleMidpointAndOffset = (handleElement: HTMLElement | null, useOppositeDimension = false) => {
if (handleElement == null) {
return { handleMidpoint: 0, handleOffset: 0 };
}

const handleRect = handleElement.getBoundingClientRect();
const sizeKey = useOppositeDimension ? 'height' : 'width';
const handleOffset = handleRect.left;

return { handleOffset, handleMidpoint: handleRect[sizeKey] / 2 };
}

render() {
const { min, tickSizeRatio, value, disabled, label } = this.props;

const { handleMidpoint } = this.getHandleMidpointAndOffset(this.handleElement, true);
const offsetRatio = (value - min) * tickSizeRatio;
const offsetCalc = `calc(${formatPercentage(offsetRatio)} - ${handleMidpoint}px)`;
const style = { left: offsetCalc };

const className = classNames({
['Slider-handle']: true,
['Slider-handle--disabled']: disabled,
['Slider-handle--active']: this.state.isMoving
});

return (
<div
className={className}
onMouseDown={this.beginHandleMovement}
onKeyDown={this.handleKeyDown}
onKeyUp={this.handleKeyUp}
ref={this.refHandlers.handle}
style={style}
tabIndex={1}
>
{!this.state.isMoving && (
<Tooltip
tooltip={label}
position="top"
triggerClass={'Slider-tooltip'}
>
<span className="h-100 w-100" />
</Tooltip>
)}
</div>
);
}

removeDocumentEventListeners = () => {
document.removeEventListener('mousemove', this.continueHandleMovement);
document.removeEventListener('mouseup', this.endHandleMovement);
}
}

export default Handle;
72 changes: 72 additions & 0 deletions core/components/atoms/multiSlider/SliderUtils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
export const formatPercentage = (ratio: number) => {
return `${(ratio * 100).toFixed(2)}%`;
};

export const countDecimalPlaces = (value: number) => {
if (!isFinite(value)) return 0;

if (Math.floor(value) !== value) {
const valueArray = value.toString().split('.');
return valueArray[1].length || 0;
}

return 0;
};

export const approxEqual = (a: number, b: number) => {
const tolerance = 0.00001;
return Math.abs(a - b) <= tolerance;
};

export const clamp = (value: number, min: number, max: number) => {
if (value == null) {
return value;
}

return Math.min(Math.max(value, min), max);
};

export const arraysEqual = (oldValues: number[], newValues: number[]) => {

if (oldValues.length !== oldValues.length) return;

return newValues.every((value, index) => value === oldValues[index]);
};

export function argMin<T>(values: T[], argFn: (value: T) => any): T | undefined {
if (values.length === 0) {
return undefined;
}

let minValue = values[0];
let minArg = argFn(minValue);

for (let index = 1; index < values.length; index++) {
const value = values[index];
const arg = argFn(value);
if (arg < minArg) {
minValue = value;
minArg = arg;
}
}

return minValue;
}

export function fillValues<T>(values: T[], startIndex: number, endIndex: number, fillValue: T) {
const inc = startIndex < endIndex ? 1 : -1;
for (let index = startIndex; index !== endIndex + inc; index += inc) {
values[index] = fillValue;
}

}

export function isElementOfType<P = {}>(
element: any,
_ComponentType: React.ComponentType<P>,
): element is React.ReactElement<P> {
return (
element != null &&
element.type != null
);
}
Loading

0 comments on commit 3a7b0bd

Please sign in to comment.