@mskcc/carbon-react
Version:
Carbon react components for the MSKCC DSM
676 lines (647 loc) • 23.2 kB
JavaScript
/**
* 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;