diff --git a/catalog/index.js b/catalog/index.js
index 4fdc5feca..c0bb84a6e 100644
--- a/catalog/index.js
+++ b/catalog/index.js
@@ -67,10 +67,11 @@ import {
theme,
} from '../index';
import {
- Modal as V6Modal,
Button as V6Button,
SegmentedButtonGroup,
Dropdown as V6Dropdown,
+ Modal as V6Modal,
+ Slider as V6Slider,
} from '../index-v6';
import { GroupSelector, LargeGroupSelector } from '../components/group-selector';
import { ShareDialog } from '../components/share-dialog';
@@ -904,10 +905,7 @@ const pages = [
path: '/slider/variations',
title: 'Slider Variations',
content: pageLoader(() => import('./slider/variations.md')),
- imports: {
- Slider,
- DocgenTable,
- },
+ imports: { Slider, DocgenTable },
},
{
path: 'slider/documentation',
@@ -915,6 +913,18 @@ const pages = [
content: pageLoader(() => import('./slider/documentation.md')),
imports: { Slider, DocgenTable },
},
+ {
+ path: '/slider/variations-v6',
+ title: 'v6 Slider Variations',
+ content: pageLoader(() => import('./slider/variations-v6.md')),
+ imports: { Slider: V6Slider, DocgenTable },
+ },
+ {
+ path: 'slider/documentation-v6',
+ title: 'v6 Slider Documentation',
+ content: pageLoader(() => import('./slider/documentation-v6.md')),
+ imports: { Slider: V6Slider, DocgenTable },
+ },
],
},
{
diff --git a/catalog/slider/documentation-v6.md b/catalog/slider/documentation-v6.md
new file mode 100644
index 000000000..0684e9c60
--- /dev/null
+++ b/catalog/slider/documentation-v6.md
@@ -0,0 +1,11 @@
+For the next major version of Styled UI, the Slider component has been rebuilt to use Styled System primitives.
+
+You can opt-in to the new API now by importing `{ Slider } from '@faithlife/styled-ui/v6'`. When v6 is released, the `/v6` entrypoint will continue to be supported with a deprecation warning until v7 is released.
+
+This documentation is automatically generated from JSDoc comments.
+
+```react
+noSource: true
+---
+
+```
diff --git a/catalog/slider/variations-v6.md b/catalog/slider/variations-v6.md
new file mode 100644
index 000000000..e8bfb459c
--- /dev/null
+++ b/catalog/slider/variations-v6.md
@@ -0,0 +1,108 @@
+For the next major version of Styled UI, the Slider component has been rebuilt to use Styled System primitives.
+
+You can opt-in to the new API now by importing `{ Slider } from '@faithlife/styled-ui/v6'`. When v6 is released, the `/v6` entrypoint will continue to be supported with a deprecation warning until v7 is released.
+
+## Slider
+
+```react
+showSource: true
+state: {}
+---
+
+
+
+
+```
+
+### With minValue / maxValue
+
+```react
+showSource: true
+state: {}
+---
+
+
+
+
+
+```
+
+### With labels
+
+```react
+showSource: true
+state: {}
+---
+
+
+
+
+
+```
+
+### Callback props: onStop vs onSlide
+
+If your slider will be making external API calls, you may wish to call that only on the `onStop` callback, when the user has finished sliding.
+If you want to have incremental updates while the slider is moving, such as to keep multiple sliders in sync, you may want to use the `onSlide` callback prop.
+
+```react
+showSource: true
+state: { value: 1 }
+---
+
+ onStop:
+
+ onSlide:
+
+
+```
+
+### hideAvailableStops
+
+For sliders with many stops, consider using the `hideAvailableStops` option.
+
+```react
+showSource: true
+state: { value: 50, labels: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100] }
+---
+
+
Opacity: {state.value * 2}
+
+
+
+
+```
+
+### Note on background colors
+
+The slider component expects a white background to create the sections of inactive track that cover the blue gradient. If your slider is not on a white background, use the `backgroundColor` (or `bg`) prop to indicate the correct background color.
+
+```react
+showSource: true
+state: {}
+---
+
+
+
+
+
+```
diff --git a/components/slider/component.jsx b/components/slider/component.jsx
index 59c1036bc..fdb33d582 100644
--- a/components/slider/component.jsx
+++ b/components/slider/component.jsx
@@ -34,16 +34,12 @@ export class Slider extends PureComponent {
stopCount: PropTypes.number.isRequired,
/** Useful for sliders with many stops */
hideAvailableStops: PropTypes.bool,
- /** Style overrides */
- styleOverrides: PropTypes.shape({
- backgroundColor: PropTypes.string,
- }),
+ ...Box.propTypes,
};
static defaultProps = {
hideAvailableStops: false,
labels: [],
- styleOverrides: {},
};
state = {
@@ -267,7 +263,6 @@ export class Slider extends PureComponent {
onStop,
stopCount,
hideAvailableStops,
- styleOverrides,
disabled,
...props
} = this.props;
@@ -308,7 +303,7 @@ export class Slider extends PureComponent {
0}
key={index}
- styleOverrides={styleOverrides}
/>
))}
@@ -357,12 +351,11 @@ export class Slider extends PureComponent {
const TrackPart = ({ children, left, right, ...props }) => (
{children}
diff --git a/components/slider/index.js b/components/slider/index.js
index 5aace8d3a..517afe7ac 100644
--- a/components/slider/index.js
+++ b/components/slider/index.js
@@ -1 +1,2 @@
+export { Slider as LegacySlider } from './legacy-component';
export { Slider } from './component';
diff --git a/components/slider/legacy-component.jsx b/components/slider/legacy-component.jsx
new file mode 100644
index 000000000..59c1036bc
--- /dev/null
+++ b/components/slider/legacy-component.jsx
@@ -0,0 +1,402 @@
+import React, { createRef, PureComponent, useRef } from 'react';
+import PropTypes from 'prop-types';
+import debounce from 'lodash.debounce';
+import throttle from 'lodash.throttle';
+import memoize from 'memoize-one';
+import { Box } from '../Box';
+import { Popover } from '../popover-v6';
+import * as Styled from './styled';
+
+function range(from, to) {
+ return [...Array(to - from).keys()].map(num => num + from);
+}
+
+// Derived from https://github.com/Faithlife/react-util/
+function createDerivedValue(getDependencies, calculateValue) {
+ const calculateMemoizedValue = memoize(calculateValue);
+ return () => calculateMemoizedValue(...getDependencies());
+}
+
+export class Slider extends PureComponent {
+ static propTypes = {
+ disabled: PropTypes.bool,
+ value: PropTypes.number.isRequired,
+ /** Array of numbers or strings to be used as tooltip labels */
+ labels: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
+ /** 0-based index */
+ minValue: PropTypes.number,
+ /** 0-based index, should be less than stopCount if specified */
+ maxValue: PropTypes.number,
+ /** Triggered every time the slider moves */
+ onSlide: PropTypes.func,
+ /** Triggered when the slider stops moving */
+ onStop: PropTypes.func,
+ stopCount: PropTypes.number.isRequired,
+ /** Useful for sliders with many stops */
+ hideAvailableStops: PropTypes.bool,
+ /** Style overrides */
+ styleOverrides: PropTypes.shape({
+ backgroundColor: PropTypes.string,
+ }),
+ };
+
+ static defaultProps = {
+ hideAvailableStops: false,
+ labels: [],
+ styleOverrides: {},
+ };
+
+ state = {
+ value: this.props.value,
+ isHovered: false,
+ isSliding: false,
+ };
+
+ _slider = createRef();
+ _timeout = null;
+
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ if (nextProps.value !== this.state.value) {
+ this.setState({ value: nextProps.value });
+ }
+ }
+
+ componentWillUnmount() {
+ if (this._timeout) {
+ clearTimeout(this._timeout);
+ }
+ }
+
+ calculateValue = clientX => {
+ if (!(this._slider && this._slider.current)) {
+ return null;
+ }
+
+ const rect = this._slider.current.getBoundingClientRect();
+ const width = rect.right - rect.left;
+ const pos = clientX - rect.left;
+ const clampedX = Math.min(Math.max(0, pos), width);
+
+ const newValue = Math.round((clampedX / width) * (this.props.stopCount - 1));
+ const { minValue, maxValue } = this.props;
+ return newValue > maxValue ? maxValue : newValue < minValue ? minValue : newValue;
+ };
+
+ getStops = createDerivedValue(() => [this.props.stopCount], stopCount => range(0, stopCount));
+
+ handleTouchStart = event => {
+ if (this.props.disabled) {
+ return;
+ }
+
+ event.preventDefault();
+ document.addEventListener('touchmove', this.handleTouchMove);
+ document.addEventListener('touchend', this.handleTouchEnd);
+ this.setState({ isHovered: true, isSliding: true });
+ this.handleTouchMove(event);
+ };
+
+ handleTouchMove = event => {
+ if (this.props.disabled) {
+ return;
+ }
+
+ event.preventDefault();
+ const value = this.calculateValue(event.touches[0].clientX);
+ if (this.state.value !== value) {
+ if (this.props.onSlide) {
+ this.props.onSlide(value);
+ }
+ this.setState({ value });
+ }
+ };
+
+ handleTouchEnd = event => {
+ if (this.props.disabled) {
+ return;
+ }
+
+ event.preventDefault();
+ document.removeEventListener('touchmove', this.handleTouchMove);
+ document.removeEventListener('touchend', this.handleTouchEnd);
+
+ if (this.props.onStop) {
+ this.props.onStop(this.state.value);
+ }
+
+ this.setState({ isSliding: false });
+ this.handleTogglePopover(false, 250);
+ };
+
+ handleMouseDown = event => {
+ if (this.props.disabled) {
+ return;
+ }
+
+ if (event.button !== 0 || this.state.isSliding) {
+ return;
+ }
+ event.preventDefault();
+ document.addEventListener('mousemove', this.handleMouseMove);
+ document.addEventListener('mouseup', this.handleMouseUp);
+ this.setState({ isHovered: true, isSliding: true });
+ this.handleMouseMove(event);
+ };
+
+ handleMouseMove = event => {
+ if (this.props.disabled) {
+ return;
+ }
+
+ event.preventDefault();
+ const value = this.calculateValue(event.clientX);
+ if (this.state.value !== value) {
+ if (this.props.onSlide) {
+ this.props.onSlide(value);
+ }
+ this.setState({ value, isHovered: true });
+ }
+ };
+
+ handleMouseUp = event => {
+ if (this.props.disabled) {
+ return;
+ }
+
+ event.preventDefault();
+ document.removeEventListener('mousemove', this.handleMouseMove);
+ document.removeEventListener('mouseup', this.handleMouseUp);
+
+ if (this.props.onStop) {
+ this.props.onStop(this.state.value);
+ }
+
+ this.setState({ isSliding: false });
+ this.handleTogglePopover(false);
+ };
+
+ handleKeyDown = event => {
+ if (event.key === 'Tab' || event.key === 'Escape') {
+ return;
+ }
+
+ event.preventDefault();
+ event.persist();
+ this.handleThrottledKeyDown(event);
+ };
+
+ handleDebouncedKeyInput = debounce(() => {
+ this.handleTogglePopover(false, 150);
+ if (this.props.onStop) {
+ this.props.onStop(this.state.value);
+ }
+ }, 250);
+
+ handleThrottledKeyDown = throttle(event => {
+ const { disabled, minValue, maxValue, stopCount } = this.props;
+ const { value: currentValue } = this.state;
+
+ if (disabled) {
+ this.setState({ isHovered: true }, () => {
+ this.handleTogglePopover(false, 400);
+ });
+ return;
+ }
+
+ if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
+ const newValue = currentValue + 1;
+ const value =
+ newValue > maxValue ? maxValue : newValue > stopCount - 1 ? stopCount - 1 : newValue;
+ return this.setState({ value, isHovered: true }, () => {
+ this.handleDebouncedKeyInput();
+ if (this.props.onSlide) {
+ this.props.onSlide(value);
+ }
+ });
+ } else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
+ const newValue = currentValue - 1;
+ const value = newValue < minValue ? minValue : newValue < 0 ? 0 : newValue;
+ return this.setState({ value, isHovered: true }, () => {
+ this.handleDebouncedKeyInput();
+ if (this.props.onSlide) {
+ this.props.onSlide(value);
+ }
+ });
+ }
+
+ return false;
+ }, 50);
+
+ handleMouseEnter = event => {
+ event.preventDefault();
+ this.handleTogglePopover(true);
+ };
+
+ handleMouseLeave = event => {
+ event.preventDefault();
+ this.handleTogglePopover(false);
+ };
+
+ handleTogglePopover = (isOpen, delay = 0) => {
+ if (!isOpen) {
+ if (this._timeout) {
+ clearTimeout(this._timeout);
+ }
+ this._timeout = setTimeout(() => {
+ this.setState({ isHovered: false });
+ this._timeout = null;
+ }, delay || 0);
+ } else {
+ if (this._timeout) {
+ clearTimeout(this._timeout);
+ }
+ this._timeout = setTimeout(() => {
+ this.setState({ isHovered: true });
+ this._timeout = null;
+ }, delay || 0);
+ }
+ };
+
+ render() {
+ const {
+ value,
+ labels,
+ minValue,
+ maxValue,
+ onSlide,
+ onStop,
+ stopCount,
+ hideAvailableStops,
+ styleOverrides,
+ disabled,
+ ...props
+ } = this.props;
+
+ const { isHovered, isSliding, value: pendingValue } = this.state;
+ const stops = this.getStops();
+
+ const activeStopIndex = Math.min(Math.round(pendingValue), stopCount - 1);
+
+ return (
+ event.preventDefault()}
+ onKeyDown={this.handleKeyDown}
+ onMouseDown={this.handleMouseDown}
+ onTouchStart={this.handleTouchStart}
+ tabIndex="0"
+ ref={this._slider}
+ position="relative"
+ width="100%"
+ minHeight="28px"
+ css={`
+ cursor: ${({ disabled }) => (disabled ? 'normal' : 'pointer')};
+ touch-action: none;
+
+ &:focus {
+ box-shadow: 0 0 0 0.2rem rgba(30, 145, 214, 0.5);
+ outline: none;
+ }
+ `}
+ {...props}
+ >
+
+
+
+
+ {!hideAvailableStops || minValue ? (
+
+ {stops.map(index => (
+ (minValue ?? 0) && !(index >= maxValue) && !(index === stopCount - 1))
+ }
+ minimumAvailable={index === minValue && minValue > 0}
+ key={index}
+ styleOverrides={styleOverrides}
+ />
+ ))}
+
+ ) : null}
+
+
+
+
+ );
+ }
+}
+
+const TrackPart = ({ children, left, right, ...props }) => (
+
+ {children}
+
+);
+
+function getPercentage(index, max) {
+ return (index / max) * 100;
+}
+
+const Thumb = React.memo(
+ ({ isSliding, isHovered, isAtTrackStart, isAtTrackEnd, position, label, disabled }) => {
+ const ref = useRef();
+ const isPopupOpen = !!(isHovered && (label || label === 0));
+ return (
+
+
+ {isPopupOpen && (
+
+ {`${label}`}
+
+ )}
+
+ );
+ },
+);
diff --git a/index-v6.js b/index-v6.js
index a3ed80262..1909362fe 100644
--- a/index-v6.js
+++ b/index-v6.js
@@ -1,4 +1,5 @@
-export { Modal } from './components/modal';
export { Button, SegmentedButtonGroup } from './components/button';
-export { usePopover, Popover } from './components/popover-v6';
export { Dropdown } from './components/dropdown';
+export { Modal } from './components/modal';
+export { usePopover, Popover } from './components/popover-v6';
+export { Slider } from './components/slider';
diff --git a/index.js b/index.js
index fac215d5b..5eb33ee31 100644
--- a/index.js
+++ b/index.js
@@ -32,8 +32,8 @@ export {
Tooltip,
} from './components/popover';
export { Radio } from './components/radio';
-export { Slider } from './components/slider';
export { SimpleToast } from './components/simple-toast';
+export { LegacySlider as Slider } from './components/slider';
export {
TabManager,
Tab,