index.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import {forwardRef as reactForwardRef, useEffect, useState} from 'react';
  2. import Input from 'sentry/components/input';
  3. import {t} from 'sentry/locale';
  4. import {defined} from 'sentry/utils';
  5. import Slider from './slider';
  6. import SliderAndInputWrapper from './sliderAndInputWrapper';
  7. import SliderLabel from './sliderLabel';
  8. type SliderProps = {
  9. name: string;
  10. /**
  11. * String is a valid type here only for empty string
  12. * Otherwise react complains:
  13. * "`value` prop on `input` should not be null. Consider using an empty string to clear the component or `undefined` for uncontrolled components."
  14. *
  15. * And we want this to be a controlled input when value is empty
  16. */
  17. value: number | '';
  18. /**
  19. * Array of allowed values. Make sure `value` is in this list.
  20. * THIS NEEDS TO BE SORTED
  21. */
  22. allowedValues?: number[];
  23. className?: string;
  24. disabled?: boolean;
  25. /**
  26. * Render prop for slider's label
  27. * Is passed the value as an argument
  28. */
  29. formatLabel?: (value: number | '') => React.ReactNode;
  30. forwardRef?: React.Ref<HTMLDivElement>;
  31. /**
  32. * HTML id of the range input
  33. */
  34. id?: string;
  35. /**
  36. * max allowed value, not needed if using `allowedValues`
  37. */
  38. max?: number;
  39. /**
  40. * min allowed value, not needed if using `allowedValues`
  41. */
  42. min?: number;
  43. /**
  44. * This is called when *any* MouseUp or KeyUp event happens.
  45. * Used for "smart" Fields to trigger a "blur" event. `onChange` can
  46. * be triggered quite frequently
  47. */
  48. onBlur?: (
  49. event: React.MouseEvent<HTMLInputElement> | React.KeyboardEvent<HTMLInputElement>
  50. ) => void;
  51. onChange?: (
  52. value: SliderProps['value'],
  53. event: React.ChangeEvent<HTMLInputElement>
  54. ) => void;
  55. /**
  56. * Placeholder for custom input
  57. */
  58. placeholder?: string;
  59. /**
  60. * Show input control for custom values
  61. */
  62. showCustomInput?: boolean;
  63. /**
  64. * Show label with current value
  65. */
  66. showLabel?: boolean;
  67. step?: number;
  68. };
  69. function RangeSlider({
  70. id,
  71. value,
  72. allowedValues,
  73. showCustomInput,
  74. name,
  75. disabled,
  76. placeholder,
  77. formatLabel,
  78. className,
  79. onBlur,
  80. onChange,
  81. forwardRef,
  82. showLabel = true,
  83. ...props
  84. }: SliderProps) {
  85. const [sliderValue, setSliderValue] = useState(
  86. allowedValues ? allowedValues.indexOf(Number(value || 0)) : value
  87. );
  88. useEffect(() => {
  89. updateSliderValue();
  90. // eslint-disable-next-line react-hooks/exhaustive-deps
  91. }, [value]);
  92. function updateSliderValue() {
  93. if (!defined(value)) {
  94. return;
  95. }
  96. const newSliderValueIndex = allowedValues?.indexOf(Number(value || 0)) ?? -1;
  97. // If `allowedValues` is defined, then `sliderValue` represents index to `allowedValues`
  98. if (newSliderValueIndex > -1) {
  99. setSliderValue(newSliderValueIndex);
  100. return;
  101. }
  102. setSliderValue(value);
  103. }
  104. function getActualValue(newSliderValue: SliderProps['value']): SliderProps['value'] {
  105. if (!allowedValues) {
  106. return newSliderValue;
  107. }
  108. // If `allowedValues` is defined, then `sliderValue` represents index to `allowedValues`
  109. return allowedValues[newSliderValue];
  110. }
  111. function handleInput(e: React.ChangeEvent<HTMLInputElement>) {
  112. const newSliderValue = parseFloat(e.target.value);
  113. setSliderValue(newSliderValue);
  114. onChange?.(getActualValue(newSliderValue), e);
  115. }
  116. function handleCustomInputChange(e: React.ChangeEvent<HTMLInputElement>) {
  117. setSliderValue(parseFloat(e.target.value) || 0);
  118. }
  119. function handleBlur(
  120. e: React.MouseEvent<HTMLInputElement> | React.KeyboardEvent<HTMLInputElement>
  121. ) {
  122. if (typeof onBlur !== 'function') {
  123. return;
  124. }
  125. onBlur(e);
  126. }
  127. function getSliderData() {
  128. if (!allowedValues) {
  129. const {min, max, step} = props;
  130. return {
  131. min,
  132. max,
  133. step,
  134. actualValue: sliderValue,
  135. displayValue: sliderValue,
  136. };
  137. }
  138. const actualValue = allowedValues[sliderValue];
  139. return {
  140. step: 1,
  141. min: 0,
  142. max: allowedValues.length - 1,
  143. actualValue,
  144. displayValue: defined(actualValue) ? actualValue : t('Invalid value'),
  145. };
  146. }
  147. const {min, max, step, actualValue, displayValue} = getSliderData();
  148. const labelText = formatLabel?.(actualValue) ?? displayValue;
  149. return (
  150. <div className={className} ref={forwardRef}>
  151. {!showCustomInput && showLabel && <SliderLabel>{labelText}</SliderLabel>}
  152. <SliderAndInputWrapper showCustomInput={showCustomInput}>
  153. <Slider
  154. type="range"
  155. name={name}
  156. id={id}
  157. min={min}
  158. max={max}
  159. step={step}
  160. disabled={disabled}
  161. onChange={handleInput}
  162. onInput={handleInput}
  163. onMouseUp={handleBlur}
  164. onKeyUp={handleBlur}
  165. value={sliderValue}
  166. hasLabel={!showCustomInput}
  167. aria-valuetext={labelText}
  168. />
  169. {showCustomInput && (
  170. <Input
  171. placeholder={placeholder}
  172. value={sliderValue}
  173. onChange={handleCustomInputChange}
  174. onBlur={handleInput}
  175. />
  176. )}
  177. </SliderAndInputWrapper>
  178. </div>
  179. );
  180. }
  181. const RangeSliderContainer = reactForwardRef(function RangeSliderContainer(
  182. props: SliderProps,
  183. ref: React.Ref<any>
  184. ) {
  185. return <RangeSlider {...props} forwardRef={ref} />;
  186. });
  187. export default RangeSliderContainer;
  188. export type {SliderProps};