diff --git a/src/Slider/Slider.js b/src/Slider/Slider.js
deleted file mode 100644
index 6b4db6a0..00000000
--- a/src/Slider/Slider.js
+++ /dev/null
@@ -1,579 +0,0 @@
-// helper functions and event handling basically copied from Material UI (https://github.com/mui-org/material-ui) Slider component
-import React, { useRef } from 'react';
-import propTypes from 'prop-types';
-
-import styled, { css } from 'styled-components';
-import {
- createBoxStyles,
- createBorderStyles,
- createFlatBoxStyles,
- createDisabledTextStyles,
- createHatchedBackground
-} from '../common';
-import { clamp, getSize, roundValueToStep } from '../common/utils';
-import useControlledOrUncontrolled from '../common/hooks/useControlledOrUncontrolled';
-import useForkRef from '../common/hooks/useForkRef';
-import { useIsFocusVisible } from '../common/hooks/useIsFocusVisible';
-import { StyledCutout } from '../Cutout/Cutout';
-
-function percentToValue(percent, min, max) {
- return (max - min) * percent + min;
-}
-
-function trackFinger(event, touchId) {
- if (touchId.current !== undefined && event.changedTouches) {
- for (let i = 0; i < event.changedTouches.length; i += 1) {
- const touch = event.changedTouches[i];
- if (touch.identifier === touchId.current) {
- return {
- x: touch.clientX,
- y: touch.clientY
- };
- }
- }
- return false;
- }
- return {
- x: event.clientX,
- y: event.clientY
- };
-}
-const useEnhancedEffect =
- typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
-
-function useEventCallback(fn) {
- const ref = React.useRef(fn);
- useEnhancedEffect(() => {
- ref.current = fn;
- });
- return React.useCallback((...args) => (0, ref.current)(...args), []);
-}
-function ownerDocument(node) {
- return (node && node.ownerDocument) || document;
-}
-function findClosest(values, currentValue) {
- const { index: closestIndex } = values.reduce((acc, value, index) => {
- const distance = Math.abs(currentValue - value);
-
- if (acc === null || distance < acc.distance || distance === acc.distance) {
- return {
- distance,
- index
- };
- }
-
- return acc;
- }, null);
- return closestIndex;
-}
-
-function focusThumb(sliderRef) {
- if (!sliderRef.current.contains(document.activeElement)) {
- sliderRef.current.querySelector(`#swag`).focus();
- }
-}
-const Wrapper = styled.div`
- display: inline-block;
- position: relative;
- touch-action: none;
- &:before {
- content: '';
- display: inline-block;
- position: absolute;
- top: -2px;
- left: -15px;
- width: calc(100% + 30px);
- height: ${({ hasMarks }) => (hasMarks ? '41px' : '39px')};
- ${({ isFocused, theme }) =>
- isFocused &&
- `
- outline: 2px dotted ${theme.materialText};
- `}
- }
-
- ${({ vertical, size }) =>
- vertical
- ? css`
- height: ${size};
- margin-right: 1.5rem;
- &:before {
- left: -6px;
- top: -15px;
- height: calc(100% + 30px);
- width: ${({ hasMarks }) => (hasMarks ? '41px' : '39px')};
- }
- `
- : css`
- width: ${size};
- margin-bottom: 1.5rem;
- &:before {
- top: -2px;
- left: -15px;
- width: calc(100% + 30px);
- height: ${({ hasMarks }) => (hasMarks ? '41px' : '39px')};
- }
- `}
-
- pointer-events: ${({ isDisabled }) => (isDisabled ? 'none' : 'auto')};
-`;
-const sharedGrooveStyles = () => css`
- position: absolute;
- ${({ vertical }) =>
- vertical
- ? css`
- bottom: 0;
- left: 50%;
- transform: translateX(-50%);
- height: 100%;
- width: 8px;
- `
- : css`
- left: 0;
- top: 50%;
- transform: translateY(-50%);
- height: 8px;
- width: 100%;
- `}
-`;
-const StyledGroove = styled(StyledCutout)`
- ${sharedGrooveStyles()}
-`;
-const StyledFlatGroove = styled(StyledCutout)`
- ${sharedGrooveStyles()}
-
- border-left-color: ${({ theme }) => theme.flatLight};
- border-top-color: ${({ theme }) => theme.flatLight};
- border-right-color: ${({ theme }) => theme.canvas};
- border-bottom-color: ${({ theme }) => theme.canvas};
- &:before {
- border-left-color: ${({ theme }) => theme.flatDark};
- border-top-color: ${({ theme }) => theme.flatDark};
- border-right-color: ${({ theme }) => theme.flatLight};
- border-bottom-color: ${({ theme }) => theme.flatLight};
- }
-`;
-const Thumb = styled.span`
- position: relative;
- ${({ vertical }) =>
- vertical
- ? css`
- width: 32px;
- height: 18px;
- right: 2px;
- transform: translateY(-50%);
- `
- : css`
- height: 32px;
- width: 18px;
- top: 2px;
- transform: translateX(-50%);
- `}
- ${({ variant }) =>
- variant === 'flat'
- ? css`
- ${createFlatBoxStyles()}
- outline: 2px solid ${({ theme }) => theme.flatDark};
- background: ${({ theme }) => theme.flatLight};
- `
- : css`
- ${createBoxStyles()}
- ${createBorderStyles()}
- `}
- ${({ isDisabled, theme }) =>
- isDisabled &&
- createHatchedBackground({
- mainColor: theme.material,
- secondaryColor: theme.borderLightest
- })}
-`;
-
-const tickHeight = 6;
-const Tick = styled.span`
- display: inline-block;
- position: absolute;
-
- ${({ vertical }) =>
- vertical
- ? css`
- right: ${-tickHeight - 2}px;
- bottom: 0px;
- transform: translateY(1px);
- width: ${tickHeight}px;
- border-bottom: 2px solid ${({ theme }) => theme.materialText};
- `
- : css`
- bottom: ${-tickHeight}px;
- height: ${tickHeight}px;
- transform: translateX(-1px);
- border-left: 1px solid ${({ theme }) => theme.materialText};
- border-right: 1px solid ${({ theme }) => theme.materialText};
- `}
-
- color: ${({ theme }) => theme.materialText};
- ${({ isDisabled, theme }) =>
- isDisabled &&
- css`
- ${createDisabledTextStyles()}
- box-shadow: 1px 1px 0px ${theme.materialTextDisabledShadow};
- border-color: ${theme.materialTextDisabled};
- `}
-`;
-const Mark = styled.div`
- position: absolute;
- bottom: 0;
- left: 0;
- line-height: 1;
- font-size: 0.875rem;
-
- ${({ vertical }) =>
- vertical
- ? css`
- transform: translate(${tickHeight + 2}px, ${tickHeight + 1}px);
- `
- : css`
- transform: translate(-0.5ch, calc(100% + 2px));
- `}
-`;
-
-const Slider = React.forwardRef(function Slider(props, ref) {
- const {
- value,
- defaultValue,
- step,
- min,
- max,
- size,
- marks: marksProp,
- onChange,
- onChangeCommitted,
- onMouseDown,
- name,
- orientation,
- variant,
- disabled,
- ...otherProps
- } = props;
- const Groove = variant === 'flat' ? StyledFlatGroove : StyledGroove;
- const vertical = orientation === 'vertical';
- const [valueDerived, setValueState] = useControlledOrUncontrolled({
- value,
- defaultValue
- });
-
- const {
- isFocusVisible,
- onBlurVisible,
- ref: focusVisibleRef
- } = useIsFocusVisible();
- const [focusVisible, setFocusVisible] = React.useState(false);
- const sliderRef = useRef();
- const handleFocusRef = useForkRef(focusVisibleRef, sliderRef);
- const handleRef = useForkRef(ref, handleFocusRef);
-
- const handleFocus = useEventCallback(event => {
- if (isFocusVisible(event)) {
- setFocusVisible(true);
- }
- });
- const handleBlur = useEventCallback(() => {
- if (focusVisible !== false) {
- setFocusVisible(false);
- onBlurVisible();
- }
- });
-
- const touchId = React.useRef();
-
- const marks =
- marksProp === true && step !== null
- ? [...Array(Math.round((max - min) / step) + 1)].map((_, index) => ({
- value: min + step * index
- }))
- : marksProp || [];
-
- const handleKeyDown = useEventCallback(event => {
- const tenPercents = (max - min) / 10;
- const marksValues = marks.map(mark => mark.value);
- const marksIndex = marksValues.indexOf(valueDerived);
- let newValue;
-
- switch (event.key) {
- case 'Home':
- newValue = min;
- break;
- case 'End':
- newValue = max;
- break;
- case 'PageUp':
- if (step) {
- newValue = valueDerived + tenPercents;
- }
- break;
- case 'PageDown':
- if (step) {
- newValue = valueDerived - tenPercents;
- }
- break;
- case 'ArrowRight':
- case 'ArrowUp':
- if (step) {
- newValue = valueDerived + step;
- } else {
- newValue =
- marksValues[marksIndex + 1] || marksValues[marksValues.length - 1];
- }
- break;
- case 'ArrowLeft':
- case 'ArrowDown':
- if (step) {
- newValue = valueDerived - step;
- } else {
- newValue = marksValues[marksIndex - 1] || marksValues[0];
- }
- break;
- default:
- return;
- }
-
- // Prevent scroll of the page
- event.preventDefault();
- if (step) {
- newValue = roundValueToStep(newValue, step, min);
- }
-
- newValue = clamp(newValue, min, max);
-
- setValueState(newValue);
- setFocusVisible(true);
-
- if (onChange) {
- onChange(event, newValue);
- }
- if (onChangeCommitted) {
- onChangeCommitted(event, newValue);
- }
- });
-
- const getNewValue = React.useCallback(
- finger => {
- const { current: slider } = sliderRef;
- const rect = slider.getBoundingClientRect();
-
- let percent;
- if (vertical) {
- percent = (rect.bottom - finger.y) / rect.height;
- } else {
- percent = (finger.x - rect.left) / rect.width;
- }
- let newValue;
-
- newValue = percentToValue(percent, min, max);
- if (step) {
- newValue = roundValueToStep(newValue, step, min);
- } else {
- const marksValues = marks.map(mark => mark.value);
- const closestIndex = findClosest(marksValues, newValue);
- newValue = marksValues[closestIndex];
- }
- newValue = clamp(newValue, min, max);
- return newValue;
- },
- [max, min, step]
- );
-
- const handleTouchMove = useEventCallback(event => {
- const finger = trackFinger(event, touchId);
-
- if (!finger) {
- return;
- }
- const newValue = getNewValue(finger);
-
- focusThumb(sliderRef);
- setValueState(newValue);
- setFocusVisible(true);
-
- if (onChange) {
- onChange(event, newValue);
- }
- });
- const handleTouchEnd = useEventCallback(event => {
- const finger = trackFinger(event, touchId);
-
- if (!finger) {
- return;
- }
-
- const newValue = getNewValue(finger);
-
- if (onChangeCommitted) {
- onChangeCommitted(event, newValue);
- }
-
- touchId.current = undefined;
-
- const doc = ownerDocument(sliderRef.current);
- doc.removeEventListener('mousemove', handleTouchMove);
- doc.removeEventListener('mouseup', handleTouchEnd);
- doc.removeEventListener('touchmove', handleTouchMove);
- doc.removeEventListener('touchend', handleTouchEnd);
- });
- const handleMouseDown = useEventCallback(event => {
- // TODO should we also pass event together with new value to callbacks? (same thing with other input components)
- if (onMouseDown) {
- onMouseDown(event);
- }
- event.preventDefault();
- const finger = trackFinger(event, touchId);
- const newValue = getNewValue(finger);
- focusThumb(sliderRef);
-
- setValueState(newValue);
- setFocusVisible(true);
-
- if (onChange) {
- onChange(event, newValue);
- }
- const doc = ownerDocument(sliderRef.current);
- doc.addEventListener('mousemove', handleTouchMove);
- doc.addEventListener('mouseup', handleTouchEnd);
- });
- const handleTouchStart = useEventCallback(event => {
- // Workaround as Safari has partial support for touchAction: 'none'.
- event.preventDefault();
- const touch = event.changedTouches[0];
- if (touch != null) {
- // A number that uniquely identifies the current finger in the touch session.
- touchId.current = touch.identifier;
- }
- const finger = trackFinger(event, touchId);
- const newValue = getNewValue(finger);
- focusThumb(sliderRef);
-
- setValueState(newValue);
- setFocusVisible(true);
-
- if (onChange) {
- onChange(event, newValue);
- }
-
- const doc = ownerDocument(sliderRef.current);
- doc.addEventListener('touchmove', handleTouchMove);
- doc.addEventListener('touchend', handleTouchEnd);
- });
- React.useEffect(() => {
- const { current: slider } = sliderRef;
- slider.addEventListener('touchstart', handleTouchStart);
- const doc = ownerDocument(slider);
-
- return () => {
- slider.removeEventListener('touchstart', handleTouchStart);
- doc.removeEventListener('mousemove', handleTouchMove);
- doc.removeEventListener('mouseup', handleTouchEnd);
- doc.removeEventListener('touchmove', handleTouchMove);
- doc.removeEventListener('touchend', handleTouchEnd);
- };
- }, [handleTouchEnd, handleTouchMove, handleTouchStart]);
-
-
- return (
-
- {/* should we keep the hidden input ? */}
-
- {marks &&
- marks.map(m => (
-
- {m.label && (
-
- {m.label}
-
- )}
-
- ))}
-
-
-
- );
-});
-
-Slider.defaultProps = {
- defaultValue: undefined,
- value: undefined,
- step: 1,
- min: 0,
- max: 100,
- size: '100%',
- onChange: null,
- onChangeCommitted: null,
- onMouseDown: null,
-
- name: null,
- marks: false,
- variant: 'default',
- orientation: 'horizontal',
- disabled: false
-};
-
-Slider.propTypes = {
- value: propTypes.number,
- defaultValue: propTypes.number,
-
- step: propTypes.number,
- min: propTypes.number,
- max: propTypes.number,
- size: propTypes.oneOfType([propTypes.string, propTypes.number]),
- onChange: propTypes.func,
- onChangeCommitted: propTypes.func,
- onMouseDown: propTypes.func,
-
- name: propTypes.string,
- marks: propTypes.oneOfType([propTypes.bool, propTypes.array]),
- variant: propTypes.oneOf(['default', 'flat']),
- orientation: propTypes.oneOf(['horizontal', 'vertical']),
- disabled: propTypes.bool
-};
-export default Slider;
diff --git a/src/Slider/Slider.spec.js b/src/Slider/Slider.spec.tsx
similarity index 76%
rename from src/Slider/Slider.spec.js
rename to src/Slider/Slider.spec.tsx
index ebee1e21..e377e47b 100644
--- a/src/Slider/Slider.spec.js
+++ b/src/Slider/Slider.spec.tsx
@@ -1,20 +1,15 @@
// Pretty much straight out copied from https://github.com/mui-org/material-ui 😂
-import React from 'react';
import { fireEvent } from '@testing-library/react';
import { renderWithTheme, Touch } from '../../test/utils';
-import Slider from './Slider';
+import { Slider } from './Slider';
-function createTouches(touches) {
+function createTouches(
+ touches: { identifier: number; clientX?: number; clientY?: number }[]
+) {
return {
- changedTouches: touches.map(
- touch =>
- new Touch({
- target: document.body,
- ...touch
- })
- )
+ changedTouches: touches.map(touch => new Touch(touch))
};
}
@@ -31,14 +26,16 @@ describe('', () => {
/>
);
- fireEvent.mouseDown(container.firstChild);
+ const slider = container.firstElementChild as HTMLElement;
+ fireEvent.mouseDown(slider);
fireEvent.mouseUp(document.body);
expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleChangeCommitted).toHaveBeenCalledTimes(1);
getByRole('slider').focus();
- fireEvent.keyDown(document.activeElement, {
+ const focusedSlider = document.activeElement as HTMLElement;
+ fireEvent.keyDown(focusedSlider, {
key: 'Home'
});
expect(handleChange).toHaveBeenCalledTimes(2);
@@ -56,10 +53,8 @@ describe('', () => {
/>
);
- fireEvent.touchStart(
- container.firstChild,
- createTouches([{ identifier: 1 }])
- );
+ const slider = container.firstElementChild as HTMLElement;
+ fireEvent.touchStart(slider, createTouches([{ identifier: 1 }]));
expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleChangeCommitted).not.toHaveBeenCalled();
@@ -93,7 +88,8 @@ describe('', () => {
const { container } = renderWithTheme(
);
- fireEvent.mouseDown(container.firstChild);
+ const slider = container.firstElementChild as HTMLElement;
+ fireEvent.mouseDown(slider);
expect(handleMouseDown).toHaveBeenCalledTimes(1);
});
describe('prop: step', () => {
@@ -105,28 +101,30 @@ describe('', () => {
defaultValue={0}
/>
);
+ const slider = container.firstElementChild as HTMLElement;
// mocking containers size
- container.firstChild.getBoundingClientRect = () => ({
- width: 100,
- height: 20,
- bottom: 20,
- left: 0
- });
+ slider.getBoundingClientRect = () =>
+ ({
+ width: 100,
+ height: 20,
+ bottom: 20,
+ left: 0
+ } as DOMRect);
const thumb = getByRole('slider');
fireEvent.touchStart(
- container.firstChild,
+ slider,
createTouches([{ identifier: 1, clientX: 22, clientY: 0 }])
);
expect(thumb).toHaveAttribute('aria-valuenow', '20');
thumb.focus();
- fireEvent.keyDown(document.activeElement, {
+ fireEvent.keyDown(document.activeElement as HTMLElement, {
key: 'ArrowUp'
});
expect(thumb).toHaveAttribute('aria-valuenow', '30');
- fireEvent.keyDown(document.activeElement, {
+ fireEvent.keyDown(document.activeElement as HTMLElement, {
key: 'ArrowDown'
});
expect(thumb).toHaveAttribute('aria-valuenow', '20');
@@ -143,11 +141,10 @@ describe('', () => {
disabled
/>
);
+ const slider = container.firstElementChild as HTMLElement;
const thumb = getByRole('slider');
expect(
- window
- .getComputedStyle(container.firstChild, null)
- .getPropertyValue('pointer-events')
+ window.getComputedStyle(slider, null).getPropertyValue('pointer-events')
).toBe('none');
expect(thumb).toHaveAttribute('aria-disabled', 'true');
});
@@ -159,27 +156,27 @@ describe('', () => {
const thumb = getByRole('slider');
thumb.focus();
- fireEvent.keyDown(document.activeElement, {
+ fireEvent.keyDown(document.activeElement as HTMLElement, {
key: 'Home'
});
expect(thumb).toHaveAttribute('aria-valuenow', '0');
- fireEvent.keyDown(document.activeElement, {
+ fireEvent.keyDown(document.activeElement as HTMLElement, {
key: 'End'
});
expect(thumb).toHaveAttribute('aria-valuenow', '100');
- fireEvent.keyDown(document.activeElement, {
+ fireEvent.keyDown(document.activeElement as HTMLElement, {
key: 'PageDown'
});
expect(thumb).toHaveAttribute('aria-valuenow', '90');
- fireEvent.keyDown(document.activeElement, {
+ fireEvent.keyDown(document.activeElement as HTMLElement, {
key: 'Escape'
});
expect(thumb).toHaveAttribute('aria-valuenow', '90');
- fireEvent.keyDown(document.activeElement, {
+ fireEvent.keyDown(document.activeElement as HTMLElement, {
key: 'PageUp'
});
expect(thumb).toHaveAttribute('aria-valuenow', '100');
@@ -199,10 +196,10 @@ describe('', () => {
const thumb = getByRole('slider');
thumb.focus();
- fireEvent.keyDown(document.activeElement, moveRightEvent);
+ fireEvent.keyDown(document.activeElement as HTMLElement, moveRightEvent);
expect(thumb).toHaveAttribute('aria-valuenow', '6');
- fireEvent.keyDown(document.activeElement, moveLeftEvent);
+ fireEvent.keyDown(document.activeElement as HTMLElement, moveLeftEvent);
expect(thumb).toHaveAttribute('aria-valuenow', '4');
expect(thumb.style.left).toBe('20%');
@@ -215,19 +212,19 @@ describe('', () => {
const thumb = getByRole('slider');
thumb.focus();
- fireEvent.keyDown(document.activeElement, moveRightEvent);
+ fireEvent.keyDown(document.activeElement as HTMLElement, moveRightEvent);
expect(thumb).toHaveAttribute('aria-valuenow', '96');
- fireEvent.keyDown(document.activeElement, moveRightEvent);
+ fireEvent.keyDown(document.activeElement as HTMLElement, moveRightEvent);
expect(thumb).toHaveAttribute('aria-valuenow', '106');
- fireEvent.keyDown(document.activeElement, moveRightEvent);
+ fireEvent.keyDown(document.activeElement as HTMLElement, moveRightEvent);
expect(thumb).toHaveAttribute('aria-valuenow', '108');
- fireEvent.keyDown(document.activeElement, moveLeftEvent);
+ fireEvent.keyDown(document.activeElement as HTMLElement, moveLeftEvent);
expect(thumb).toHaveAttribute('aria-valuenow', '96');
- fireEvent.keyDown(document.activeElement, moveLeftEvent);
+ fireEvent.keyDown(document.activeElement as HTMLElement, moveLeftEvent);
expect(thumb).toHaveAttribute('aria-valuenow', '86');
});
@@ -238,13 +235,13 @@ describe('', () => {
const thumb = getByRole('slider');
thumb.focus();
- fireEvent.keyDown(document.activeElement, moveLeftEvent);
+ fireEvent.keyDown(document.activeElement as HTMLElement, moveLeftEvent);
expect(thumb).toHaveAttribute('aria-valuenow', '6');
- fireEvent.keyDown(document.activeElement, moveRightEvent);
+ fireEvent.keyDown(document.activeElement as HTMLElement, moveRightEvent);
expect(thumb).toHaveAttribute('aria-valuenow', '16');
- fireEvent.keyDown(document.activeElement, moveRightEvent);
+ fireEvent.keyDown(document.activeElement as HTMLElement, moveRightEvent);
expect(thumb).toHaveAttribute('aria-valuenow', '26');
});
@@ -255,7 +252,7 @@ describe('', () => {
const thumb = getByRole('slider');
thumb.focus();
- fireEvent.keyDown(document.activeElement, moveRightEvent);
+ fireEvent.keyDown(document.activeElement as HTMLElement, moveRightEvent);
expect(thumb).toHaveAttribute('aria-valuenow', '0.3');
});
@@ -271,7 +268,7 @@ describe('', () => {
const thumb = getByRole('slider');
thumb.focus();
- fireEvent.keyDown(document.activeElement, moveRightEvent);
+ fireEvent.keyDown(document.activeElement as HTMLElement, moveRightEvent);
expect(thumb).toHaveAttribute('aria-valuenow', '3e-8');
});
@@ -287,7 +284,7 @@ describe('', () => {
const thumb = getByRole('slider');
thumb.focus();
- fireEvent.keyDown(document.activeElement, moveLeftEvent);
+ fireEvent.keyDown(document.activeElement as HTMLElement, moveLeftEvent);
expect(thumb).toHaveAttribute('aria-valuenow', '-3e-8');
});
});
@@ -314,16 +311,19 @@ describe('', () => {
/>
);
+ const slider = container.firstElementChild as HTMLElement;
+
// mocking containers size
- container.firstChild.getBoundingClientRect = () => ({
- width: 20,
- height: 100,
- bottom: 100,
- left: 0
- });
+ slider.getBoundingClientRect = () =>
+ ({
+ width: 20,
+ height: 100,
+ bottom: 100,
+ left: 0
+ } as DOMRect);
fireEvent.touchStart(
- container.firstChild,
+ slider,
createTouches([{ identifier: 1, clientX: 0, clientY: 20 }])
);
fireEvent.touchMove(
diff --git a/src/Slider/Slider.stories.js b/src/Slider/Slider.stories.tsx
similarity index 96%
rename from src/Slider/Slider.stories.js
rename to src/Slider/Slider.stories.tsx
index 2090e585..caec2332 100644
--- a/src/Slider/Slider.stories.js
+++ b/src/Slider/Slider.stories.tsx
@@ -1,8 +1,7 @@
-import React from 'react';
+import { ComponentMeta } from '@storybook/react';
+import { Cutout, Slider } from 'react95';
import styled from 'styled-components';
-import { Slider, Cutout } from '..';
-
const Wrapper = styled.div`
background: ${({ theme }) => theme.material};
padding: 5rem;
@@ -38,7 +37,7 @@ export default {
title: 'Slider',
component: Slider,
decorators: [story => {story()}]
-};
+} as ComponentMeta;
export function Default() {
return (
diff --git a/src/Slider/Slider.tsx b/src/Slider/Slider.tsx
new file mode 100644
index 00000000..4fcfd70f
--- /dev/null
+++ b/src/Slider/Slider.tsx
@@ -0,0 +1,638 @@
+// helper functions and event handling basically copied from Material UI (https://github.com/mui-org/material-ui) Slider component
+import React, {
+ forwardRef,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState
+} from 'react';
+import styled, { css } from 'styled-components';
+
+import {
+ createBorderStyles,
+ createBoxStyles,
+ createDisabledTextStyles,
+ createFlatBoxStyles,
+ createHatchedBackground
+} from '../common';
+import useControlledOrUncontrolled from '../common/hooks/useControlledOrUncontrolled';
+import useForkRef from '../common/hooks/useForkRef';
+import { useIsFocusVisible } from '../common/hooks/useIsFocusVisible';
+import { clamp, getSize, roundValueToStep } from '../common/utils';
+import { StyledCutout } from '../Cutout/Cutout';
+import { CommonStyledProps } from '../types';
+
+type SliderProps = {
+ defaultValue?: number;
+ disabled?: boolean;
+ marks?: boolean | { label?: string; value: number }[];
+ max?: number;
+ min?: number;
+ name?: string;
+ onChange?: (
+ event:
+ | MouseEvent
+ | React.KeyboardEvent
+ | React.MouseEvent
+ | TouchEvent,
+ value: number
+ ) => void;
+ onChangeCommitted?: (
+ event: MouseEvent | React.KeyboardEvent | TouchEvent,
+ value: number
+ ) => void;
+ onMouseDown?: (event: React.MouseEvent) => void;
+ orientation?: 'horizontal' | 'vertical';
+ size?: string | number;
+ step?: number | null;
+ value?: number;
+ variant?: 'default' | 'flat';
+} & React.HTMLAttributes &
+ CommonStyledProps;
+
+function percentToValue(percent: number, min: number, max: number) {
+ return (max - min) * percent + min;
+}
+
+function trackFinger(
+ event: MouseEvent | React.MouseEvent | TouchEvent,
+ touchId: number | undefined
+) {
+ if (touchId !== undefined && 'changedTouches' in event) {
+ for (let i = 0; i < event.changedTouches.length; i += 1) {
+ const touch = event.changedTouches[i];
+ if (touch.identifier === touchId) {
+ return {
+ x: touch.clientX,
+ y: touch.clientY
+ };
+ }
+ }
+
+ return false;
+ }
+
+ if ('clientX' in event) {
+ return {
+ x: event.clientX,
+ y: event.clientY
+ };
+ }
+
+ return false;
+}
+
+function ownerDocument(node?: Element) {
+ return (node && node.ownerDocument) || document;
+}
+
+function findClosest(values: number[], currentValue: number) {
+ const { index: closestIndex } =
+ values.reduce<{
+ distance: number;
+ index: number;
+ } | null>((acc, value, index) => {
+ const distance = Math.abs(currentValue - value);
+
+ if (
+ acc === null ||
+ distance < acc.distance ||
+ distance === acc.distance
+ ) {
+ return {
+ distance,
+ index
+ };
+ }
+
+ return acc;
+ }, null) ?? {};
+
+ return closestIndex ?? -1;
+}
+
+type StyledSliderProps = Pick<
+ SliderProps,
+ 'orientation' | 'size' | 'variant'
+> & {
+ $disabled?: boolean;
+ hasMarks?: boolean;
+ isFocused?: boolean;
+};
+
+const Wrapper = styled.div`
+ display: inline-block;
+ position: relative;
+ touch-action: none;
+ &:before {
+ content: '';
+ display: inline-block;
+ position: absolute;
+ top: -2px;
+ left: -15px;
+ width: calc(100% + 30px);
+ height: ${({ hasMarks }) => (hasMarks ? '41px' : '39px')};
+ ${({ isFocused, theme }) =>
+ isFocused &&
+ `
+ outline: 2px dotted ${theme.materialText};
+ `}
+ }
+
+ ${({ orientation, size }) =>
+ orientation === 'vertical'
+ ? css`
+ height: ${size};
+ margin-right: 1.5rem;
+ &:before {
+ left: -6px;
+ top: -15px;
+ height: calc(100% + 30px);
+ width: ${({ hasMarks }) => (hasMarks ? '41px' : '39px')};
+ }
+ `
+ : css`
+ width: ${size};
+ margin-bottom: 1.5rem;
+ &:before {
+ top: -2px;
+ left: -15px;
+ width: calc(100% + 30px);
+ height: ${({ hasMarks }) => (hasMarks ? '41px' : '39px')};
+ }
+ `}
+
+ pointer-events: ${({ $disabled }) => ($disabled ? 'none' : 'auto')};
+`;
+const sharedGrooveStyles = () => css`
+ position: absolute;
+ ${({ orientation }) =>
+ orientation === 'vertical'
+ ? css`
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ height: 100%;
+ width: 8px;
+ `
+ : css`
+ left: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ height: 8px;
+ width: 100%;
+ `}
+`;
+const StyledGroove = styled(StyledCutout)`
+ ${sharedGrooveStyles()}
+`;
+const StyledFlatGroove = styled(StyledCutout)`
+ ${sharedGrooveStyles()}
+
+ border-left-color: ${({ theme }) => theme.flatLight};
+ border-top-color: ${({ theme }) => theme.flatLight};
+ border-right-color: ${({ theme }) => theme.canvas};
+ border-bottom-color: ${({ theme }) => theme.canvas};
+ &:before {
+ border-left-color: ${({ theme }) => theme.flatDark};
+ border-top-color: ${({ theme }) => theme.flatDark};
+ border-right-color: ${({ theme }) => theme.flatLight};
+ border-bottom-color: ${({ theme }) => theme.flatLight};
+ }
+`;
+const Thumb = styled.span`
+ position: relative;
+ ${({ orientation }) =>
+ orientation === 'vertical'
+ ? css`
+ width: 32px;
+ height: 18px;
+ right: 2px;
+ transform: translateY(-50%);
+ `
+ : css`
+ height: 32px;
+ width: 18px;
+ top: 2px;
+ transform: translateX(-50%);
+ `}
+ ${({ variant }) =>
+ variant === 'flat'
+ ? css`
+ ${createFlatBoxStyles()}
+ outline: 2px solid ${({ theme }) => theme.flatDark};
+ background: ${({ theme }) => theme.flatLight};
+ `
+ : css`
+ ${createBoxStyles()}
+ ${createBorderStyles()}
+ &:focus {
+ outline: none;
+ }
+ `}
+ ${({ $disabled, theme }) =>
+ $disabled &&
+ createHatchedBackground({
+ mainColor: theme.material,
+ secondaryColor: theme.borderLightest
+ })}
+`;
+
+const tickHeight = 6;
+const Tick = styled.span`
+ display: inline-block;
+ position: absolute;
+
+ ${({ orientation }) =>
+ orientation === 'vertical'
+ ? css`
+ right: ${-tickHeight - 2}px;
+ bottom: 0px;
+ transform: translateY(1px);
+ width: ${tickHeight}px;
+ border-bottom: 2px solid ${({ theme }) => theme.materialText};
+ `
+ : css`
+ bottom: ${-tickHeight}px;
+ height: ${tickHeight}px;
+ transform: translateX(-1px);
+ border-left: 1px solid ${({ theme }) => theme.materialText};
+ border-right: 1px solid ${({ theme }) => theme.materialText};
+ `}
+
+ color: ${({ theme }) => theme.materialText};
+ ${({ $disabled, theme }) =>
+ $disabled &&
+ css`
+ ${createDisabledTextStyles()}
+ box-shadow: 1px 1px 0px ${theme.materialTextDisabledShadow};
+ border-color: ${theme.materialTextDisabled};
+ `}
+`;
+const Mark = styled.div`
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ line-height: 1;
+ font-size: 0.875rem;
+
+ ${({ orientation }) =>
+ orientation === 'vertical'
+ ? css`
+ transform: translate(${tickHeight + 2}px, ${tickHeight + 1}px);
+ `
+ : css`
+ transform: translate(-0.5ch, calc(100% + 2px));
+ `}
+`;
+
+const Slider = forwardRef(function Slider(
+ {
+ defaultValue,
+ disabled = false,
+ marks: marksProp = false,
+ max = 100,
+ min = 0,
+ name,
+ onChange,
+ onChangeCommitted,
+ onMouseDown,
+ orientation = 'horizontal',
+ size = '100%',
+ step = 1,
+ value,
+ variant = 'default',
+ ...otherProps
+ },
+ ref
+) {
+ const Groove = variant === 'flat' ? StyledFlatGroove : StyledGroove;
+ const vertical = orientation === 'vertical';
+ const [valueDerived, setValueState] = useControlledOrUncontrolled({
+ value,
+ defaultValue
+ });
+
+ const {
+ isFocusVisible,
+ onBlurVisible,
+ ref: focusVisibleRef
+ } = useIsFocusVisible();
+ const [focusVisible, setFocusVisible] = useState(false);
+ const sliderRef = useRef();
+ const thumbRef = useRef(null);
+ const handleFocusRef = useForkRef(focusVisibleRef, sliderRef);
+ const handleRef = useForkRef(ref, handleFocusRef);
+
+ const handleFocus = useCallback(
+ (event: React.FocusEvent) => {
+ if (isFocusVisible(event)) {
+ setFocusVisible(true);
+ }
+ },
+ [isFocusVisible]
+ );
+
+ const handleBlur = useCallback(() => {
+ if (focusVisible !== false) {
+ setFocusVisible(false);
+ onBlurVisible();
+ }
+ }, [focusVisible, onBlurVisible]);
+
+ const touchId = useRef();
+
+ const marks = useMemo(
+ () =>
+ marksProp === true && Number.isFinite(step)
+ ? [...Array(Math.round((max - min) / (step as number)) + 1)].map(
+ (_, index) => ({
+ label: undefined,
+ value: min + (step as number) * index
+ })
+ )
+ : Array.isArray(marksProp)
+ ? marksProp
+ : [],
+ [marksProp, max, min, step]
+ );
+
+ const handleKeyDown = useCallback(
+ (event: React.KeyboardEvent) => {
+ const tenPercents = (max - min) / 10;
+ const marksValues = marks.map(mark => mark.value);
+ const marksIndex = marksValues.indexOf(valueDerived);
+ let newValue;
+
+ switch (event.key) {
+ case 'Home':
+ newValue = min;
+ break;
+ case 'End':
+ newValue = max;
+ break;
+ case 'PageUp':
+ if (step) {
+ newValue = valueDerived + tenPercents;
+ }
+ break;
+ case 'PageDown':
+ if (step) {
+ newValue = valueDerived - tenPercents;
+ }
+ break;
+ case 'ArrowRight':
+ case 'ArrowUp':
+ if (step) {
+ newValue = valueDerived + step;
+ } else {
+ newValue =
+ marksValues[marksIndex + 1] ||
+ marksValues[marksValues.length - 1];
+ }
+ break;
+ case 'ArrowLeft':
+ case 'ArrowDown':
+ if (step) {
+ newValue = valueDerived - step;
+ } else {
+ newValue = marksValues[marksIndex - 1] || marksValues[0];
+ }
+ break;
+ default:
+ return;
+ }
+
+ // Prevent scroll of the page
+ event.preventDefault();
+ if (step) {
+ newValue = roundValueToStep(newValue, step, min);
+ }
+
+ newValue = clamp(newValue, min, max);
+
+ setValueState(newValue);
+ setFocusVisible(true);
+
+ onChange?.(event, newValue);
+ onChangeCommitted?.(event, newValue);
+ },
+ [
+ marks,
+ max,
+ min,
+ onChange,
+ onChangeCommitted,
+ setValueState,
+ step,
+ valueDerived
+ ]
+ );
+
+ const getNewValue = useCallback(
+ (finger: { x: number; y: number }) => {
+ if (!sliderRef.current) {
+ return 0;
+ }
+ const rect = sliderRef.current.getBoundingClientRect();
+
+ let percent;
+ if (vertical) {
+ percent = (rect.bottom - finger.y) / rect.height;
+ } else {
+ percent = (finger.x - rect.left) / rect.width;
+ }
+ let newValue;
+
+ newValue = percentToValue(percent, min, max);
+ if (step) {
+ newValue = roundValueToStep(newValue, step, min);
+ } else {
+ const marksValues = marks.map(mark => mark.value);
+ const closestIndex = findClosest(marksValues, newValue);
+ newValue = marksValues[closestIndex];
+ }
+ newValue = clamp(newValue, min, max);
+ return newValue;
+ },
+ [marks, max, min, step, vertical]
+ );
+
+ const handleTouchMove = useCallback(
+ (event: MouseEvent | TouchEvent) => {
+ const finger = trackFinger(event, touchId.current);
+
+ if (!finger) {
+ return;
+ }
+ const newValue = getNewValue(finger);
+
+ thumbRef.current?.focus();
+ setValueState(newValue);
+ setFocusVisible(true);
+
+ onChange?.(event, newValue);
+ },
+ [getNewValue, onChange, setValueState]
+ );
+
+ const handleTouchEnd = useCallback(
+ (event: MouseEvent | TouchEvent) => {
+ const finger = trackFinger(event, touchId.current);
+
+ if (!finger) {
+ return;
+ }
+
+ const newValue = getNewValue(finger);
+
+ onChangeCommitted?.(event, newValue);
+
+ touchId.current = undefined;
+
+ const doc = ownerDocument(sliderRef.current);
+ doc.removeEventListener('mousemove', handleTouchMove);
+ doc.removeEventListener('mouseup', handleTouchEnd);
+ doc.removeEventListener('touchmove', handleTouchMove);
+ doc.removeEventListener('touchend', handleTouchEnd);
+ },
+ [getNewValue, handleTouchMove, onChangeCommitted]
+ );
+
+ const handleMouseDown = useCallback(
+ (event: React.MouseEvent) => {
+ // TODO should we also pass event together with new value to callbacks? (same thing with other input components)
+ onMouseDown?.(event);
+
+ event.preventDefault();
+ thumbRef.current?.focus();
+ setFocusVisible(true);
+
+ const finger = trackFinger(event, touchId.current);
+ if (finger) {
+ const newValue = getNewValue(finger);
+ setValueState(newValue);
+ onChange?.(event, newValue);
+ }
+
+ const doc = ownerDocument(sliderRef.current);
+ doc.addEventListener('mousemove', handleTouchMove);
+ doc.addEventListener('mouseup', handleTouchEnd);
+ },
+ [
+ getNewValue,
+ handleTouchEnd,
+ handleTouchMove,
+ onChange,
+ onMouseDown,
+ setValueState
+ ]
+ );
+
+ const handleTouchStart = useCallback(
+ (event: TouchEvent) => {
+ // Workaround as Safari has partial support for touchAction: 'none'.
+ event.preventDefault();
+ const touch = event.changedTouches[0];
+ if (touch != null) {
+ // A number that uniquely identifies the current finger in the touch session.
+ touchId.current = touch.identifier;
+ }
+
+ thumbRef.current?.focus();
+ setFocusVisible(true);
+
+ const finger = trackFinger(event, touchId.current);
+ if (finger) {
+ const newValue = getNewValue(finger);
+ setValueState(newValue);
+ onChange?.(event, newValue);
+ }
+
+ const doc = ownerDocument(sliderRef.current);
+ doc.addEventListener('touchmove', handleTouchMove);
+ doc.addEventListener('touchend', handleTouchEnd);
+ },
+ [getNewValue, handleTouchEnd, handleTouchMove, onChange, setValueState]
+ );
+
+ useEffect(() => {
+ const { current: slider } = sliderRef;
+ slider?.addEventListener('touchstart', handleTouchStart);
+ const doc = ownerDocument(slider);
+
+ return () => {
+ slider?.removeEventListener('touchstart', handleTouchStart);
+ doc.removeEventListener('mousemove', handleTouchMove);
+ doc.removeEventListener('mouseup', handleTouchEnd);
+ doc.removeEventListener('touchmove', handleTouchMove);
+ doc.removeEventListener('touchend', handleTouchEnd);
+ };
+ }, [handleTouchEnd, handleTouchMove, handleTouchStart]);
+
+ return (
+
+ {/* should we keep the hidden input ? */}
+
+ {marks &&
+ marks.map(m => (
+
+ {m.label && (
+
+ {m.label}
+
+ )}
+
+ ))}
+
+
+
+ );
+});
+
+export { Slider, SliderProps };
diff --git a/src/common/hooks/useIsFocusVisible.ts b/src/common/hooks/useIsFocusVisible.ts
index b3a0af5e..f4192ade 100644
--- a/src/common/hooks/useIsFocusVisible.ts
+++ b/src/common/hooks/useIsFocusVisible.ts
@@ -5,9 +5,9 @@ import * as ReactDOM from 'react-dom';
let hadKeyboardEvent = true;
let hadFocusVisibleRecently = false;
-let hadFocusVisibleRecentlyTimeout = null;
+let hadFocusVisibleRecentlyTimeout: number;
-const inputTypesWhitelist = {
+const inputTypesWhitelist: Record = {
text: true,
search: true,
url: true,
@@ -30,18 +30,22 @@ const inputTypesWhitelist = {
* @param {Element} node
* @return {boolean}
*/
-function focusTriggersKeyboardModality(node) {
- const { type, tagName } = node;
-
- if (tagName === 'INPUT' && inputTypesWhitelist[type] && !node.readOnly) {
- return true;
- }
+function focusTriggersKeyboardModality(
+ node: Element | HTMLElement | HTMLInputElement
+) {
+ if ('type' in node) {
+ const { type, tagName } = node;
+
+ if (tagName === 'INPUT' && inputTypesWhitelist[type] && !node.readOnly) {
+ return true;
+ }
- if (tagName === 'TEXTAREA' && !node.readOnly) {
- return true;
+ if (tagName === 'TEXTAREA' && !node.readOnly) {
+ return true;
+ }
}
- if (node.isContentEditable) {
+ if ('isContentEditable' in node && node.isContentEditable) {
return true;
}
@@ -55,7 +59,7 @@ function focusTriggersKeyboardModality(node) {
* then the modality is keyboard. Otherwise, the modality is not keyboard.
* @param {KeyboardEvent} event
*/
-function handleKeyDown(event) {
+function handleKeyDown(event: KeyboardEvent) {
if (event.metaKey || event.altKey || event.ctrlKey) {
return;
}
@@ -73,7 +77,7 @@ function handlePointerDown() {
hadKeyboardEvent = false;
}
-function handleVisibilityChange() {
+function handleVisibilityChange(this: Document) {
if (this.visibilityState === 'hidden') {
// If the tab becomes active again, the browser will handle calling focus
// on the element (Safari actually calls it twice).
@@ -85,7 +89,7 @@ function handleVisibilityChange() {
}
}
-function prepare(doc) {
+function prepare(doc: Document) {
doc.addEventListener('keydown', handleKeyDown, true);
doc.addEventListener('mousedown', handlePointerDown, true);
doc.addEventListener('pointerdown', handlePointerDown, true);
@@ -93,7 +97,7 @@ function prepare(doc) {
doc.addEventListener('visibilitychange', handleVisibilityChange, true);
}
-export function teardown(doc) {
+export function teardown(doc: Document) {
doc.removeEventListener('keydown', handleKeyDown, true);
doc.removeEventListener('mousedown', handlePointerDown, true);
doc.removeEventListener('pointerdown', handlePointerDown, true);
@@ -101,7 +105,7 @@ export function teardown(doc) {
doc.removeEventListener('visibilitychange', handleVisibilityChange, true);
}
-function isFocusVisible(event) {
+function isFocusVisible(event: React.FocusEvent) {
const { target } = event;
try {
return target.matches(':focus-visible');
@@ -132,8 +136,8 @@ function handleBlurVisible() {
}, 100);
}
-export function useIsFocusVisible() {
- const ref = React.useCallback(instance => {
+export function useIsFocusVisible() {
+ const ref = React.useCallback((instance: T) => {
// eslint-disable-next-line react/no-find-dom-node
const node = ReactDOM.findDOMNode(instance);
if (node != null) {
diff --git a/src/index.ts b/src/index.ts
index 845ad31c..590d7e60 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -24,7 +24,7 @@ export * from './Panel/Panel';
export * from './Progress/Progress';
export * from './Radio/Radio';
export * from './Select/Select';
-export { default as Slider } from './Slider/Slider';
+export * from './Slider/Slider';
export { default as Tab } from './Tab/Tab';
export { default as TabBody } from './TabBody/TabBody';
export { default as Table } from './Table/Table';
diff --git a/test/utils.tsx b/test/utils.tsx
index cbfb19f6..c44a405b 100644
--- a/test/utils.tsx
+++ b/test/utils.tsx
@@ -10,29 +10,53 @@ export const renderWithTheme = (component: React.ReactNode) =>
render({component});
export class Touch {
- instance: any;
-
- constructor(instance: any) {
- this.instance = instance;
+ #identifier: number;
+
+ #clientX = 0;
+
+ #clientY = 0;
+
+ #pageX = 0;
+
+ #pageY = 0;
+
+ constructor({
+ identifier,
+ clientX = 0,
+ clientY = 0,
+ pageX = 0,
+ pageY = 0
+ }: {
+ identifier: number;
+ clientX?: number;
+ clientY?: number;
+ pageX?: number;
+ pageY?: number;
+ }) {
+ this.#identifier = identifier;
+ this.#clientX = clientX;
+ this.#clientY = clientY;
+ this.#pageX = pageX;
+ this.#pageY = pageY;
}
get identifier() {
- return this.instance.identifier;
+ return this.#identifier;
}
get pageX() {
- return this.instance.pageX;
+ return this.#pageX;
}
get pageY() {
- return this.instance.pageY;
+ return this.#pageY;
}
get clientX() {
- return this.instance.clientX;
+ return this.#clientX;
}
get clientY() {
- return this.instance.clientY;
+ return this.#clientY;
}
}