diff --git a/packages/material-ui/src/Slider/Slider.js b/packages/material-ui/src/Slider/Slider.js index 157bdaaabc12ff..22492e9f43520d 100644 --- a/packages/material-ui/src/Slider/Slider.js +++ b/packages/material-ui/src/Slider/Slider.js @@ -124,10 +124,6 @@ const axisProps = { offset: percent => ({ bottom: `${percent}%` }), leap: percent => ({ height: `${percent}%` }), }, - 'vertical-reverse': { - offset: percent => ({ top: `${percent}%` }), - leap: percent => ({ height: `${percent}%` }), - }, }; const defaultMarks = []; diff --git a/packages/material-ui/src/Slider/Slider.test.js b/packages/material-ui/src/Slider/Slider.test.js index 3c6add08dc57d0..501b3c8f4022d9 100644 --- a/packages/material-ui/src/Slider/Slider.test.js +++ b/packages/material-ui/src/Slider/Slider.test.js @@ -1,13 +1,12 @@ import React from 'react'; -import { spy } from 'sinon'; -import { assert } from 'chai'; -import { - createMount, - getClasses, - findOutermostIntrinsic, - wrapsIntrinsicElement, -} from '@material-ui/core/test-utils'; +import PropTypes from 'prop-types'; +import { spy, stub } from 'sinon'; +import { expect } from 'chai'; +import { createMount, getClasses } from '@material-ui/core/test-utils'; import describeConformance from '@material-ui/core/test-utils/describeConformance'; +import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'; +import { cleanup, createClientRender, fireEvent } from 'test/utils/createClientRender'; +import consoleErrorMock from 'test/utils/consoleErrorMock'; import Slider from './Slider'; function touchList(touchArray) { @@ -27,14 +26,17 @@ function fireBodyMouseEvent(name, properties = {}) { describe('', () => { let mount; + let render; let classes; before(() => { + render = createClientRender({ strict: true }); classes = getClasses(); - mount = createMount({ strict: false }); + mount = createMount({ strict: true }); }); after(() => { + cleanup(); mount.cleanUp(); }); @@ -46,167 +48,177 @@ describe('', () => { testComponentPropWith: 'span', })); - function findThumb(wrapper) { - // Will also match any other react component if not filtered. They won't appear in the DOM - // and are therefore an implementation detail. We're interested in what the user - // interacts with. - return wrapper.find('[role="slider"]').filterWhere(wrapsIntrinsicElement); - } - it('should call handlers', () => { const handleChange = spy(); const handleChangeCommitted = spy(); - const wrapper = mount( + const { container, getByRole } = render( , ); - wrapper.simulate('click'); - wrapper.simulate('mousedown'); - // document.simulate('mouseup') + fireEvent.mouseDown(container.firstChild); document.body.dispatchEvent(new window.MouseEvent('mouseup')); - assert.strictEqual(handleChange.callCount, 1); - assert.strictEqual(handleChangeCommitted.callCount, 1); + expect(handleChange.callCount).to.equal(1); + expect(handleChangeCommitted.callCount).to.equal(1); - assert.strictEqual(handleChange.args[0].length, 2); - assert.strictEqual(handleChangeCommitted.args[0].length, 2); + fireEvent.keyDown(getByRole('slider'), { + key: 'Home', + }); + expect(handleChange.callCount).to.equal(2); + expect(handleChangeCommitted.callCount).to.equal(2); }); it('should only listen to changes from the same touchpoint', () => { const handleChange = spy(); const handleChangeCommitted = spy(); - const touches = [{ pageX: 0, pageY: 0 }]; - const wrapper = mount( + const { container } = render( , ); const event = fireBodyMouseEvent('touchstart', { changedTouches: touchList([{ identifier: 1 }]), - touches, }); - wrapper.getDOMNode().dispatchEvent(event); - assert.strictEqual(handleChange.callCount, 1); - assert.strictEqual(handleChangeCommitted.callCount, 0); + container.firstChild.dispatchEvent(event); + expect(handleChange.callCount).to.equal(1); + expect(handleChangeCommitted.callCount).to.equal(0); fireBodyMouseEvent('touchend', { changedTouches: touchList([{ identifier: 2 }]), - touches, }); - assert.strictEqual(handleChange.callCount, 1); - assert.strictEqual(handleChangeCommitted.callCount, 0); + expect(handleChange.callCount).to.equal(1); + expect(handleChangeCommitted.callCount).to.equal(0); fireBodyMouseEvent('touchmove', { changedTouches: touchList([{ identifier: 1 }]), - touches, }); - assert.strictEqual(handleChange.callCount, 2); - assert.strictEqual(handleChangeCommitted.callCount, 0); + expect(handleChange.callCount).to.equal(2); + expect(handleChangeCommitted.callCount).to.equal(0); fireBodyMouseEvent('touchend', { changedTouches: touchList([{ identifier: 1 }]), - touches, - }); - assert.strictEqual(handleChange.callCount, 2); - assert.strictEqual(handleChangeCommitted.callCount, 1); - }); - - describe('when mouse leaves window', () => { - it('should move to the end', () => { - const handleChange = spy(); - - const wrapper = mount(); - - wrapper.simulate('mousedown'); - document.body.dispatchEvent(new window.MouseEvent('mouseleave')); - - assert.strictEqual(handleChange.callCount, 1); }); + expect(handleChange.callCount).to.equal(2); + expect(handleChangeCommitted.callCount).to.equal(1); }); describe('when mouse reenters window', () => { it('should update if mouse is still clicked', () => { const handleChange = spy(); + const { container } = render(); - const wrapper = mount(); - - wrapper.simulate('mousedown'); + fireEvent.mouseDown(container.firstChild); document.body.dispatchEvent(new window.MouseEvent('mouseleave')); - const mouseEnter = new window.Event('mouseenter'); mouseEnter.buttons = 1; document.body.dispatchEvent(mouseEnter); - document.body.dispatchEvent(new window.MouseEvent('mousemove')); + expect(handleChange.callCount).to.equal(1); - assert.strictEqual(handleChange.callCount, 2); + document.body.dispatchEvent(new window.MouseEvent('mousemove')); + expect(handleChange.callCount).to.equal(2); }); it('should not update if mouse is not clicked', () => { const handleChange = spy(); + const { container } = render(); - const wrapper = mount(); - - wrapper.simulate('mousedown'); + fireEvent.mouseDown(container.firstChild); document.body.dispatchEvent(new window.MouseEvent('mouseleave')); - const mouseEnter = new window.Event('mouseenter'); mouseEnter.buttons = 0; document.body.dispatchEvent(mouseEnter); - document.body.dispatchEvent(new window.MouseEvent('mousemove')); + expect(handleChange.callCount).to.equal(1); - assert.strictEqual(handleChange.callCount, 1); + document.body.dispatchEvent(new window.MouseEvent('mousemove')); + expect(handleChange.callCount).to.equal(1); }); }); - describe('unmount', () => { - it('should not have global event listeners registered after unmount', () => { - const handleChange = spy(); - const handleChangeCommitted = spy(); - - const wrapper = mount( - , - ); + describe('prop: orientation', () => { + it('should render with the default and vertical classes', () => { + const { container } = render(); + expect(container.firstChild).to.have.class(classes.vertical); + }); + }); - const callGlobalListeners = () => { - document.body.dispatchEvent(new window.MouseEvent('mousemove')); - document.body.dispatchEvent(new window.MouseEvent('mouseup')); - }; - - wrapper.simulate('mousedown'); - callGlobalListeners(); - // pre condition: the dispatched event actually did something when mounted - assert.strictEqual(handleChange.callCount, 2); - assert.strictEqual(handleChangeCommitted.callCount, 1); - wrapper.unmount(); - // After unmounting global listeners should not be registered anymore since that would - // break component encapsulation. If they are still mounted either react will throw warnings - // or other component logic throws. - // post condition: the dispatched events dont cause errors/warnings - callGlobalListeners(); - assert.strictEqual(handleChange.callCount, 2); - assert.strictEqual(handleChangeCommitted.callCount, 1); + describe('range', () => { + it('should support keyboard', () => { + const { container } = render(); + const thumb1 = container.querySelectorAll('[role="slider"]')[0]; + const thumb2 = container.querySelectorAll('[role="slider"]')[1]; + + fireEvent.keyDown(thumb1, { + key: 'ArrowRight', + }); + expect(thumb1.getAttribute('aria-valuenow')).to.equal('21'); + + fireEvent.keyDown(thumb2, { + key: 'ArrowLeft', + }); + expect(thumb2.getAttribute('aria-valuenow')).to.equal('29'); }); }); - describe('prop: orientation', () => { - it('should render with the default and vertical classes', () => { - const wrapper = mount(); - assert.strictEqual( - wrapper - .find(`.${classes.root}`) - .first() - .hasClass(classes.vertical), - true, + describe('prop: step', () => { + it('should handle a null step', () => { + const { getByRole, container } = render( + , ); + stub(container.firstChild, 'getBoundingClientRect').callsFake(() => ({ + width: 100, + height: 10, + bottom: 10, + left: 0, + })); + const thumb = getByRole('slider'); + + const touches = { pageX: 21, pageY: 0 }; + const event = fireBodyMouseEvent('touchstart', { + changedTouches: touchList([{ identifier: 1, ...touches }]), + }); + container.firstChild.dispatchEvent(event); + expect(thumb.getAttribute('aria-valuenow')).to.equal('20'); }); }); describe('prop: disabled', () => { it('should render the disabled classes', () => { - const wrapper = mount(); - assert.strictEqual(findOutermostIntrinsic(wrapper).hasClass(classes.disabled), true); + const { container } = render(); + expect(container.firstChild).to.have.class(classes.disabled); }); }); describe('keyboard', () => { - let wrapper; + it('should handle all the keys', () => { + const { getByRole } = render(); + const thumb = getByRole('slider'); + + fireEvent.keyDown(thumb, { + key: 'Home', + }); + expect(thumb.getAttribute('aria-valuenow')).to.equal('0'); + + fireEvent.keyDown(thumb, { + key: 'End', + }); + expect(thumb.getAttribute('aria-valuenow')).to.equal('100'); + + fireEvent.keyDown(thumb, { + key: 'PageDown', + }); + expect(thumb.getAttribute('aria-valuenow')).to.equal('90'); + + fireEvent.keyDown(thumb, { + key: 'Escape', + }); + expect(thumb.getAttribute('aria-valuenow')).to.equal('90'); + + fireEvent.keyDown(thumb, { + key: 'PageUp', + }); + expect(thumb.getAttribute('aria-valuenow')).to.equal('100'); + }); const moveLeftEvent = { key: 'ArrowLeft', @@ -215,99 +227,151 @@ describe('', () => { key: 'ArrowRight', }; - before(() => { - const onChange = (_, value) => { - wrapper.setProps({ value }); - }; - wrapper = mount(); - }); - it('should reach right edge value', () => { - wrapper.setProps({ value: 90 }); - const thumb = findThumb(wrapper); + const { getByRole } = render(); + const thumb = getByRole('slider'); - thumb.simulate('keydown', moveRightEvent); - assert.strictEqual(wrapper.props().value, 100); + fireEvent.keyDown(thumb, moveRightEvent); + expect(thumb.getAttribute('aria-valuenow')).to.equal('100'); - thumb.simulate('keydown', moveRightEvent); - assert.strictEqual(wrapper.props().value, 108); + fireEvent.keyDown(thumb, moveRightEvent); + expect(thumb.getAttribute('aria-valuenow')).to.equal('108'); - thumb.simulate('keydown', moveLeftEvent); - assert.strictEqual(wrapper.props().value, 100); + fireEvent.keyDown(thumb, moveLeftEvent); + expect(thumb.getAttribute('aria-valuenow')).to.equal('100'); - thumb.simulate('keydown', moveLeftEvent); - assert.strictEqual(wrapper.props().value, 90); + fireEvent.keyDown(thumb, moveLeftEvent); + expect(thumb.getAttribute('aria-valuenow')).to.equal('90'); }); it('should reach left edge value', () => { - wrapper.setProps({ value: 20 }); - const thumb = findThumb(wrapper); - thumb.simulate('keydown', moveLeftEvent); - assert.strictEqual(wrapper.props().value, 10); + const { getByRole } = render(); + const thumb = getByRole('slider'); + + fireEvent.keyDown(thumb, moveLeftEvent); + expect(thumb.getAttribute('aria-valuenow')).to.equal('10'); - thumb.simulate('keydown', moveLeftEvent); - assert.strictEqual(wrapper.props().value, 6); + fireEvent.keyDown(thumb, moveLeftEvent); + expect(thumb.getAttribute('aria-valuenow')).to.equal('6'); - thumb.simulate('keydown', moveRightEvent); - assert.strictEqual(wrapper.props().value, 20); + fireEvent.keyDown(thumb, moveRightEvent); + expect(thumb.getAttribute('aria-valuenow')).to.equal('20'); - thumb.simulate('keydown', moveRightEvent); - assert.strictEqual(wrapper.props().value, 30); + fireEvent.keyDown(thumb, moveRightEvent); + expect(thumb.getAttribute('aria-valuenow')).to.equal('30'); }); it('should round value to step precision', () => { - wrapper.setProps({ value: 0.2, step: 0.1, min: 0 }); - const thumb = findThumb(wrapper); - thumb.simulate('keydown', moveRightEvent); - assert.strictEqual(wrapper.props().value, 0.3); + const { getByRole } = render(); + const thumb = getByRole('slider'); + + fireEvent.keyDown(thumb, moveRightEvent); + expect(thumb.getAttribute('aria-valuenow')).to.equal('0.3'); }); it('should not fail to round value to step precision when step is very small', () => { - wrapper.setProps({ value: 0.00000002, step: 0.00000001, min: 0, max: 0.00000005 }); - const thumb = findThumb(wrapper); - thumb.simulate('keydown', moveRightEvent); - assert.strictEqual(wrapper.props().value, 0.00000003); + const { getByRole } = render( + , + ); + const thumb = getByRole('slider'); + + fireEvent.keyDown(thumb, moveRightEvent); + expect(thumb.getAttribute('aria-valuenow')).to.equal('3e-8'); }); it('should not fail to round value to step precision when step is very small and negative', () => { - wrapper.setProps({ value: -0.00000002, step: 0.00000001, min: -0.00000005, max: 0 }); - const thumb = findThumb(wrapper); - thumb.simulate('keydown', moveLeftEvent); - assert.strictEqual(wrapper.props().value, -0.00000003); + const { getByRole } = render( + , + ); + const thumb = getByRole('slider'); + + fireEvent.keyDown(thumb, moveLeftEvent); + expect(thumb.getAttribute('aria-valuenow')).to.equal('-3e-8'); + }); + }); + + describe('prop: valueLabelDisplay', () => { + it('should always display the value label', () => { + const { getByRole, setProps } = render(); + const thumb = getByRole('slider'); + expect(thumb.textContent).to.equal('50'); + setProps({ + valueLabelDisplay: 'off', + }); + expect(thumb.textContent).to.equal(''); }); }); describe('markActive state', () => { - function getActives(wrapper) { - return wrapper - .find(`.${classes.markLabel}`) - .map(node => node.hasClass(classes.markLabelActive)); + function getActives(container) { + return Array.from(container.querySelectorAll(`.${classes.markLabel}`)).map(node => + node.classList.contains(classes.markLabelActive), + ); } it('sets the marks active that are `within` the value', () => { const marks = [{ value: 5 }, { value: 10 }, { value: 15 }]; - const singleValueWrapper = mount( + const { container: container1 } = render( , ); - assert.deepEqual(getActives(singleValueWrapper), [true, true, false]); + expect(getActives(container1)).to.deep.equal([true, true, false]); - const rangeValueWrapper = mount( + const { container: container2 } = render( , ); - assert.deepEqual(getActives(rangeValueWrapper), [false, true, false]); + expect(getActives(container2)).to.deep.equal([false, true, false]); }); it('uses closed intervals for the within check', () => { - const exactMarkWrapper = mount( + const { container: container1 } = render( , ); - assert.deepEqual(getActives(exactMarkWrapper), [true, true, true]); + expect(getActives(container1)).to.deep.equal([true, true, true]); - const ofByOneWrapper = mount( + const { container: container2 } = render( , ); - assert.deepEqual(getActives(ofByOneWrapper), [true, true, false]); + expect(getActives(container2)).to.deep.equal([true, true, false]); + }); + }); + + it('should forward mouseDown', () => { + const handleMouseDown = spy(); + const { container } = render(); + fireEvent.mouseDown(container.firstChild); + expect(handleMouseDown.callCount).to.equal(1); + }); + + it('should handle RTL', () => { + const { getByRole } = render( + + + , + ); + const thumb = getByRole('slider'); + expect(thumb.style.right).to.equal('30%'); + }); + + describe('warnings', () => { + beforeEach(() => { + consoleErrorMock.spy(); + }); + + afterEach(() => { + consoleErrorMock.reset(); + PropTypes.resetWarningCache(); + }); + + it('should warn if aria-valuetext is a string', () => { + render(); + expect(consoleErrorMock.args()[0][0]).to.include( + 'you need to use the `getAriaValueText` prop instead of', + ); }); }); }); diff --git a/packages/material-ui/src/test-utils/createMount.js b/packages/material-ui/src/test-utils/createMount.js index 3a4c14c6d5d8df..e0de01a86e8ad3 100644 --- a/packages/material-ui/src/test-utils/createMount.js +++ b/packages/material-ui/src/test-utils/createMount.js @@ -59,7 +59,9 @@ export default function createMount(options = {}) { mountWithContext.attachTo = attachTo; mountWithContext.cleanUp = () => { ReactDOM.unmountComponentAtNode(attachTo); - attachTo.parentNode.removeChild(attachTo); + if (attachTo.parentNode) { + attachTo.parentNode.removeChild(attachTo); + } }; return mountWithContext; diff --git a/packages/material-ui/src/test-utils/describeConformance.js b/packages/material-ui/src/test-utils/describeConformance.js index 932fd7e3093595..c4b91f7d7e9b30 100644 --- a/packages/material-ui/src/test-utils/describeConformance.js +++ b/packages/material-ui/src/test-utils/describeConformance.js @@ -1,5 +1,6 @@ import { assert } from 'chai'; import React from 'react'; +import ReactDOM from 'react-dom'; import findOutermostIntrinsic from './findOutermostIntrinsic'; import testRef from './testRef'; @@ -176,6 +177,13 @@ const fullSuite = { export default function describeConformance(minimalElement, getOptions) { const { only = Object.keys(fullSuite), skip = [] } = getOptions(); describe('Material-UI component API', () => { + after(() => { + const { mount } = getOptions(); + if (mount.attachTo) { + ReactDOM.unmountComponentAtNode(mount.attachTo); + } + }); + Object.keys(fullSuite) .filter(testKey => only.indexOf(testKey) !== -1 && skip.indexOf(testKey) === -1) .forEach(testKey => { diff --git a/pages/api/slider.md b/pages/api/slider.md index 590969bcb57d15..4f28dd346f05c2 100644 --- a/pages/api/slider.md +++ b/pages/api/slider.md @@ -76,7 +76,7 @@ you need to use the following style sheet name: `MuiSlider`. ## Notes -The component can cause issues in [StrictMode](https://reactjs.org/docs/strict-mode.html). +The component is fully [StrictMode](https://reactjs.org/docs/strict-mode.html) compatible. ## Demos