All Downloads are FREE. Search and download functionalities are using the official Maven repository.

package.src.components.Slider.Slider.tsx Maven / Gradle / Ivy

Go to download

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';




© 2015 - 2024 Weber Informatics LLC | Privacy Policy