package.src.components.Slider.Slider.tsx Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of react-core Show documentation
Show all versions of react-core Show documentation
This library provides a set of common React components for use with the PatternFly reference implementation.
The newest version!
import * as React from 'react';
import { useState } from 'react';
import styles from '@patternfly/react-styles/css/components/Slider/slider';
import { css } from '@patternfly/react-styles';
import { SliderStep } from './SliderStep';
import { InputGroup, InputGroupText, InputGroupItem } from '../InputGroup';
import { TextInput } from '../TextInput';
import { Tooltip } from '../Tooltip';
import cssSliderValue from '@patternfly/react-tokens/dist/esm/c_slider_value';
import cssFormControlWidthChars from '@patternfly/react-tokens/dist/esm/c_slider__value_c_form_control_width_chars';
import { getLanguageDirection } from '../../helpers/util';
/** Properties for creating custom steps in a slider. These properties should be passed in as
* an object within an array to the slider component's customSteps property.
*/
export interface SliderStepObject {
/** Flag to hide the label. */
isLabelHidden?: boolean;
/** The display label for the step value. This is also used for the aria-valuetext attribute. */
label: string;
/** Value of the step. This value is a percentage of the slider where the tick is drawn. */
value: number;
}
export type SliderOnChangeEvent =
| React.MouseEvent
| React.KeyboardEvent
| React.FormEvent
| React.TouchEvent
| React.FocusEvent;
/** The main slider component. */
export interface SliderProps extends Omit, 'onChange'> {
/** Flag indicating if the slider is discrete for custom steps. This will cause the slider
* to snap to the closest value.
*/
areCustomStepsContinuous?: boolean;
/** One or more id's to use for the slider thumb's accessible description. */
'aria-describedby'?: string;
/** One or more id's to use for the slider thumb's accessible label. */
'aria-labelledby'?: string;
/** Additional classes added to the slider. */
className?: string;
/** Array of custom slider step objects (value and label of each step) for the slider. */
customSteps?: SliderStepObject[];
/* Adds a tooltip over the slider thumb containing the current value. */
hasTooltipOverThumb?: boolean;
/** Accessible label for the input field. */
inputAriaLabel?: string;
/** Text label that is place after the input field. */
inputLabel?: string | number;
/** Position of the input. Note "right" is deprecated. Use "end" instead*/
inputPosition?: 'aboveThumb' | 'right' | 'end';
/** Value displayed in the input field. */
inputValue?: number;
/** Adds disabled styling, and disables the slider and the input component if present. */
isDisabled?: boolean;
/** Flag to show value input field. */
isInputVisible?: boolean;
/** @deprecated Use startActions instead. Actions placed at the start of the slider. */
leftActions?: React.ReactNode;
/** Actions placed at the start of the slider. */
startActions?: React.ReactNode;
/** The maximum permitted value. */
max?: number;
/** The minimum permitted value. */
min?: number;
/** Value change callback. This is called when the slider value changes. */
onChange?: (
event: SliderOnChangeEvent,
value: number,
inputValue?: number,
setLocalInputValue?: React.Dispatch>
) => void;
/** @deprecated Use endActions instead. Actions placed to the right of the slider. */
rightActions?: React.ReactNode;
/** Actions placed at the end of the slider. */
endActions?: React.ReactNode;
/** Flag to indicate if boundaries should be shown for slider that does not have custom steps. */
showBoundaries?: boolean;
/** Flag to indicate if ticks should be shown for slider that does not have custom steps. */
showTicks?: boolean;
/** The step interval. */
step?: number;
/* Accessible label for the slider thumb. */
thumbAriaLabel?: string;
/** Current value of the slider. */
value?: number;
}
const getPercentage = (current: number, max: number) => (100 * current) / max;
export const Slider: React.FunctionComponent = ({
className,
value = 0,
customSteps,
areCustomStepsContinuous = false,
isDisabled = false,
isInputVisible = false,
inputValue = 0,
inputLabel,
inputAriaLabel = 'Slider value input',
thumbAriaLabel = 'Value',
hasTooltipOverThumb = false,
inputPosition = 'end',
onChange,
leftActions,
startActions,
rightActions,
endActions,
step = 1,
min = 0,
max = 100,
showTicks = false,
showBoundaries = true,
'aria-describedby': ariaDescribedby,
'aria-labelledby': ariaLabelledby,
...props
}: SliderProps) => {
const sliderRailRef = React.useRef();
const thumbRef = React.useRef();
const [localValue, setValue] = useState(value);
const [localInputValue, setLocalInputValue] = useState(inputValue);
let isRTL: boolean;
React.useEffect(() => {
isRTL = getLanguageDirection(sliderRailRef.current) === 'rtl';
});
React.useEffect(() => {
setValue(value);
}, [value]);
React.useEffect(() => {
setLocalInputValue(inputValue);
}, [inputValue]);
let diff = 0;
let snapValue: number;
// calculate style value percentage
const stylePercent = ((localValue - min) * 100) / (max - min);
const style = { [cssSliderValue.name]: `${stylePercent}%` } as React.CSSProperties;
const widthChars = React.useMemo(() => localInputValue.toString().length, [localInputValue]);
const inputStyle = { [cssFormControlWidthChars.name]: widthChars } as React.CSSProperties;
const onChangeHandler = (_event: React.FormEvent, value: string) => {
setLocalInputValue(Number(value));
};
const handleKeyPressOnInput = (event: React.KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault();
if (onChange) {
onChange(event, localValue, localInputValue, setLocalInputValue);
}
}
};
const onInputFocus = (e: any) => {
e.stopPropagation();
};
const onThumbClick = () => {
thumbRef.current.focus();
};
const onBlur = (event: React.FocusEvent) => {
if (onChange) {
onChange(event, localValue, localInputValue, setLocalInputValue);
}
};
const findAriaTextValue = () => {
if (!areCustomStepsContinuous && customSteps) {
const matchingStep = customSteps.find((stepObj) => stepObj.value === localValue);
if (matchingStep) {
return matchingStep.label;
}
}
// For continuous steps default to showing 2 decimals in tooltip
// Consider making it configurable via a property
return Number(Number(localValue).toFixed(2)).toString();
};
const handleThumbDragEnd = () => {
document.removeEventListener('mousemove', callbackThumbMove);
document.removeEventListener('mouseup', callbackThumbUp);
document.removeEventListener('touchmove', callbackThumbMove);
document.removeEventListener('touchend', callbackThumbUp);
document.removeEventListener('touchcancel', callbackThumbUp);
};
const handleMouseDown = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
if (isRTL) {
diff = thumbRef.current.getBoundingClientRect().right - e.clientX;
} else {
diff = e.clientX - thumbRef.current.getBoundingClientRect().left;
}
document.addEventListener('mousemove', callbackThumbMove);
document.addEventListener('mouseup', callbackThumbUp);
};
const handleTouchStart = (e: React.TouchEvent) => {
e.stopPropagation();
if (isRTL) {
diff = thumbRef.current.getBoundingClientRect().right - e.touches[0].clientX;
} else {
diff = e.touches[0].clientX - thumbRef.current.getBoundingClientRect().left;
}
document.addEventListener('touchmove', callbackThumbMove, { passive: false });
document.addEventListener('touchend', callbackThumbUp);
document.addEventListener('touchcancel', callbackThumbUp);
};
const onSliderRailClick = (e: any) => {
handleThumbMove(e);
if (snapValue && !areCustomStepsContinuous) {
thumbRef.current.style.setProperty(cssSliderValue.name, `${snapValue}%`);
setValue(snapValue);
if (onChange) {
onChange(e, snapValue);
}
}
};
const handleThumbMove = (e: any) => {
if (e.type === 'touchmove') {
e.preventDefault();
e.stopImmediatePropagation();
}
const clientPosition = e.touches && e.touches.length ? e.touches[0].clientX : e.clientX;
let newPosition;
if (isRTL) {
newPosition = sliderRailRef.current.getBoundingClientRect().right - clientPosition - diff;
} else {
newPosition = clientPosition - diff - sliderRailRef.current.getBoundingClientRect().left;
}
const end = sliderRailRef.current.offsetWidth - thumbRef.current.offsetWidth;
const start = 0;
if (newPosition < start) {
newPosition = 0;
}
if (newPosition > end) {
newPosition = end;
}
const newPercentage = getPercentage(newPosition, end);
thumbRef.current.style.setProperty(cssSliderValue.name, `${newPercentage}%`);
// convert percentage to value
const newValue = Math.round(((newPercentage * (max - min)) / 100 + min) * 100) / 100;
setValue(newValue);
if (!customSteps) {
// snap to new value if not custom steps
snapValue = Math.round((Math.round((newValue - min) / step) * step + min) * 100) / 100;
thumbRef.current.style.setProperty(cssSliderValue.name, `${snapValue}%`);
setValue(snapValue);
}
/* If custom steps are discrete, snap to closest step value */
if (!areCustomStepsContinuous && customSteps) {
let percentage = newPercentage;
if (customSteps[customSteps.length - 1].value !== 100) {
percentage = (newPercentage * (max - min)) / 100 + min;
}
const stepIndex = customSteps.findIndex((stepObj) => stepObj.value >= percentage);
if (customSteps[stepIndex].value === percentage) {
snapValue = customSteps[stepIndex].value;
} else {
const midpoint = (customSteps[stepIndex].value + customSteps[stepIndex - 1].value) / 2;
if (midpoint > percentage) {
snapValue = customSteps[stepIndex - 1].value;
} else {
snapValue = customSteps[stepIndex].value;
}
}
setValue(snapValue);
}
// Call onchange callback
if (onChange) {
if (snapValue !== undefined) {
onChange(e, snapValue);
} else {
onChange(e, newValue);
}
}
};
const callbackThumbMove = React.useCallback(handleThumbMove, [min, max, customSteps, onChange]);
const callbackThumbUp = React.useCallback(handleThumbDragEnd, [min, max, customSteps, onChange]);
const handleThumbKeys = (e: React.KeyboardEvent) => {
const key = e.key;
if (key !== 'ArrowLeft' && key !== 'ArrowRight') {
return;
}
e.preventDefault();
let newValue: number = localValue;
if (!areCustomStepsContinuous && customSteps) {
const stepIndex = customSteps.findIndex((stepObj) => stepObj.value === localValue);
if (key === 'ArrowRight') {
if (isRTL) {
if (stepIndex - 1 >= 0) {
newValue = customSteps[stepIndex - 1].value;
}
} else {
if (stepIndex + 1 < customSteps.length) {
{
newValue = customSteps[stepIndex + 1].value;
}
}
}
} else if (key === 'ArrowLeft') {
if (isRTL) {
if (stepIndex + 1 < customSteps.length) {
{
newValue = customSteps[stepIndex + 1].value;
}
}
} else {
if (stepIndex - 1 >= 0) {
newValue = customSteps[stepIndex - 1].value;
}
}
}
} else {
if (key === 'ArrowRight') {
if (isRTL) {
newValue = localValue - step >= min ? localValue - step : min;
} else {
newValue = localValue + step <= max ? localValue + step : max;
}
} else if (key === 'ArrowLeft') {
if (isRTL) {
newValue = localValue + step <= max ? localValue + step : max;
} else {
newValue = localValue - step >= min ? localValue - step : min;
}
}
}
if (newValue !== localValue) {
thumbRef.current.style.setProperty(cssSliderValue.name, `${newValue}%`);
setValue(newValue);
if (onChange) {
onChange(e, newValue);
}
}
};
const displayInput = () => {
const textInput = (
);
if (inputLabel) {
return (
{textInput}
{inputLabel}
);
} else {
return textInput;
}
};
const getStepValue = (val: number, min: number, max: number) => ((val - min) * 100) / (max - min);
const buildSteps = () => {
const builtSteps = [];
for (let i = min; i <= max; i = i + step) {
const stepValue = getStepValue(i, min, max);
// If boundaries but not ticks just generate the needed steps
// so that we don't pollute them DOM with empty divs
if (!showTicks && showBoundaries && i !== min && i !== max) {
continue;
}
builtSteps.push(
);
}
return builtSteps;
};
const thumbComponent = (
);
return (
{(leftActions || startActions) && {leftActions || startActions}}
{customSteps && (
)}
{!customSteps && (showTicks || showBoundaries) && (
)}
{hasTooltipOverThumb ? (
{thumbComponent}
) : (
thumbComponent
)}
{isInputVisible && inputPosition === 'aboveThumb' && (
{displayInput()}
)}
{isInputVisible && (inputPosition === 'right' || inputPosition === 'end') && (
{displayInput()}
)}
{(rightActions || endActions) && {rightActions || endActions}}
);
};
Slider.displayName = 'Slider';