UNPKG

@mskcc/carbon-react

Version:

Carbon react components for the MSKCC DSM

676 lines (647 loc) 23.2 kB
/** * MSKCC 2021, 2024 */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js'); var React = require('react'); var PropTypes = require('prop-types'); var cx = require('classnames'); var throttle = require('lodash.throttle'); var keys = require('../../internal/keyboard/keys.js'); var match = require('../../internal/keyboard/match.js'); var usePrefix = require('../../internal/usePrefix.js'); var deprecate = require('../../prop-types/deprecate.js'); var index = require('../FeatureFlags/index.js'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var React__default = /*#__PURE__*/_interopDefaultLegacy(React); var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes); var cx__default = /*#__PURE__*/_interopDefaultLegacy(cx); var throttle__default = /*#__PURE__*/_interopDefaultLegacy(throttle); var _span; const defaultFormatLabel = (value, label) => { return typeof label === 'function' ? label(value) : `${value}${label}`; }; /** * Minimum time between processed "drag" events. */ const EVENT_THROTTLE = 16; // ms /** * Event types that trigger "drags". */ const DRAG_EVENT_TYPES = new Set(['mousemove', 'touchmove']); /** * Event types that trigger a "drag" to stop. */ const DRAG_STOP_EVENT_TYPES = new Set(['mouseup', 'touchend', 'touchcancel']); class Slider extends React.PureComponent { constructor(props) { super(props); _rollupPluginBabelHelpers.defineProperty(this, "state", { value: this.props.value, left: 0, needsOnRelease: false, isValid: true }); _rollupPluginBabelHelpers.defineProperty(this, "thumbRef", void 0); _rollupPluginBabelHelpers.defineProperty(this, "filledTrackRef", void 0); _rollupPluginBabelHelpers.defineProperty(this, "element", null); _rollupPluginBabelHelpers.defineProperty(this, "inputId", ''); _rollupPluginBabelHelpers.defineProperty(this, "track", void 0); /** * Sets up "drag" event handlers and calls `this.onDrag` in case dragging * started on somewhere other than the thumb without a corresponding "move" * event. * * @param {Event} evt The event. */ _rollupPluginBabelHelpers.defineProperty(this, "onDragStart", evt => { // Do nothing if component is disabled if (this.props.disabled || this.props.readOnly) { return; } // Register drag stop handlers DRAG_STOP_EVENT_TYPES.forEach(element => { this.element?.ownerDocument.addEventListener(element, this.onDragStop); }); // Register drag handlers DRAG_EVENT_TYPES.forEach(element => { this.element?.ownerDocument.addEventListener(element, this.onDrag); }); // Perform first recalculation since we probably didn't click exactly in the // middle of the thumb this.onDrag(evt); }); /** * Unregisters "drag" and "drag stop" event handlers and calls sets the flag * indicating that the `onRelease` callback should be called. */ _rollupPluginBabelHelpers.defineProperty(this, "onDragStop", () => { // Do nothing if component is disabled if (this.props.disabled || this.props.readOnly) { return; } // Remove drag stop handlers DRAG_STOP_EVENT_TYPES.forEach(element => { this.element?.ownerDocument.removeEventListener(element, this.onDragStop); }); // Remove drag handlers DRAG_EVENT_TYPES.forEach(element => { this.element?.ownerDocument.removeEventListener(element, this.onDrag); }); // Set needsOnRelease flag so event fires on next update this.setState({ needsOnRelease: true, isValid: true }); }); /** * Handles a "drag" event by recalculating the value/thumb and setting state * accordingly. * * @param {Event} evt The event. */ _rollupPluginBabelHelpers.defineProperty(this, "_onDrag", evt => { // Do nothing if component is disabled or we have no event if (this.props.disabled || this.props.readOnly || !evt) { return; } let clientX; if ('clientX' in evt) { clientX = evt.clientX; } else if ('touches' in evt && 0 in evt.touches && 'clientX' in evt.touches[0]) { clientX = evt.touches[0].clientX; } else { // Do nothing if we have no valid clientX return; } const { value, left } = this.calcValue({ clientX }); this.setState({ value, left, isValid: true }); }); /** * Throttles calls to `this._onDrag` by limiting events to being processed at * most once every `EVENT_THROTTLE` milliseconds. */ _rollupPluginBabelHelpers.defineProperty(this, "onDrag", throttle__default["default"](this._onDrag, EVENT_THROTTLE, { leading: true, trailing: false })); /** * Handles a `keydown` event by recalculating the value/thumb and setting * state accordingly. * * @param {Event} evt The event. */ _rollupPluginBabelHelpers.defineProperty(this, "onKeyDown", evt => { // Do nothing if component is disabled or we don't have a valid event if (this.props.disabled || this.props.readOnly || !('which' in evt)) { return; } let delta = 0; if (match.matches(evt.which, [keys.ArrowDown, keys.ArrowLeft])) { delta = -(this.props.step ?? Slider.defaultProps.step); } else if (match.matches(evt.which, [keys.ArrowUp, keys.ArrowRight])) { delta = this.props.step ?? Slider.defaultProps.step; } else { // Ignore keys we don't want to handle return; } // If shift was held, account for the stepMultiplier if (evt.shiftKey) { const stepMultiplier = this.props.stepMultiplier; delta *= stepMultiplier ?? Slider.defaultProps.stepMultiplier; } Math.floor(this.state.value / (this.props.step ?? Slider.defaultProps.step)) * (this.props.step ?? Slider.defaultProps.step); const { value, left } = this.calcValue({ // Ensures custom value from `<input>` won't cause skipping next stepping point with right arrow key, // e.g. Typing 51 in `<input>`, moving focus onto the thumb and the hitting right arrow key should yield 52 instead of 54 value: (delta > 0 ? Math.floor(this.state.value / (this.props.step ?? Slider.defaultProps.step)) * (this.props.step ?? Slider.defaultProps.step) : this.state.value) + delta }); this.setState({ value, left, isValid: true }); }); /** * Provides the two-way binding for the input field of the Slider. It also * Handles a change to the input field by recalculating the value/thumb and * setting state accordingly. * * @param {Event} evt The event. */ _rollupPluginBabelHelpers.defineProperty(this, "onChange", evt => { // Do nothing if component is disabled if (this.props.disabled || this.props.readOnly) { return; } // Do nothing if we have no valid event, target, or value if (!evt || !('target' in evt) || typeof evt.target.value !== 'string') { return; } const targetValue = Number.parseFloat(evt.target.value); // Avoid calling calcValue for invalid numbers, but still update the state if (isNaN(targetValue)) { this.setState({ value: evt.target.value }); } else { const { value, left } = this.calcValue({ value: targetValue, useRawValue: true }); this.setState({ value, left }); } }); /** * Checks for validity of input value after clicking out of the input. It also * Handles state change to isValid state. * * @param {Event} evt The event. */ _rollupPluginBabelHelpers.defineProperty(this, "onBlur", evt => { // Do nothing if we have no valid event, target, or value if (!evt || !('target' in evt) || typeof evt.target.value !== 'string') { return; } // determine validity of input change after clicking out of input const validity = evt.target.checkValidity(); const { value } = evt.target; this.setState({ isValid: validity }); this.props.onBlur?.({ value }); }); /** * Calculates a new Slider `value` and `left` (thumb offset) given a `clientX`, * `value`, or neither of those. * - If `clientX` is specified, it will be used in * conjunction with the Slider's bounding rectangle to calculate the new * values. * - If `clientX` is not specified and `value` is, it will be used to * calculate new values as though it were the current value of the Slider. * - If neither `clientX` nor `value` are specified, `this.props.value` will * be used to calculate the new values as though it were the current value * of the Slider. * * @param {object} params * @param {number} [params.clientX] Optional clientX value expected to be from * an event fired by one of the Slider's `DRAG_EVENT_TYPES` events. * @param {number} [params.value] Optional value use during calculations if * clientX is not provided. * @param {boolean} [params.useRawValue=false] `true` to use the given value as-is. */ _rollupPluginBabelHelpers.defineProperty(this, "calcValue", _ref => { let { clientX, value, useRawValue = false } = _ref; const range = this.props.max - this.props.min; const boundingRect = this.element?.getBoundingClientRect?.(); const totalSteps = range / (this.props.step ?? Slider.defaultProps.step); let width = boundingRect ? boundingRect.right - boundingRect.left : 0; // Enforce a minimum width of at least 1 for calculations if (width <= 0) { width = 1; } // If a clientX is specified, use it to calculate the leftPercent. If not, // use the provided value or state's value to calculate it instead. let leftPercent; if (clientX != null) { const leftOffset = clientX - (boundingRect?.left ?? 0); leftPercent = leftOffset / width; } else { if (value == null) { value = this.state.value; } // prevent NaN calculation if the range is 0 leftPercent = range === 0 ? 0 : (value - this.props.min) / range; } if (useRawValue) { // Adjusts only for min/max of thumb position return { value, left: Math.min(1, Math.max(0, leftPercent)) * 100 }; } let steppedValue = Math.round(leftPercent * totalSteps) * (this.props.step ?? Slider.defaultProps.step); const steppedPercent = this.clamp(steppedValue / range, 0, 1); steppedValue = this.clamp(steppedValue + this.props.min, this.props.min, this.props.max); return { value: steppedValue, left: steppedPercent * 100 }; }); this.thumbRef = /*#__PURE__*/React__default["default"].createRef(); this.filledTrackRef = /*#__PURE__*/React__default["default"].createRef(); } /** * Sets up initial slider position and value in response to component mount. */ componentDidMount() { if (this.element) { const { value, left } = this.calcValue({ useRawValue: true }); this.setState({ value, left }); } } /** * Handles firing of `onChange` and `onRelease` callbacks to parent in * response to state changes. * * @param {*} prevProps prevProps * @param {*} prevState The previous Slider state, used to see if callbacks * should be called. */ componentDidUpdate(prevProps, prevState) { // Fire onChange event handler if present, if there's a usable value, and // if the value is different from the last one if (this.thumbRef.current) { this.thumbRef.current.style.left = `${this.state.left}%`; } if (this.filledTrackRef.current) { this.filledTrackRef.current.style.transform = `translate(0%, -50%) scaleX(${this.state.left / 100})`; } if (prevState.value !== this.state.value && typeof this.props.onChange === 'function') { this.props.onChange({ value: this.state.value }); } // Fire onRelease event handler if present and if needed if (this.state.needsOnRelease && typeof this.props.onRelease === 'function') { this.props.onRelease({ value: this.state.value }); // Reset the flag this.setState({ needsOnRelease: false }); } // If value from props does not change, do nothing here. // Otherwise, do prop -> state sync without "value capping". if (prevProps.value === this.props.value && prevProps.max === this.props.max && prevProps.min === this.props.min) { return; } this.setState(this.calcValue({ value: this.props.value, useRawValue: true })); } /** * Synonymous to ECMA2017+ `Math.clamp`. * * @param {number} val * @param {number} min * @param {number} max * * @returns `val` if `max>=val>=min`; `min` if `val<min`; `max` if `val>max`. */ clamp(val, min, max) { return Math.max(min, Math.min(val, max)); } // syncs invalid state and prop static getDerivedStateFromProps(props, state) { const { isValid } = state; // will override state in favor of invalid prop if (props.invalid === true && isValid === true) { return { isValid: false }; } if (props.invalid === false && isValid === false) { return { isValid: true }; } //if invalid prop is not provided, state will remain the same return null; } render() { const { ariaLabelInput, className, hideTextInput, id = this.inputId = this.inputId || `__carbon-slider_${Math.random().toString(36).substr(2)}`, min, minLabel, max, maxLabel, formatLabel = defaultFormatLabel, labelText, step, stepMultiplier: _stepMultiplier, inputType, invalidText, required, disabled, name, light, readOnly, warn, warnText, ...other } = this.props; delete other.onRelease; delete other.invalid; const { value, isValid } = this.state; return /*#__PURE__*/React__default["default"].createElement(usePrefix.PrefixContext.Consumer, null, prefix => { const labelId = `${id}-label`; const labelClasses = cx__default["default"](`${prefix}--label`, { [`${prefix}--label--disabled`]: disabled }); const sliderClasses = cx__default["default"](`${prefix}--slider`, { [`${prefix}--slider--disabled`]: disabled }, { [`${prefix}--slider--readonly`]: readOnly }); const inputClasses = cx__default["default"](`${prefix}--text-input`, `${prefix}--slider-text-input`, { [`${prefix}--text-input--light`]: light, [`${prefix}--text-input--invalid`]: !readOnly && isValid === false, [`${prefix}--slider-text-input--hidden`]: hideTextInput, [`${prefix}--slider-text-input--warn`]: !readOnly && warn }); return /*#__PURE__*/React__default["default"].createElement("div", { className: cx__default["default"](`${prefix}--form-item`, className) }, /*#__PURE__*/React__default["default"].createElement("label", { htmlFor: id, className: labelClasses, id: labelId }, labelText), /*#__PURE__*/React__default["default"].createElement("div", { className: `${prefix}--slider-container` }, /*#__PURE__*/React__default["default"].createElement("span", { className: `${prefix}--slider__range-label` }, formatLabel(min, minLabel)), /*#__PURE__*/React__default["default"].createElement("div", _rollupPluginBabelHelpers["extends"]({ className: sliderClasses, ref: node => { this.element = node; }, onMouseDown: this.onDragStart, onTouchStart: this.onDragStart, onKeyDown: this.onKeyDown, role: "presentation", tabIndex: -1, "data-invalid": !isValid && !readOnly ? true : null }, other), /*#__PURE__*/React__default["default"].createElement("div", { className: `${prefix}--slider__thumb`, role: "slider", id: id, tabIndex: !readOnly ? 0 : -1, "aria-valuemax": max, "aria-valuemin": min, "aria-valuenow": value, "aria-labelledby": labelId, ref: this.thumbRef }), /*#__PURE__*/React__default["default"].createElement("div", { className: `${prefix}--slider__track`, ref: node => { this.track = node; } }), /*#__PURE__*/React__default["default"].createElement("div", { className: `${prefix}--slider__filled-track`, ref: this.filledTrackRef })), /*#__PURE__*/React__default["default"].createElement("span", { className: `${prefix}--slider__range-label` }, formatLabel(max, maxLabel)), /*#__PURE__*/React__default["default"].createElement("input", { type: hideTextInput ? 'hidden' : inputType, id: `${id}-input-for-slider`, name: name, className: inputClasses, value: value, "aria-labelledby": !ariaLabelInput ? labelId : undefined, "aria-label": ariaLabelInput ? ariaLabelInput : undefined, disabled: disabled, required: required, min: min, max: max, step: step, onChange: this.onChange, onBlur: this.onBlur, onKeyUp: this.props.onInputKeyUp, "data-invalid": !isValid && !readOnly ? true : null, "aria-invalid": !isValid && !readOnly ? true : undefined, readOnly: readOnly })), !readOnly && isValid === false && /*#__PURE__*/React__default["default"].createElement("div", { className: cx__default["default"](`${prefix}--slider__validation-msg`, `${prefix}--slider__validation-msg--invalid`, `${prefix}--form-requirement`) }, invalidText), !readOnly && warn && isValid && /*#__PURE__*/ // <div // className={classNames( // `${prefix}--slider__validation-msg`, // `${prefix}--form-requirement`, // )} // > // {warnText} // </div> React__default["default"].createElement("div", { className: `msk-validation-msg` }, _span || (_span = /*#__PURE__*/React__default["default"].createElement("span", { className: `msk-validation-msg--icon msk-icon` }, "warning")), /*#__PURE__*/React__default["default"].createElement("div", { className: `${prefix}--slider__validation-msg ${prefix}--form-requirement` }, warnText))); }); } } _rollupPluginBabelHelpers.defineProperty(Slider, "propTypes", { /** * The `ariaLabel` for the `<input>`. */ ariaLabelInput: PropTypes__default["default"].string, /** * The child nodes. */ children: PropTypes__default["default"].node, /** * The CSS class name for the slider. */ className: PropTypes__default["default"].string, /** * `true` to disable this slider. */ disabled: PropTypes__default["default"].bool, /** * The callback to format the label associated with the minimum/maximum value. */ formatLabel: PropTypes__default["default"].func, /** * `true` to hide the number input box. */ hideTextInput: PropTypes__default["default"].bool, /** * The ID of the `<input>`. */ id: PropTypes__default["default"].string, /** * The `type` attribute of the `<input>`. */ inputType: PropTypes__default["default"].string, /** * `Specify whether the Slider is currently invalid */ invalid: PropTypes__default["default"].bool, /** * Provide the text that is displayed when the Slider is in an invalid state */ invalidText: PropTypes__default["default"].node, /** * The label for the slider. */ labelText: PropTypes__default["default"].node, /** * `true` to use the light version. */ light: deprecate["default"](PropTypes__default["default"].bool, 'The `light` prop for `Slider` is no longer needed and has ' + 'been deprecated in v11 in favor of the new `Layer` component. It will be moved in the next major release.'), /** * The maximum value. */ max: PropTypes__default["default"].number.isRequired, /** * The label associated with the maximum value. */ maxLabel: PropTypes__default["default"].string, /** * The minimum value. */ min: PropTypes__default["default"].number.isRequired, /** * The label associated with the minimum value. */ minLabel: PropTypes__default["default"].string, /** * The `name` attribute of the `<input>`. */ name: PropTypes__default["default"].string, /** * Provide an optional function to be called when the input element * loses focus */ onBlur: PropTypes__default["default"].func, /** * The callback to get notified of change in value. */ onChange: PropTypes__default["default"].func, /** * Provide an optional function to be called when a key is pressed in the number input */ onInputKeyUp: PropTypes__default["default"].func, /** * The callback to get notified of value on handle release. */ onRelease: PropTypes__default["default"].func, /** * Whether the slider should be read-only */ readOnly: PropTypes__default["default"].bool, /** * `true` to specify if the control is required. */ required: PropTypes__default["default"].bool, /** * A value determining how much the value should increase/decrease by moving the thumb by mouse. If a value other than 1 is provided and the input is *not* hidden, the new step requirement should be added to a visible label. Values outside of the `step` increment will be considered invalid. */ step: PropTypes__default["default"].number, /** * A value determining how much the value should increase/decrease by Shift+arrow keys, * which will be `(max - min) / stepMultiplier`. */ stepMultiplier: PropTypes__default["default"].number, /** * The value. */ value: PropTypes__default["default"].number.isRequired, /** * `Specify whether the Slider is in a warn state */ warn: PropTypes__default["default"].bool, /** * Provide the text that is displayed when the Slider is in an warn state */ warnText: PropTypes__default["default"].node }); _rollupPluginBabelHelpers.defineProperty(Slider, "defaultProps", { hideTextInput: false, step: 1, stepMultiplier: 4, disabled: false, minLabel: '', maxLabel: '', inputType: 'number', readOnly: false }); _rollupPluginBabelHelpers.defineProperty(Slider, "contextType", index.FeatureFlagContext); exports["default"] = Slider;