From 64f7ff20ed9bbffecc604c0f80892ae5324f6011 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Wed, 16 Sep 2020 17:41:01 +0200 Subject: [PATCH 01/40] Implement one-handle Slider as far as possible --- .../slider/stories/Slider.stories.tsx | 36 ++--- packages/@react-spectrum/slider/README.md | 3 + .../slider/chromatic/Slider.chromatic.tsx | 29 ++++ .../@react-spectrum/slider/docs/Slider.mdx | 72 +++++++++ packages/@react-spectrum/slider/index.ts | 13 ++ packages/@react-spectrum/slider/package.json | 44 ++++++ .../@react-spectrum/slider/src/Slider.tsx | 139 ++++++++++++++++++ packages/@react-spectrum/slider/src/index.ts | 15 ++ .../slider/stories/Slider.stories.tsx | 100 +++++++++++++ .../slider/test/Slider.test.js | 26 ++++ packages/@react-types/slider/src/index.d.ts | 36 ++++- 11 files changed, 494 insertions(+), 19 deletions(-) create mode 100644 packages/@react-spectrum/slider/README.md create mode 100644 packages/@react-spectrum/slider/chromatic/Slider.chromatic.tsx create mode 100644 packages/@react-spectrum/slider/docs/Slider.mdx create mode 100644 packages/@react-spectrum/slider/index.ts create mode 100644 packages/@react-spectrum/slider/package.json create mode 100644 packages/@react-spectrum/slider/src/Slider.tsx create mode 100644 packages/@react-spectrum/slider/src/index.ts create mode 100644 packages/@react-spectrum/slider/stories/Slider.stories.tsx create mode 100644 packages/@react-spectrum/slider/test/Slider.test.js diff --git a/packages/@react-aria/slider/stories/Slider.stories.tsx b/packages/@react-aria/slider/stories/Slider.stories.tsx index 2c7184175cc..ed55875f1d7 100644 --- a/packages/@react-aria/slider/stories/Slider.stories.tsx +++ b/packages/@react-aria/slider/stories/Slider.stories.tsx @@ -6,7 +6,7 @@ import {StoryRangeSlider} from './StoryRangeSlider'; import {StorySlider} from './StorySlider'; -storiesOf('Slider', module) +storiesOf('Slider (hooks)', module) .add( 'single', () => @@ -25,11 +25,11 @@ storiesOf('Slider', module) ) .add( 'range', - () => ( ( ( ( ( - @@ -68,9 +68,9 @@ storiesOf('Slider', module) .add( '3 thumbs with disabled', () => ( - @@ -82,9 +82,9 @@ storiesOf('Slider', module) .add( '3 thumbs with aria-label', () => ( - diff --git a/packages/@react-spectrum/slider/README.md b/packages/@react-spectrum/slider/README.md new file mode 100644 index 00000000000..03d6b6d83ba --- /dev/null +++ b/packages/@react-spectrum/slider/README.md @@ -0,0 +1,3 @@ +# @react-spectrum/slider + +This package is part of [react-spectrum](https://github.com/adobe/react-spectrum). See the repo for more details. diff --git a/packages/@react-spectrum/slider/chromatic/Slider.chromatic.tsx b/packages/@react-spectrum/slider/chromatic/Slider.chromatic.tsx new file mode 100644 index 00000000000..a9383e61139 --- /dev/null +++ b/packages/@react-spectrum/slider/chromatic/Slider.chromatic.tsx @@ -0,0 +1,29 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { Slider } from '../'; +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { SpectrumSliderProps } from '@react-types/slider'; + +storiesOf('Slider', module) + .add( + 'name me', + () => render({label: "Label"}) + ); + +function render(props: SpectrumSliderProps = {}) { + return ( + + + ); +} diff --git a/packages/@react-spectrum/slider/docs/Slider.mdx b/packages/@react-spectrum/slider/docs/Slider.mdx new file mode 100644 index 00000000000..2e504b71cc2 --- /dev/null +++ b/packages/@react-spectrum/slider/docs/Slider.mdx @@ -0,0 +1,72 @@ + + +import {Layout} from '@react-spectrum/docs'; +export default Layout; + +import docs from 'docs:@react-spectrum/slider'; +import {HeaderInfo, PropTable} from '@react-spectrum/docs'; +import packageData from '@react-spectrum/slider/package.json'; + +```jsx import +import {Slider} from '@react-spectrum/slider'; +``` + +--- +category: Category Name +keywords: [] +--- + +# Slider + +

{docs.exports.Slider.description}

+ + + +## Example + +```tsx example +Button +``` + +## Content + +*If the component has a children prop that accepts any type of content (e.g. `ReactNode`), include this section. Please include a note about how to internationalize the content.* + +## Value + +*If the component displays or allows a user to input a value, include this section.* + +## Labeling + +*If the component supports a label prop, include this section. Please include a note about how to internationalize the content.* + +## Events + +*If the component supports event props, include this section. Only cover the events that are important to the main functionality of the component.* + +## Validation + +*If the component supports validation props, include this section.* + +## Props + + + +## Visual options + +*Show examples of all variants and visual props here with links to the design website for more usage details. Examples can be grouped together for conciseness.* + +### Sample Option +[View guidelines](https://spectrum.adobe.com/page/text-field/#Width) diff --git a/packages/@react-spectrum/slider/index.ts b/packages/@react-spectrum/slider/index.ts new file mode 100644 index 00000000000..1210ae1e402 --- /dev/null +++ b/packages/@react-spectrum/slider/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +export * from './src'; diff --git a/packages/@react-spectrum/slider/package.json b/packages/@react-spectrum/slider/package.json new file mode 100644 index 00000000000..8e796257980 --- /dev/null +++ b/packages/@react-spectrum/slider/package.json @@ -0,0 +1,44 @@ +{ + "name": "@react-spectrum/slider", + "version": "3.0.0-alpha.1", + "private": true, + "description": "Spectrum UI components in React", + "license": "Apache-2.0", + "main": "dist/main.js", + "module": "dist/module.js", + "types": "dist/types.d.ts", + "source": "src/index.ts", + "files": ["dist"], + "sideEffects": [ + "*.css" + ], + "targets": { + "main": { + "includeNodeModules": ["@adobe/spectrum-css-temp"] + }, + "module": { + "includeNodeModules": ["@adobe/spectrum-css-temp"] + } + }, + "repository": { + "type": "git", + "url": "https://github.com/adobe/react-spectrum" + }, + "dependencies": { + "@babel/runtime": "^7.6.2", + "@react-aria/slider": "^3.0.0-alpha.1", + "@react-aria/utils": "^3.0.0-alpha.1", + "@react-spectrum/utils": "^3.0.0-alpha.1", + "@react-stately/slider": "^3.0.0-alpha.1" + }, + "devDependencies": { + "@adobe/spectrum-css-temp": "^3.0.0-alpha.1" + }, + "peerDependencies": { + "react": "^16.8.0", + "@react-spectrum/provider": "^3.0.0-rc.1" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx new file mode 100644 index 00000000000..ec1cbb1d81c --- /dev/null +++ b/packages/@react-spectrum/slider/src/Slider.tsx @@ -0,0 +1,139 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { SliderProps, SpectrumSliderProps } from '@react-types/slider'; +import React, { useRef } from 'react'; +import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; +import { useSlider, useSliderThumb } from '@react-aria/slider'; +import { useSliderState } from '@react-stately/slider'; +import { DOMRef } from '@react-types/shared'; +import { FocusRing } from '@react-aria/focus'; +import { VisuallyHidden } from '@adobe/react-spectrum'; +import { classNames, useDOMRef } from '@react-spectrum/utils'; +import { useProviderProps } from '@react-spectrum/provider'; +import { useHover } from '@react-aria/interactions'; +import { mergeProps } from '@react-aria/utils'; + +function Slider(props: SpectrumSliderProps, ref: DOMRef) { + // needed? + useDOMRef(ref); + props = useProviderProps(props); + + let { + labelPosition = "top", isFilled, fillOffset, trackBackground, formatOptions, valueLabel, showValueLabel = !!props.label, + onChange, value, defaultValue, + tickCount, showTickLabels, tickLabels, + ...otherProps } = props; + + let { hoverProps, isHovered } = useHover({/* isDisabled */ }); + + let inputRef = useRef(); + let trackRef = useRef(); + + // Assumes that DEFAULT_MIN_VALUE and DEFAULT_MAX_VALUE are both positive, this value needs to be passed to useSliderState, so + // getThumbMinValue/getThumbMaxValue cannot be used here. + let alwaysDisplaySign = props.minValue != null && props.maxValue != null && Math.sign(props.minValue) !== Math.sign(props.maxValue); + + let ariaProps: SliderProps = { + ...otherProps, + // @ts-ignore + formatOptions: formatOptions ?? { signDisplay: alwaysDisplaySign ? "exceptZero" : "auto" }, + // Normalize `value: number[]` to `value: number` + value: value != null ? [value] : undefined, + defaultValue: defaultValue != null ? [defaultValue] : undefined, + onChange(v) { + onChange?.(v[0]); + } + }; + + let state = useSliderState(ariaProps); + let { + containerProps, + trackProps, + labelProps + } = useSlider(ariaProps, state, trackRef); + + let { thumbProps, inputProps } = useSliderThumb({ + index: 0, + isReadOnly: props.isReadOnly, + isDisabled: props.isDisabled, + trackRef, + inputRef + }, state); + + let displayValue = valueLabel ?? state.getThumbValueLabel(0); + let labelNode = ; + let valueNode =
{displayValue}
+ + let leftTrack =
; + let rightTrack =
; + + let filledTrack = null; + if (isFilled && fillOffset != null) { + let width = state.getThumbPercent(0) - state.getValuePercent(fillOffset); + let isRightOfOffset = width > 0; + let left = isRightOfOffset ? state.getValuePercent(fillOffset) : state.getThumbPercent(0); + filledTrack = +
+ } + + let ticks = null; + if (tickCount > 0) { + let tickList = []; + for (let i = 0; i < tickCount; i++) { + tickList.push( +
+ {showTickLabels &&
{state.getFormattedValue(state.getPercentValue(i / (tickCount - 1)))}
} +
+ ); + } + ticks =
+ {tickList} +
+ } + + return ( +
+ {labelPosition === "top" && (props.label || showValueLabel) && +
+ {props.label && labelNode} + {showValueLabel && valueNode} +
+ } +
+ {labelPosition === "side" && props.label && labelNode} + {leftTrack} + {ticks} + +
+ + + +
+
+ {rightTrack} + {filledTrack} + {labelPosition === "side" && showValueLabel && valueNode} +
+
); +} + +const _Slider = React.forwardRef(Slider); +export { _Slider as Slider }; diff --git a/packages/@react-spectrum/slider/src/index.ts b/packages/@react-spectrum/slider/src/index.ts new file mode 100644 index 00000000000..38c9c883f89 --- /dev/null +++ b/packages/@react-spectrum/slider/src/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/// + +export * from './Slider'; diff --git a/packages/@react-spectrum/slider/stories/Slider.stories.tsx b/packages/@react-spectrum/slider/stories/Slider.stories.tsx new file mode 100644 index 00000000000..493b8f8bf3c --- /dev/null +++ b/packages/@react-spectrum/slider/stories/Slider.stories.tsx @@ -0,0 +1,100 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { action } from '@storybook/addon-actions'; +import { Slider } from '../'; +import React, { useState } from 'react'; +import { storiesOf } from '@storybook/react'; +import { SpectrumSliderProps } from '@react-types/slider'; + +storiesOf('Slider', module) + .add( + 'Default', + () => render({ "aria-label": "Label" }) + ) + .add( + 'label', + () => render({ label: "Label" }) + ) + .add( + 'label overflow', + () => render({ label: "This is a rather long label for this narrow slider element.", maxValue: 1000 }, "100px") + ) + .add( + 'showValueLabel: false', + () => render({ label: "Label", showValueLabel: false }) + ) + .add( + 'formatOptions percent', + () => render({ label: "Label", minValue: 0, maxValue: 1, step: 0.01, formatOptions: { style: "percent" } }) + ) + .add( + 'formatOptions centimeter', + // @ts-ignore TODO why is "unit" even missing? How well is it supported? + () => render({ label: "Label", maxValue: 1000, formatOptions: { style: "unit", unit: "centimeter" } }) + ) + .add( + 'custom valueLabel', + () => { + let [state, setState] = useState(0); + return render({ label: "Label", value: state, onChange: setState, valueLabel: `A ${state} B` }); + } + ) + .add( + '* labelPosition: side', + () => render({ label: "Label", labelPosition: "side" }) + ) + .add( + 'min/max', + () => render({ label: "Label", minValue: 30, maxValue: 70 }) + ) + .add( + 'isFilled: true', + () => render({ label: "Label", isFilled: true }) + ) + .add( + 'fillOffset', + () => render({ label: "Exposure", isFilled: true, fillOffset: 0, defaultValue: 0, minValue: -7, maxValue: 5 }) + ) + .add( + 'ticks', + () => render({ label: "Label", tickCount: 4 }) + ) + .add( + 'showTickLabels: true', + () => render({ label: "Label", tickCount: 4, showTickLabels: true }) + ) + .add( + 'tickLabels', + () => render({ label: "Label", tickCount: 3, showTickLabels: true, tickLabels: ["A", "B", "C"] }) + ) + .add( + '* trackBackground', + () => render({ label: "Label", trackBackground: "linear-gradient(to right, blue, red)" }) + ) + .add( + '* trackBackground with fillOffset', + () => render({ label: "Label", trackBackground: "linear-gradient(to right, blue, red)", isFilled: true, fillOffset: 50 }) + ) + .add( + '* orientation: vertical', + () => render({ label: "Label", orientation: "vertical" }) + ); + +function render(props: SpectrumSliderProps = {}, width = "200px") { + if (props.onChange == null) { + props.onChange = action('change'); + } + return
+ +
; +} diff --git a/packages/@react-spectrum/slider/test/Slider.test.js b/packages/@react-spectrum/slider/test/Slider.test.js new file mode 100644 index 00000000000..befb10b254d --- /dev/null +++ b/packages/@react-spectrum/slider/test/Slider.test.js @@ -0,0 +1,26 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +// import React from 'react'; +// import {render} from '@testing-library/react'; +import Slider from '../'; + +describe('Slider', function () { + it.each` + Name | Component | props + ${'Slider'} | ${Slider} | ${{}} + `('$Name handles defaults', function ({Component, props}) { + // let {getByRole, getByText} = render(); + + // expect(true).toBeTruthy(); + }); +}); diff --git a/packages/@react-types/slider/src/index.d.ts b/packages/@react-types/slider/src/index.d.ts index 07062d8f5b7..4a1a3931794 100644 --- a/packages/@react-types/slider/src/index.d.ts +++ b/packages/@react-types/slider/src/index.d.ts @@ -1,4 +1,5 @@ -import {AriaLabelingProps, AriaValidationProps, FocusableDOMProps, FocusableProps, LabelableProps, RangeInputBase, Validation, ValueBase} from '@react-types/shared'; +import { AriaLabelingProps, AriaValidationProps, FocusableDOMProps, FocusableProps, LabelableProps, LabelPosition, Orientation, RangeInputBase, RangeValue, Validation, ValueBase } from '@react-types/shared'; +import { ReactNode } from 'react'; export interface BaseSliderProps extends RangeInputBase, LabelableProps, AriaLabelingProps { isReadOnly?: boolean, @@ -15,3 +16,36 @@ export interface SliderThumbProps extends AriaLabelingProps, FocusableDOMProps, isDisabled?: boolean, index: number } + +export interface SpectrumBarSliderBase extends BaseSliderProps, ValueBase { + orientation?: Orientation, + labelPosition?: LabelPosition, + /** Whether the value's label is displayed. True by default if there's a `label`, false by default if not. */ + showValueLabel?: boolean, + /** The content to display as the value's label. Overrides default formatted number. */ + valueLabel?: ReactNode +} + +export interface SpectrumSliderTicksBase { + /** Enables tick marks if > 0. Ticks will be evenly distributed between the min and max values. */ + tickCount?: number, + + /** Enables tick labels. */ + showTickLabels?: boolean, + /** + * By default, labels are formatted using the slider's number formatter, + * but you can use the tickLabels prop to override these with custom labels. + */ + tickLabels?: Array +} + +export interface SpectrumSliderProps extends SpectrumBarSliderBase, SpectrumSliderTicksBase { + /** Whether a fill color is shown between the start of the slider and the current value. See https://spectrum.adobe.com/page/slider/#Fill */ + isFilled?: boolean, + /** The offset from which to start the fill. See https://spectrum.adobe.com/page/slider/#Fill-start */ + fillOffset?: number, + /** The background of the track, e.g. a CSS linear-gradient(). See https://spectrum.adobe.com/page/slider/#Gradient */ + trackBackground?: string +} + +export interface SpectrumRangeSlider extends SpectrumBarSliderBase>, SpectrumSliderTicksBase { } From ae2d0ab37c05d8be9d6f6860b9c685539aa5996f Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Thu, 17 Sep 2020 12:17:47 +0200 Subject: [PATCH 02/40] Implement track background using background-* CSS --- .../components/slider/skin.css | 2 ++ packages/@react-spectrum/slider/src/Slider.tsx | 18 ++++++++++++++++-- .../slider/stories/Slider.stories.tsx | 4 ++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/@adobe/spectrum-css-temp/components/slider/skin.css b/packages/@adobe/spectrum-css-temp/components/slider/skin.css index 8ed5b802e45..4625cd17cb0 100644 --- a/packages/@adobe/spectrum-css-temp/components/slider/skin.css +++ b/packages/@adobe/spectrum-css-temp/components/slider/skin.css @@ -21,6 +21,8 @@ governing permissions and limitations under the License. .spectrum-Slider-track { &::before { background: var(--spectrum-slider-track-color); + background-size: var(--spectrum-track-background-size); + background-position: var(--spectrum-track-background-position); } } diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx index ec1cbb1d81c..082a3702b49 100644 --- a/packages/@react-spectrum/slider/src/Slider.tsx +++ b/packages/@react-spectrum/slider/src/Slider.tsx @@ -74,8 +74,22 @@ function Slider(props: SpectrumSliderProps, ref: DOMRef) { let labelNode = ; let valueNode =
{displayValue}
- let leftTrack =
; - let rightTrack =
; + let leftTrack =
; + let rightTrack =
; let filledTrack = null; if (isFilled && fillOffset != null) { diff --git a/packages/@react-spectrum/slider/stories/Slider.stories.tsx b/packages/@react-spectrum/slider/stories/Slider.stories.tsx index 493b8f8bf3c..1597da89742 100644 --- a/packages/@react-spectrum/slider/stories/Slider.stories.tsx +++ b/packages/@react-spectrum/slider/stories/Slider.stories.tsx @@ -78,11 +78,11 @@ storiesOf('Slider', module) () => render({ label: "Label", tickCount: 3, showTickLabels: true, tickLabels: ["A", "B", "C"] }) ) .add( - '* trackBackground', + 'trackBackground', () => render({ label: "Label", trackBackground: "linear-gradient(to right, blue, red)" }) ) .add( - '* trackBackground with fillOffset', + 'trackBackground with fillOffset', () => render({ label: "Label", trackBackground: "linear-gradient(to right, blue, red)", isFilled: true, fillOffset: 50 }) ) .add( From a352071692ae881e60fea0ce7a9879653349f87f Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Thu, 17 Sep 2020 12:32:48 +0200 Subject: [PATCH 03/40] Clamp fillOffset, isDisabled --- .../@react-spectrum/slider/src/Slider.tsx | 20 ++++++++++++++----- .../slider/stories/Slider.stories.tsx | 4 ++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx index 082a3702b49..8c07b31ee70 100644 --- a/packages/@react-spectrum/slider/src/Slider.tsx +++ b/packages/@react-spectrum/slider/src/Slider.tsx @@ -61,7 +61,6 @@ function Slider(props: SpectrumSliderProps, ref: DOMRef) { trackProps, labelProps } = useSlider(ariaProps, state, trackRef); - let { thumbProps, inputProps } = useSliderThumb({ index: 0, isReadOnly: props.isReadOnly, @@ -70,6 +69,8 @@ function Slider(props: SpectrumSliderProps, ref: DOMRef) { inputRef }, state); + fillOffset = fillOffset != null ? Math.max(state.getThumbMinValue(0), Math.min(fillOffset, state.getThumbMaxValue(0))) : fillOffset; + let displayValue = valueLabel ?? state.getThumbValueLabel(0); let labelNode = ; let valueNode =
{displayValue}
@@ -120,11 +121,20 @@ function Slider(props: SpectrumSliderProps, ref: DOMRef) {
} + return ( -
+
{labelPosition === "top" && (props.label || showValueLabel) &&
{props.label && labelNode} diff --git a/packages/@react-spectrum/slider/stories/Slider.stories.tsx b/packages/@react-spectrum/slider/stories/Slider.stories.tsx index 1597da89742..0466ec9f8d2 100644 --- a/packages/@react-spectrum/slider/stories/Slider.stories.tsx +++ b/packages/@react-spectrum/slider/stories/Slider.stories.tsx @@ -25,6 +25,10 @@ storiesOf('Slider', module) 'label', () => render({ label: "Label" }) ) + .add( + 'disabled', + () => render({ label: "Label", defaultValue: 50, isDisabled: true }) + ) .add( 'label overflow', () => render({ label: "This is a rather long label for this narrow slider element.", maxValue: 1000 }, "100px") From b61970e920eab5a24d90f1e55f6b58779d04b927 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Thu, 17 Sep 2020 12:49:50 +0200 Subject: [PATCH 04/40] Overwrite signDisplay if not explicitly specified --- packages/@react-spectrum/slider/src/Slider.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx index 8c07b31ee70..d337c458028 100644 --- a/packages/@react-spectrum/slider/src/Slider.tsx +++ b/packages/@react-spectrum/slider/src/Slider.tsx @@ -43,10 +43,22 @@ function Slider(props: SpectrumSliderProps, ref: DOMRef) { // getThumbMinValue/getThumbMaxValue cannot be used here. let alwaysDisplaySign = props.minValue != null && props.maxValue != null && Math.sign(props.minValue) !== Math.sign(props.maxValue); + if (alwaysDisplaySign) { + if (formatOptions != null) { + if (!("signDisplay" in formatOptions)) { + // @ts-ignore + formatOptions.signDisplay = "exceptZero"; + } + } else { + // @ts-ignore + formatOptions = { signDisplay: "exceptZero" }; + } + } + let ariaProps: SliderProps = { ...otherProps, // @ts-ignore - formatOptions: formatOptions ?? { signDisplay: alwaysDisplaySign ? "exceptZero" : "auto" }, + formatOptions: formatOptions, // Normalize `value: number[]` to `value: number` value: value != null ? [value] : undefined, defaultValue: defaultValue != null ? [defaultValue] : undefined, From 819bb8343cdec659e2a1bba8117f03af7cb94b14 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Thu, 17 Sep 2020 13:06:29 +0200 Subject: [PATCH 05/40] Hack labelPosition side into CSS --- .../spectrum-css-temp/components/slider/skin.css | 15 +++++++++++++++ packages/@react-spectrum/slider/src/Slider.tsx | 12 ++++++++---- .../slider/stories/Slider.stories.tsx | 2 +- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/@adobe/spectrum-css-temp/components/slider/skin.css b/packages/@adobe/spectrum-css-temp/components/slider/skin.css index 4625cd17cb0..ef1d9805a84 100644 --- a/packages/@adobe/spectrum-css-temp/components/slider/skin.css +++ b/packages/@adobe/spectrum-css-temp/components/slider/skin.css @@ -304,3 +304,18 @@ governing permissions and limitations under the License. } } } + +.spectrum-Slider--label-side { + display: flex; + gap: 7px; + align-items: center; + + & > * { + display: inline-block; + } + + & .spectrum-Slider-labelContainer { + padding-top: 0; + flex-shrink: 0; + } +} diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx index d337c458028..2608d1296ac 100644 --- a/packages/@react-spectrum/slider/src/Slider.tsx +++ b/packages/@react-spectrum/slider/src/Slider.tsx @@ -139,6 +139,7 @@ function Slider(props: SpectrumSliderProps, ref: DOMRef) { "spectrum-Slider", { 'spectrum-Slider--filled': isFilled && fillOffset == null, + 'spectrum-Slider--label-side': labelPosition === "side", 'is-disabled': props.isDisabled })} style={ @@ -147,14 +148,13 @@ function Slider(props: SpectrumSliderProps, ref: DOMRef) { } {...containerProps} > - {labelPosition === "top" && (props.label || showValueLabel) && + {(props.label) &&
{props.label && labelNode} - {showValueLabel && valueNode} + {labelPosition === "top" && showValueLabel && valueNode}
}
- {labelPosition === "side" && props.label && labelNode} {leftTrack} {ticks} @@ -166,8 +166,12 @@ function Slider(props: SpectrumSliderProps, ref: DOMRef) { {rightTrack} {filledTrack} - {labelPosition === "side" && showValueLabel && valueNode}
+ {labelPosition === "side" && +
+ {showValueLabel && valueNode} +
+ }
); } diff --git a/packages/@react-spectrum/slider/stories/Slider.stories.tsx b/packages/@react-spectrum/slider/stories/Slider.stories.tsx index 0466ec9f8d2..c53cec83ab0 100644 --- a/packages/@react-spectrum/slider/stories/Slider.stories.tsx +++ b/packages/@react-spectrum/slider/stories/Slider.stories.tsx @@ -54,7 +54,7 @@ storiesOf('Slider', module) } ) .add( - '* labelPosition: side', + 'labelPosition: side', () => render({ label: "Label", labelPosition: "side" }) ) .add( From eb283d38c703b8876daaa2ffd20b0852be3e764f Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Thu, 17 Sep 2020 15:18:23 +0200 Subject: [PATCH 06/40] Custom tickLabels and lint --- .../slider/chromatic/Slider.chromatic.tsx | 11 +- packages/@react-spectrum/slider/package.json | 21 ++- .../@react-spectrum/slider/src/Slider.tsx | 136 ++++++++++-------- .../slider/stories/Slider.stories.tsx | 52 +++---- packages/@react-types/slider/src/index.d.ts | 10 +- 5 files changed, 127 insertions(+), 103 deletions(-) diff --git a/packages/@react-spectrum/slider/chromatic/Slider.chromatic.tsx b/packages/@react-spectrum/slider/chromatic/Slider.chromatic.tsx index a9383e61139..84c276b09a8 100644 --- a/packages/@react-spectrum/slider/chromatic/Slider.chromatic.tsx +++ b/packages/@react-spectrum/slider/chromatic/Slider.chromatic.tsx @@ -10,20 +10,19 @@ * governing permissions and limitations under the License. */ -import { Slider } from '../'; import React from 'react'; -import { storiesOf } from '@storybook/react'; -import { SpectrumSliderProps } from '@react-types/slider'; +import {Slider} from '../'; +import {SpectrumSliderProps} from '@react-types/slider'; +import {storiesOf} from '@storybook/react'; storiesOf('Slider', module) .add( 'name me', - () => render({label: "Label"}) + () => render({label: 'Label'}) ); function render(props: SpectrumSliderProps = {}) { return ( - - + ); } diff --git a/packages/@react-spectrum/slider/package.json b/packages/@react-spectrum/slider/package.json index 8e796257980..3f2a871347c 100644 --- a/packages/@react-spectrum/slider/package.json +++ b/packages/@react-spectrum/slider/package.json @@ -8,16 +8,23 @@ "module": "dist/module.js", "types": "dist/types.d.ts", "source": "src/index.ts", - "files": ["dist"], + "files": [ + "dist", + "src" + ], "sideEffects": [ "*.css" ], "targets": { "main": { - "includeNodeModules": ["@adobe/spectrum-css-temp"] + "includeNodeModules": [ + "@adobe/spectrum-css-temp" + ] }, "module": { - "includeNodeModules": ["@adobe/spectrum-css-temp"] + "includeNodeModules": [ + "@adobe/spectrum-css-temp" + ] } }, "repository": { @@ -25,11 +32,17 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { + "@adobe/react-spectrum": "3.3.0", "@babel/runtime": "^7.6.2", + "@react-aria/focus": "3.2.1", + "@react-aria/i18n": "3.1.1", + "@react-aria/interactions": "3.2.0", "@react-aria/slider": "^3.0.0-alpha.1", "@react-aria/utils": "^3.0.0-alpha.1", "@react-spectrum/utils": "^3.0.0-alpha.1", - "@react-stately/slider": "^3.0.0-alpha.1" + "@react-stately/slider": "^3.0.0-alpha.1", + "@react-types/shared": "3.2.0", + "@react-types/slider": "3.0.0-alpha.0" }, "devDependencies": { "@adobe/spectrum-css-temp": "^3.0.0-alpha.1" diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx index 2608d1296ac..0e8bab88c4c 100644 --- a/packages/@react-spectrum/slider/src/Slider.tsx +++ b/packages/@react-spectrum/slider/src/Slider.tsx @@ -10,18 +10,18 @@ * governing permissions and limitations under the License. */ -import { SliderProps, SpectrumSliderProps } from '@react-types/slider'; -import React, { useRef } from 'react'; +import {classNames, useDOMRef} from '@react-spectrum/utils'; +import {DOMRef} from '@react-types/shared'; +import {FocusRing} from '@react-aria/focus'; +import {mergeProps} from '@react-aria/utils'; +import React, {useRef} from 'react'; +import {SliderProps, SpectrumSliderProps} from '@react-types/slider'; import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; -import { useSlider, useSliderThumb } from '@react-aria/slider'; -import { useSliderState } from '@react-stately/slider'; -import { DOMRef } from '@react-types/shared'; -import { FocusRing } from '@react-aria/focus'; -import { VisuallyHidden } from '@adobe/react-spectrum'; -import { classNames, useDOMRef } from '@react-spectrum/utils'; -import { useProviderProps } from '@react-spectrum/provider'; -import { useHover } from '@react-aria/interactions'; -import { mergeProps } from '@react-aria/utils'; +import {useHover} from '@react-aria/interactions'; +import {useProviderProps} from '@react-spectrum/provider'; +import {useSlider, useSliderThumb} from '@react-aria/slider'; +import {useSliderState} from '@react-stately/slider'; +import {VisuallyHidden} from '@adobe/react-spectrum'; function Slider(props: SpectrumSliderProps, ref: DOMRef) { // needed? @@ -29,12 +29,12 @@ function Slider(props: SpectrumSliderProps, ref: DOMRef) { props = useProviderProps(props); let { - labelPosition = "top", isFilled, fillOffset, trackBackground, formatOptions, valueLabel, showValueLabel = !!props.label, + labelPosition = 'top', isFilled, fillOffset, trackBackground, formatOptions, valueLabel, showValueLabel = !!props.label, onChange, value, defaultValue, tickCount, showTickLabels, tickLabels, - ...otherProps } = props; + ...otherProps} = props; - let { hoverProps, isHovered } = useHover({/* isDisabled */ }); + let {hoverProps, isHovered} = useHover({/* isDisabled */ }); let inputRef = useRef(); let trackRef = useRef(); @@ -45,13 +45,13 @@ function Slider(props: SpectrumSliderProps, ref: DOMRef) { if (alwaysDisplaySign) { if (formatOptions != null) { - if (!("signDisplay" in formatOptions)) { + if (!('signDisplay' in formatOptions)) { // @ts-ignore - formatOptions.signDisplay = "exceptZero"; + formatOptions.signDisplay = 'exceptZero'; } } else { // @ts-ignore - formatOptions = { signDisplay: "exceptZero" }; + formatOptions = {signDisplay: 'exceptZero'}; } } @@ -73,7 +73,7 @@ function Slider(props: SpectrumSliderProps, ref: DOMRef) { trackProps, labelProps } = useSlider(ariaProps, state, trackRef); - let { thumbProps, inputProps } = useSliderThumb({ + let {thumbProps, inputProps} = useSliderThumb({ index: 0, isReadOnly: props.isReadOnly, isDisabled: props.isDisabled, @@ -84,25 +84,29 @@ function Slider(props: SpectrumSliderProps, ref: DOMRef) { fillOffset = fillOffset != null ? Math.max(state.getThumbMinValue(0), Math.min(fillOffset, state.getThumbMaxValue(0))) : fillOffset; let displayValue = valueLabel ?? state.getThumbValueLabel(0); - let labelNode = ; - let valueNode =
{displayValue}
- - let leftTrack =
; - let rightTrack =
; + let labelNode = ; + let valueNode =
{displayValue}
; + + let leftTrack = (
); + let rightTrack = (
); let filledTrack = null; if (isFilled && fillOffset != null) { @@ -110,65 +114,73 @@ function Slider(props: SpectrumSliderProps, ref: DOMRef) { let isRightOfOffset = width > 0; let left = isRightOfOffset ? state.getValuePercent(fillOffset) : state.getThumbPercent(0); filledTrack = -
+ }} />); } let ticks = null; if (tickCount > 0) { let tickList = []; for (let i = 0; i < tickCount; i++) { + let tickLabel = tickLabels ? tickLabels[i] : state.getFormattedValue(state.getPercentValue(i / (tickCount - 1))); tickList.push( -
- {showTickLabels &&
{state.getFormattedValue(state.getPercentValue(i / (tickCount - 1)))}
} +
+ {showTickLabels && +
+ {tickLabel} +
+ }
); } - ticks =
+ ticks = (
{tickList} -
+
); } return ( -
+ {...containerProps}> {(props.label) && -
+
{props.label && labelNode} - {labelPosition === "top" && showValueLabel && valueNode} + {labelPosition === 'top' && showValueLabel && valueNode}
} -
+
{leftTrack} {ticks} -
+
- +
{rightTrack} {filledTrack}
- {labelPosition === "side" && -
+ {labelPosition === 'side' && +
{showValueLabel && valueNode}
} @@ -176,4 +188,4 @@ function Slider(props: SpectrumSliderProps, ref: DOMRef) { } const _Slider = React.forwardRef(Slider); -export { _Slider as Slider }; +export {_Slider as Slider}; diff --git a/packages/@react-spectrum/slider/stories/Slider.stories.tsx b/packages/@react-spectrum/slider/stories/Slider.stories.tsx index c53cec83ab0..1a403c42c29 100644 --- a/packages/@react-spectrum/slider/stories/Slider.stories.tsx +++ b/packages/@react-spectrum/slider/stories/Slider.stories.tsx @@ -10,95 +10,95 @@ * governing permissions and limitations under the License. */ -import { action } from '@storybook/addon-actions'; -import { Slider } from '../'; -import React, { useState } from 'react'; -import { storiesOf } from '@storybook/react'; -import { SpectrumSliderProps } from '@react-types/slider'; +import {action} from '@storybook/addon-actions'; +import React, {useState} from 'react'; +import {Slider} from '../'; +import {SpectrumSliderProps} from '@react-types/slider'; +import {storiesOf} from '@storybook/react'; storiesOf('Slider', module) .add( 'Default', - () => render({ "aria-label": "Label" }) + () => render({'aria-label': 'Label'}) ) .add( 'label', - () => render({ label: "Label" }) + () => render({label: 'Label'}) ) .add( 'disabled', - () => render({ label: "Label", defaultValue: 50, isDisabled: true }) + () => render({label: 'Label', defaultValue: 50, isDisabled: true}) ) .add( 'label overflow', - () => render({ label: "This is a rather long label for this narrow slider element.", maxValue: 1000 }, "100px") + () => render({label: 'This is a rather long label for this narrow slider element.', maxValue: 1000}, '100px') ) .add( 'showValueLabel: false', - () => render({ label: "Label", showValueLabel: false }) + () => render({label: 'Label', showValueLabel: false}) ) .add( 'formatOptions percent', - () => render({ label: "Label", minValue: 0, maxValue: 1, step: 0.01, formatOptions: { style: "percent" } }) + () => render({label: 'Label', minValue: 0, maxValue: 1, step: 0.01, formatOptions: {style: 'percent'}}) ) .add( 'formatOptions centimeter', // @ts-ignore TODO why is "unit" even missing? How well is it supported? - () => render({ label: "Label", maxValue: 1000, formatOptions: { style: "unit", unit: "centimeter" } }) + () => render({label: 'Label', maxValue: 1000, formatOptions: {style: 'unit', unit: 'centimeter'}}) ) .add( 'custom valueLabel', () => { let [state, setState] = useState(0); - return render({ label: "Label", value: state, onChange: setState, valueLabel: `A ${state} B` }); + return render({label: 'Label', value: state, onChange: setState, valueLabel: `A ${state} B`}); } ) .add( 'labelPosition: side', - () => render({ label: "Label", labelPosition: "side" }) + () => render({label: 'Label', labelPosition: 'side'}) ) .add( 'min/max', - () => render({ label: "Label", minValue: 30, maxValue: 70 }) + () => render({label: 'Label', minValue: 30, maxValue: 70}) ) .add( 'isFilled: true', - () => render({ label: "Label", isFilled: true }) + () => render({label: 'Label', isFilled: true}) ) .add( 'fillOffset', - () => render({ label: "Exposure", isFilled: true, fillOffset: 0, defaultValue: 0, minValue: -7, maxValue: 5 }) + () => render({label: 'Exposure', isFilled: true, fillOffset: 0, defaultValue: 0, minValue: -7, maxValue: 5}) ) .add( 'ticks', - () => render({ label: "Label", tickCount: 4 }) + () => render({label: 'Label', tickCount: 4}) ) .add( 'showTickLabels: true', - () => render({ label: "Label", tickCount: 4, showTickLabels: true }) + () => render({label: 'Label', tickCount: 4, showTickLabels: true}) ) .add( 'tickLabels', - () => render({ label: "Label", tickCount: 3, showTickLabels: true, tickLabels: ["A", "B", "C"] }) + () => render({label: 'Label', tickCount: 3, showTickLabels: true, tickLabels: ['A', 'B', 'C']}) ) .add( 'trackBackground', - () => render({ label: "Label", trackBackground: "linear-gradient(to right, blue, red)" }) + () => render({label: 'Label', trackBackground: 'linear-gradient(to right, blue, red)'}) ) .add( 'trackBackground with fillOffset', - () => render({ label: "Label", trackBackground: "linear-gradient(to right, blue, red)", isFilled: true, fillOffset: 50 }) + () => render({label: 'Label', trackBackground: 'linear-gradient(to right, blue, red)', isFilled: true, fillOffset: 50}) ) .add( '* orientation: vertical', - () => render({ label: "Label", orientation: "vertical" }) + () => render({label: 'Label', orientation: 'vertical'}) ); -function render(props: SpectrumSliderProps = {}, width = "200px") { +function render(props: SpectrumSliderProps = {}, width = '200px') { if (props.onChange == null) { props.onChange = action('change'); } - return
+ return (
-
; +
); } diff --git a/packages/@react-types/slider/src/index.d.ts b/packages/@react-types/slider/src/index.d.ts index 4a1a3931794..e77dbe111bc 100644 --- a/packages/@react-types/slider/src/index.d.ts +++ b/packages/@react-types/slider/src/index.d.ts @@ -1,5 +1,5 @@ -import { AriaLabelingProps, AriaValidationProps, FocusableDOMProps, FocusableProps, LabelableProps, LabelPosition, Orientation, RangeInputBase, RangeValue, Validation, ValueBase } from '@react-types/shared'; -import { ReactNode } from 'react'; +import {AriaLabelingProps, AriaValidationProps, FocusableDOMProps, FocusableProps, LabelableProps, LabelPosition, Orientation, RangeInputBase, RangeValue, Validation, ValueBase} from '@react-types/shared'; +import {ReactNode} from 'react'; export interface BaseSliderProps extends RangeInputBase, LabelableProps, AriaLabelingProps { isReadOnly?: boolean, @@ -40,11 +40,11 @@ export interface SpectrumSliderTicksBase { } export interface SpectrumSliderProps extends SpectrumBarSliderBase, SpectrumSliderTicksBase { - /** Whether a fill color is shown between the start of the slider and the current value. See https://spectrum.adobe.com/page/slider/#Fill */ + /** Whether a fill color is shown between the start of the slider and the current value. See https://spectrum.adobe.com/page/slider/#Fill. */ isFilled?: boolean, - /** The offset from which to start the fill. See https://spectrum.adobe.com/page/slider/#Fill-start */ + /** The offset from which to start the fill. See https://spectrum.adobe.com/page/slider/#Fill-start. */ fillOffset?: number, - /** The background of the track, e.g. a CSS linear-gradient(). See https://spectrum.adobe.com/page/slider/#Gradient */ + /** The background of the track, e.g. a CSS linear-gradient(). See https://spectrum.adobe.com/page/slider/#Gradient. */ trackBackground?: string } From ce9ff913fc743c6b410e1f13eff0f23e0b71ce0b Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Thu, 17 Sep 2020 17:42:00 +0200 Subject: [PATCH 07/40] Fix sign, add RTL story --- packages/@react-spectrum/slider/src/Slider.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx index 0e8bab88c4c..e258ef56b38 100644 --- a/packages/@react-spectrum/slider/src/Slider.tsx +++ b/packages/@react-spectrum/slider/src/Slider.tsx @@ -41,7 +41,8 @@ function Slider(props: SpectrumSliderProps, ref: DOMRef) { // Assumes that DEFAULT_MIN_VALUE and DEFAULT_MAX_VALUE are both positive, this value needs to be passed to useSliderState, so // getThumbMinValue/getThumbMaxValue cannot be used here. - let alwaysDisplaySign = props.minValue != null && props.maxValue != null && Math.sign(props.minValue) !== Math.sign(props.maxValue); + // `Math.abs(Math.sign(a) - Math.sign(b)) === 2` is true if the values have a different sign and neither is null. + let alwaysDisplaySign = props.minValue != null && props.maxValue != null && Math.abs(Math.sign(props.minValue) - Math.sign(props.maxValue)) === 2; if (alwaysDisplaySign) { if (formatOptions != null) { From 9717da1210124aea63329c7f1bdb0288b393d74b Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Thu, 17 Sep 2020 23:00:23 +0200 Subject: [PATCH 08/40] Deduplicate `@types/react` to silence tsc errors --- package.json | 2 +- yarn.lock | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 8b3ed801ca1..47a7339cf26 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "@testing-library/react": "^10.4.9", "@testing-library/react-hooks": "^3.4.1", "@testing-library/user-event": "^12.1.3", - "@types/react": "16.9.23", + "@types/react": "^16.9.23", "@types/storybook__react": "^4.0.1", "@typescript-eslint/eslint-plugin": "^2.28.0", "@typescript-eslint/parser": "^2.28.0", diff --git a/yarn.lock b/yarn.lock index a97da377e0e..05a34541f7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4519,13 +4519,13 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@16.9.23": - version "16.9.23" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.23.tgz#1a66c6d468ba11a8943ad958a8cb3e737568271c" - integrity sha512-SsGVT4E7L2wLN3tPYLiF20hmZTPGuzaayVunfgXzUn1x4uHVsKH6QDJQ/TdpHqwsTLd4CwrmQ2vOgxN7gE24gw== +"@types/react@*", "@types/react@^16.9.23": + version "16.9.49" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.49.tgz#09db021cf8089aba0cdb12a49f8021a69cce4872" + integrity sha512-DtLFjSj0OYAdVLBbyjhuV9CdGVHCkHn2R+xr3XkBvK2rS1Y1tkc14XSGjYgm5Fjjr90AxH9tiSzc1pCFMGO06g== dependencies: "@types/prop-types" "*" - csstype "^2.2.0" + csstype "^3.0.2" "@types/stack-utils@^1.0.1": version "1.0.1" @@ -8340,11 +8340,16 @@ cssstyle@^2.2.0: dependencies: cssom "~0.3.6" -csstype@^2.2.0, csstype@^2.5.7: +csstype@^2.5.7: version "2.6.9" resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.9.tgz#05141d0cd557a56b8891394c1911c40c8a98d098" integrity sha512-xz39Sb4+OaTsULgUERcCk+TJj8ylkL4aSVDQiX/ksxbELSqwkgt4d4RD7fovIdgJGSuNYqwZEiVjYY5l0ask+Q== +csstype@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.3.tgz#2b410bbeba38ba9633353aff34b05d9755d065f8" + integrity sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag== + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" From 56d9913d41c7d62fd012613ee83babaefe65feb3 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Thu, 17 Sep 2020 17:42:15 +0200 Subject: [PATCH 09/40] PoC RangeSlider --- .../slider/src/RangeSlider.tsx | 182 ++++++++++++++++++ packages/@react-spectrum/slider/src/index.ts | 1 + .../slider/stories/RangeSlider.stories.tsx | 89 +++++++++ .../slider/stories/Slider.stories.tsx | 9 + packages/@react-types/slider/src/index.d.ts | 2 +- 5 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 packages/@react-spectrum/slider/src/RangeSlider.tsx create mode 100644 packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx diff --git a/packages/@react-spectrum/slider/src/RangeSlider.tsx b/packages/@react-spectrum/slider/src/RangeSlider.tsx new file mode 100644 index 00000000000..b46c7e3bfc3 --- /dev/null +++ b/packages/@react-spectrum/slider/src/RangeSlider.tsx @@ -0,0 +1,182 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {classNames, useDOMRef} from '@react-spectrum/utils'; +import {DOMRef} from '@react-types/shared'; +import {FocusRing} from '@react-aria/focus'; +import {mergeProps} from '@react-aria/utils'; +import React, {useRef} from 'react'; +import {SliderProps, SpectrumRangeSliderProps} from '@react-types/slider'; +import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; +import {useHover} from '@react-aria/interactions'; +import {useProviderProps} from '@react-spectrum/provider'; +import {useSlider, useSliderThumb} from '@react-aria/slider'; +import {DEFAULT_MAX_VALUE, DEFAULT_MIN_VALUE, useSliderState} from '@react-stately/slider'; +import {VisuallyHidden} from '@adobe/react-spectrum'; + +function RangeSlider(props: SpectrumRangeSliderProps, ref: DOMRef) { + // needed? + useDOMRef(ref); + props = useProviderProps(props); + + let { + labelPosition = 'top', formatOptions, valueLabel, showValueLabel = !!props.label, + onChange, value, defaultValue, + tickCount, showTickLabels, tickLabels, + ...otherProps} = props; + + let {hoverProps, isHovered} = useHover({/* isDisabled */ }); + + let inputMinRef = useRef(); + let inputMaxRef = useRef(); + let trackRef = useRef(); + + // Assumes that DEFAULT_MIN_VALUE and DEFAULT_MAX_VALUE are both positive, this value needs to be passed to useSliderState, so + // getThumbMinValue/getThumbMaxValue cannot be used here. + // `Math.abs(Math.sign(a) - Math.sign(b)) === 2` is true if the values have a different sign and neither is null. + let alwaysDisplaySign = props.minValue != null && props.maxValue != null && Math.abs(Math.sign(props.minValue) - Math.sign(props.maxValue)) === 2; + + if (alwaysDisplaySign) { + if (formatOptions != null) { + if (!('signDisplay' in formatOptions)) { + // @ts-ignore + formatOptions.signDisplay = 'exceptZero'; + } + } else { + // @ts-ignore + formatOptions = {signDisplay: 'exceptZero'}; + } + } + + let ariaProps: SliderProps = { + ...otherProps, + // @ts-ignore + formatOptions: formatOptions, + // Normalize `value: number[]` to `value: number` + value: value != null ? [value.start, value.end] : undefined, + defaultValue: defaultValue != null ? [defaultValue.start, defaultValue.end] : + // make sure that useSliderState knows we have two handles + [props.minValue ?? DEFAULT_MIN_VALUE, props.maxValue ?? DEFAULT_MAX_VALUE], + onChange(v) { + onChange?.({start: v[0], end: v[1]}); + } + }; + + let state = useSliderState(ariaProps); + let { + containerProps, + trackProps, + labelProps + } = useSlider(ariaProps, state, trackRef); + let {thumbProps: minThumbProps, inputProps: minInputProps} = useSliderThumb({ + index: 0, + isReadOnly: props.isReadOnly, + isDisabled: props.isDisabled, + trackRef, + inputRef: inputMinRef + }, state); + let {thumbProps: maxThumbProps, inputProps: maxInputProps} = useSliderThumb({ + index: 1, + isReadOnly: props.isReadOnly, + isDisabled: props.isDisabled, + trackRef, + inputRef: inputMaxRef + }, state); + + // TODO intl/rtl for ranges? + let displayValue = valueLabel ?? `${state.getThumbValueLabel(0)} - ${state.getThumbValueLabel(1)}`; + let labelNode = ; + let valueNode =
{displayValue}
; + + let leftTrack = (
); + let middleTrack = (
); + let rightTrack = (
); + + let ticks = null; + if (tickCount > 0) { + let tickList = []; + for (let i = 0; i < tickCount; i++) { + let tickLabel = tickLabels ? tickLabels[i] : state.getFormattedValue(state.getPercentValue(i / (tickCount - 1))); + tickList.push( +
+ {showTickLabels && +
+ {tickLabel} +
+ } +
+ ); + } + ticks = (
+ {tickList} +
); + } + + + return ( +
+ {(props.label) && +
+ {props.label && labelNode} + {labelPosition === 'top' && showValueLabel && valueNode} +
+ } +
+ {leftTrack} + {ticks} + +
+ + + +
+
+ {middleTrack} + +
+ + + +
+
+ {rightTrack} +
+ {labelPosition === 'side' && +
+ {showValueLabel && valueNode} +
+ } +
); +} + +const _RangeSlider = React.forwardRef(RangeSlider); +export {_RangeSlider as RangeSlider}; diff --git a/packages/@react-spectrum/slider/src/index.ts b/packages/@react-spectrum/slider/src/index.ts index 38c9c883f89..df218fa71cb 100644 --- a/packages/@react-spectrum/slider/src/index.ts +++ b/packages/@react-spectrum/slider/src/index.ts @@ -13,3 +13,4 @@ /// export * from './Slider'; +export * from './RangeSlider'; diff --git a/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx b/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx new file mode 100644 index 00000000000..e5fd0a78501 --- /dev/null +++ b/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx @@ -0,0 +1,89 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {action} from '@storybook/addon-actions'; +import {RangeSlider} from '../'; +import React, {useState} from 'react'; +import {SpectrumRangeSliderProps} from '@react-types/slider'; +import {storiesOf} from '@storybook/react'; +import {Provider} from '@adobe/react-spectrum'; + +storiesOf('RangeSlider', module) + .add( + 'Default', + () => render({'aria-label': 'Label'}) + ) + .add( + 'label', + () => render({label: 'Label'}) + ) + .add( + 'rtl', + () => {render({label: 'فهو يتحدّث بلغة '})} + ) + .add( + 'disabled', + () => render({label: 'Label', defaultValue: {start: 30, end: 50}, isDisabled: true}) + ) + .add( + 'label overflow', + () => render({label: 'This is a rather long label for this narrow slider element.', maxValue: 1000}, '100px') + ) + .add( + 'showValueLabel: false', + () => render({label: 'Label', showValueLabel: false}) + ) + .add( + 'formatOptions percent', + () => render({label: 'Label', minValue: 0, maxValue: 1, step: 0.01, formatOptions: {style: 'percent'}}) + ) + .add( + 'formatOptions centimeter', + // @ts-ignore TODO why is "unit" even missing? How well is it supported? + () => render({label: 'Label', maxValue: 1000, formatOptions: {style: 'unit', unit: 'centimeter'}}) + ) + .add( + 'custom valueLabel', + () => { + let [state, setState] = useState({start: 20, end: 50}); + return render({label: 'Label', value: state, onChange: setState, valueLabel: `${state.start} <-> ${state.end}`}); + } + ) + .add( + 'labelPosition: side', + () => render({label: 'Label', labelPosition: 'side'}) + ) + .add( + 'min/max', + () => render({label: 'Label', minValue: 30, maxValue: 70}) + ) + .add( + 'ticks', + () => render({label: 'Label', tickCount: 4}) + ) + .add( + 'showTickLabels: true', + () => render({label: 'Label', tickCount: 4, showTickLabels: true}) + ) + .add( + 'tickLabels', + () => render({label: 'Label', tickCount: 3, showTickLabels: true, tickLabels: ['A', 'B', 'C']}) + ); + +function render(props: SpectrumRangeSliderProps = {}, width = '200px') { + if (props.onChange == null) { + props.onChange = action('change'); + } + return (
+ +
); +} diff --git a/packages/@react-spectrum/slider/stories/Slider.stories.tsx b/packages/@react-spectrum/slider/stories/Slider.stories.tsx index 1a403c42c29..4069e1a86ef 100644 --- a/packages/@react-spectrum/slider/stories/Slider.stories.tsx +++ b/packages/@react-spectrum/slider/stories/Slider.stories.tsx @@ -11,6 +11,7 @@ */ import {action} from '@storybook/addon-actions'; +import {Provider} from '@adobe/react-spectrum'; import React, {useState} from 'react'; import {Slider} from '../'; import {SpectrumSliderProps} from '@react-types/slider'; @@ -25,6 +26,10 @@ storiesOf('Slider', module) 'label', () => render({label: 'Label'}) ) + .add( + 'rtl', + () => {render({label: 'فهو يتحدّث بلغة '})} + ) .add( 'disabled', () => render({label: 'Label', defaultValue: 50, isDisabled: true}) @@ -81,6 +86,10 @@ storiesOf('Slider', module) 'tickLabels', () => render({label: 'Label', tickCount: 3, showTickLabels: true, tickLabels: ['A', 'B', 'C']}) ) + .add( + 'rtl trackBackground', + () => {render({label: 'فهو يتحدّث بلغة ', trackBackground: 'linear-gradient(to right, blue, red)'})} + ) .add( 'trackBackground', () => render({label: 'Label', trackBackground: 'linear-gradient(to right, blue, red)'}) diff --git a/packages/@react-types/slider/src/index.d.ts b/packages/@react-types/slider/src/index.d.ts index e77dbe111bc..f189f788b8a 100644 --- a/packages/@react-types/slider/src/index.d.ts +++ b/packages/@react-types/slider/src/index.d.ts @@ -48,4 +48,4 @@ export interface SpectrumSliderProps extends SpectrumBarSliderBase, Spec trackBackground?: string } -export interface SpectrumRangeSlider extends SpectrumBarSliderBase>, SpectrumSliderTicksBase { } +export interface SpectrumRangeSliderProps extends SpectrumBarSliderBase>, SpectrumSliderTicksBase { } From c8092f5a0ccdafe8cded90893219a743ea9e3531 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Thu, 17 Sep 2020 19:29:43 +0200 Subject: [PATCH 10/40] Abstract into SliderBase --- .../slider/src/RangeSlider.tsx | 174 ++++---------- .../@react-spectrum/slider/src/Slider.tsx | 149 +++--------- .../@react-spectrum/slider/src/SliderBase.tsx | 218 ++++++++++++++++++ .../slider/stories/RangeSlider.stories.tsx | 6 +- 4 files changed, 291 insertions(+), 256 deletions(-) create mode 100644 packages/@react-spectrum/slider/src/SliderBase.tsx diff --git a/packages/@react-spectrum/slider/src/RangeSlider.tsx b/packages/@react-spectrum/slider/src/RangeSlider.tsx index b46c7e3bfc3..6dd5190870b 100644 --- a/packages/@react-spectrum/slider/src/RangeSlider.tsx +++ b/packages/@react-spectrum/slider/src/RangeSlider.tsx @@ -10,57 +10,20 @@ * governing permissions and limitations under the License. */ -import {classNames, useDOMRef} from '@react-spectrum/utils'; -import {DOMRef} from '@react-types/shared'; +import {classNames} from '@react-spectrum/utils'; +import {DEFAULT_MAX_VALUE, DEFAULT_MIN_VALUE} from '@react-stately/slider'; import {FocusRing} from '@react-aria/focus'; -import {mergeProps} from '@react-aria/utils'; -import React, {useRef} from 'react'; +import React from 'react'; +import {SliderBase, useSliderBase} from './SliderBase'; import {SliderProps, SpectrumRangeSliderProps} from '@react-types/slider'; import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; -import {useHover} from '@react-aria/interactions'; -import {useProviderProps} from '@react-spectrum/provider'; -import {useSlider, useSliderThumb} from '@react-aria/slider'; -import {DEFAULT_MAX_VALUE, DEFAULT_MIN_VALUE, useSliderState} from '@react-stately/slider'; import {VisuallyHidden} from '@adobe/react-spectrum'; -function RangeSlider(props: SpectrumRangeSliderProps, ref: DOMRef) { - // needed? - useDOMRef(ref); - props = useProviderProps(props); - - let { - labelPosition = 'top', formatOptions, valueLabel, showValueLabel = !!props.label, - onChange, value, defaultValue, - tickCount, showTickLabels, tickLabels, - ...otherProps} = props; - - let {hoverProps, isHovered} = useHover({/* isDisabled */ }); - - let inputMinRef = useRef(); - let inputMaxRef = useRef(); - let trackRef = useRef(); - - // Assumes that DEFAULT_MIN_VALUE and DEFAULT_MAX_VALUE are both positive, this value needs to be passed to useSliderState, so - // getThumbMinValue/getThumbMaxValue cannot be used here. - // `Math.abs(Math.sign(a) - Math.sign(b)) === 2` is true if the values have a different sign and neither is null. - let alwaysDisplaySign = props.minValue != null && props.maxValue != null && Math.abs(Math.sign(props.minValue) - Math.sign(props.maxValue)) === 2; - - if (alwaysDisplaySign) { - if (formatOptions != null) { - if (!('signDisplay' in formatOptions)) { - // @ts-ignore - formatOptions.signDisplay = 'exceptZero'; - } - } else { - // @ts-ignore - formatOptions = {signDisplay: 'exceptZero'}; - } - } +function RangeSlider(props: SpectrumRangeSliderProps) { + let {onChange, value, defaultValue, ...otherProps} = props; let ariaProps: SliderProps = { ...otherProps, - // @ts-ignore - formatOptions: formatOptions, // Normalize `value: number[]` to `value: number` value: value != null ? [value.start, value.end] : undefined, defaultValue: defaultValue != null ? [defaultValue.start, defaultValue.end] : @@ -71,31 +34,12 @@ function RangeSlider(props: SpectrumRangeSliderProps, ref: DOMRef) { } }; - let state = useSliderState(ariaProps); - let { - containerProps, - trackProps, - labelProps - } = useSlider(ariaProps, state, trackRef); - let {thumbProps: minThumbProps, inputProps: minInputProps} = useSliderThumb({ - index: 0, - isReadOnly: props.isReadOnly, - isDisabled: props.isDisabled, - trackRef, - inputRef: inputMinRef - }, state); - let {thumbProps: maxThumbProps, inputProps: maxInputProps} = useSliderThumb({ - index: 1, - isReadOnly: props.isReadOnly, - isDisabled: props.isDisabled, - trackRef, - inputRef: inputMaxRef - }, state); + let {inputRefs, + thumbProps, + inputProps, ticks, + isHovered, ...containerProps} = useSliderBase(2, ariaProps); - // TODO intl/rtl for ranges? - let displayValue = valueLabel ?? `${state.getThumbValueLabel(0)} - ${state.getThumbValueLabel(1)}`; - let labelNode = ; - let valueNode =
{displayValue}
; + let {state} = containerProps; let leftTrack = (
); - let ticks = null; - if (tickCount > 0) { - let tickList = []; - for (let i = 0; i < tickCount; i++) { - let tickLabel = tickLabels ? tickLabels[i] : state.getFormattedValue(state.getPercentValue(i / (tickCount - 1))); - tickList.push( -
- {showTickLabels && -
- {tickLabel} -
- } -
- ); - } - ticks = (
- {tickList} -
); - } - - return ( -
- {(props.label) && -
- {props.label && labelNode} - {labelPosition === 'top' && showValueLabel && valueNode} + + {leftTrack} + {ticks} + +
+ + +
- } -
- {leftTrack} - {ticks} - -
- - - -
-
- {middleTrack} - -
- - - -
-
- {rightTrack} -
- {labelPosition === 'side' && -
- {showValueLabel && valueNode} + + {middleTrack} + +
+ + +
- } -
); +
+ {rightTrack} +
); } -const _RangeSlider = React.forwardRef(RangeSlider); -export {_RangeSlider as RangeSlider}; +// TODO forwardRef? +export {RangeSlider}; diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx index e258ef56b38..18f9e18dbd2 100644 --- a/packages/@react-spectrum/slider/src/Slider.tsx +++ b/packages/@react-spectrum/slider/src/Slider.tsx @@ -10,56 +10,19 @@ * governing permissions and limitations under the License. */ -import {classNames, useDOMRef} from '@react-spectrum/utils'; -import {DOMRef} from '@react-types/shared'; +import {classNames} from '@react-spectrum/utils'; import {FocusRing} from '@react-aria/focus'; -import {mergeProps} from '@react-aria/utils'; -import React, {useRef} from 'react'; +import React from 'react'; +import {SliderBase, useSliderBase} from './SliderBase'; import {SliderProps, SpectrumSliderProps} from '@react-types/slider'; import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; -import {useHover} from '@react-aria/interactions'; -import {useProviderProps} from '@react-spectrum/provider'; -import {useSlider, useSliderThumb} from '@react-aria/slider'; -import {useSliderState} from '@react-stately/slider'; import {VisuallyHidden} from '@adobe/react-spectrum'; -function Slider(props: SpectrumSliderProps, ref: DOMRef) { - // needed? - useDOMRef(ref); - props = useProviderProps(props); - - let { - labelPosition = 'top', isFilled, fillOffset, trackBackground, formatOptions, valueLabel, showValueLabel = !!props.label, - onChange, value, defaultValue, - tickCount, showTickLabels, tickLabels, - ...otherProps} = props; - - let {hoverProps, isHovered} = useHover({/* isDisabled */ }); - - let inputRef = useRef(); - let trackRef = useRef(); - - // Assumes that DEFAULT_MIN_VALUE and DEFAULT_MAX_VALUE are both positive, this value needs to be passed to useSliderState, so - // getThumbMinValue/getThumbMaxValue cannot be used here. - // `Math.abs(Math.sign(a) - Math.sign(b)) === 2` is true if the values have a different sign and neither is null. - let alwaysDisplaySign = props.minValue != null && props.maxValue != null && Math.abs(Math.sign(props.minValue) - Math.sign(props.maxValue)) === 2; - - if (alwaysDisplaySign) { - if (formatOptions != null) { - if (!('signDisplay' in formatOptions)) { - // @ts-ignore - formatOptions.signDisplay = 'exceptZero'; - } - } else { - // @ts-ignore - formatOptions = {signDisplay: 'exceptZero'}; - } - } +function Slider(props: SpectrumSliderProps) { + let {onChange, value, defaultValue, isFilled, fillOffset, trackBackground, ...otherProps} = props; let ariaProps: SliderProps = { ...otherProps, - // @ts-ignore - formatOptions: formatOptions, // Normalize `value: number[]` to `value: number` value: value != null ? [value] : undefined, defaultValue: defaultValue != null ? [defaultValue] : undefined, @@ -68,26 +31,11 @@ function Slider(props: SpectrumSliderProps, ref: DOMRef) { } }; - let state = useSliderState(ariaProps); - let { - containerProps, - trackProps, - labelProps - } = useSlider(ariaProps, state, trackRef); - let {thumbProps, inputProps} = useSliderThumb({ - index: 0, - isReadOnly: props.isReadOnly, - isDisabled: props.isDisabled, - trackRef, - inputRef - }, state); + let {inputRefs: [inputRef], thumbProps: [thumbProps], inputProps: [inputProps], ticks, isHovered, ...containerProps} = useSliderBase(1, ariaProps); + let {state} = containerProps; fillOffset = fillOffset != null ? Math.max(state.getThumbMinValue(0), Math.min(fillOffset, state.getThumbMaxValue(0))) : fillOffset; - let displayValue = valueLabel ?? state.getThumbValueLabel(0); - let labelNode = ; - let valueNode =
{displayValue}
; - let leftTrack = (
); } - let ticks = null; - if (tickCount > 0) { - let tickList = []; - for (let i = 0; i < tickCount; i++) { - let tickLabel = tickLabels ? tickLabels[i] : state.getFormattedValue(state.getPercentValue(i / (tickCount - 1))); - tickList.push( -
- {showTickLabels && -
- {tickLabel} -
- } -
- ); - } - ticks = (
- {tickList} -
); - } - - return ( -
- {(props.label) && -
- {props.label && labelNode} - {labelPosition === 'top' && showValueLabel && valueNode} -
- } -
- {leftTrack} - {ticks} - -
- - - -
-
- {rightTrack} - {filledTrack} -
- {labelPosition === 'side' && -
- {showValueLabel && valueNode} + }> + {leftTrack} + {ticks} + +
+ + +
- } -
); + + {rightTrack} + {filledTrack} + ); } -const _Slider = React.forwardRef(Slider); -export {_Slider as Slider}; +// TODO forwardRef? +export {Slider}; diff --git a/packages/@react-spectrum/slider/src/SliderBase.tsx b/packages/@react-spectrum/slider/src/SliderBase.tsx new file mode 100644 index 00000000000..1b35a95d7ea --- /dev/null +++ b/packages/@react-spectrum/slider/src/SliderBase.tsx @@ -0,0 +1,218 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {AriaLabelingProps, DOMRef, LabelableProps, LabelPosition, Orientation} from '@react-types/shared'; +import {classNames, useDOMRef} from '@react-spectrum/utils'; +import {mergeProps} from '@react-aria/utils'; +import React, {CSSProperties, HTMLAttributes, MutableRefObject, ReactNode, ReactNodeArray, useRef} from 'react'; +import {SliderProps, SpectrumSliderTicksBase} from '@react-types/slider'; +import {SliderState, useSliderState} from '@react-stately/slider'; +import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; +import {useHover} from '@react-aria/interactions'; +import {useLocale} from '@react-aria/i18n'; +import {useProviderProps} from '@react-spectrum/provider'; +import {useSlider, useSliderThumb} from '@react-aria/slider'; + +export interface UseSliderBaseContainerProps extends AriaLabelingProps, LabelableProps { + state: SliderState, + containerProps: HTMLAttributes, + labelProps: HTMLAttributes, + trackRef: MutableRefObject, + trackProps: HTMLAttributes, + hoverProps: HTMLAttributes, + isDisabled?: boolean, + orientation?: Orientation, + labelPosition?: LabelPosition, + showValueLabel?: boolean, + valueLabel?: ReactNode +} + +export interface UseSliderBaseInputProps extends SliderProps, SpectrumSliderTicksBase { + orientation?: Orientation, + labelPosition?: LabelPosition, + showValueLabel?: boolean, + valueLabel?: ReactNode +} + +export interface UseSliderBaseOutputProps extends UseSliderBaseContainerProps { + inputRefs: MutableRefObject[], + thumbProps: HTMLAttributes[], + inputProps: HTMLAttributes[], + labelProps: HTMLAttributes, + isHovered: boolean, + ticks: ReactNode +} + +/** Count musn't change across the livetime! */ +export function useSliderBase(count: number, props: UseSliderBaseInputProps): UseSliderBaseOutputProps { + props = useProviderProps(props); + let inputRefs = []; + let thumbProps = []; + let inputProps = []; + + let state = useSliderState(props); + + let {hoverProps, isHovered} = useHover({/* isDisabled */ }); + + // Assumes that DEFAULT_MIN_VALUE and DEFAULT_MAX_VALUE are both positive, this value needs to be passed to useSliderState, so + // getThumbMinValue/getThumbMaxValue cannot be used here. + // `Math.abs(Math.sign(a) - Math.sign(b)) === 2` is true if the values have a different sign and neither is null. + let alwaysDisplaySign = props.minValue != null && props.maxValue != null && Math.abs(Math.sign(props.minValue) - Math.sign(props.maxValue)) === 2; + + if (alwaysDisplaySign) { + if (props.formatOptions != null) { + if (!('signDisplay' in props.formatOptions)) { + // @ts-ignore + props.formatOptions.signDisplay = 'exceptZero'; + } + } else { + // @ts-ignore + props.formatOptions = {signDisplay: 'exceptZero'}; + } + } + + // @ts-ignore + let trackRef = useRef(); + for (let i = 0; i < count; i++) { + // eslint-disable-next-line react-hooks/rules-of-hooks + inputRefs[i] = useRef(); + // eslint-disable-next-line react-hooks/rules-of-hooks + let v = useSliderThumb({ + index: 0, + isReadOnly: props.isReadOnly, + isDisabled: props.isDisabled, + trackRef, + inputRef: inputRefs[i] + }, state); + + inputProps[i] = v.inputProps; + thumbProps[i] = v.thumbProps; + // TODO do we want to use the thumb's labelProps? + } + + let { + containerProps, + trackProps, + labelProps + } = useSlider(props, state, trackRef); + + let {tickCount, showTickLabels, tickLabels, isDisabled} = props; + + let ticks = null; + if (tickCount > 0) { + let tickList = []; + for (let i = 0; i < tickCount; i++) { + let tickLabel = tickLabels ? tickLabels[i] : state.getFormattedValue(state.getPercentValue(i / (tickCount - 1))); + tickList.push( +
+ {showTickLabels && +
+ {tickLabel} +
+ } +
+ ); + } + ticks = (
+ {tickList} +
); + } + + return { + ticks, + inputRefs, thumbProps, inputProps, trackRef, state, containerProps, + trackProps, labelProps, hoverProps, isHovered, isDisabled, + label: props.label, + showValueLabel: props.showValueLabel, + labelPosition: props.labelPosition, + orientation: props.orientation, + valueLabel: props.valueLabel, + 'aria-label': props['aria-label'], + 'aria-labelledby': props['aria-labelledby'], + 'aria-describedby': props['aria-describedby'], + 'aria-details': props['aria-details'] + }; +} + +export interface SliderBaseProps extends UseSliderBaseContainerProps, LabelableProps, AriaLabelingProps { + children: ReactNodeArray, + orientation?: Orientation, + labelPosition?: LabelPosition, + valueLabel?: ReactNode, + formatOptions?: Intl.NumberFormatOptions, + classes?: string[] | Object, + style?: CSSProperties +} + +function SliderBase(props: SliderBaseProps, ref: DOMRef) { + // needed? + useDOMRef(ref); + + let {direction} = useLocale(); + + let { + state, children, classes, style, + labelProps, containerProps, trackRef, trackProps, hoverProps, isDisabled, + labelPosition = 'top', valueLabel, showValueLabel = !!props.label + } = props; + + let displayValue = valueLabel; + if (!displayValue) { + switch (state.values.length) { + case 1: + displayValue = state.getThumbValueLabel(0); + break; + case 2: + // This should really use the NumberFormat#formatRange proposal + if (direction === 'ltr') { + displayValue = `${state.getThumbValueLabel(0)} - ${state.getThumbValueLabel(1)}`; + } else { + displayValue = `${state.getThumbValueLabel(1)} - ${state.getThumbValueLabel(0)}`; + } + break; + default: + throw new Error('Only sliders with 1 or 2 handles are supported!'); + } + } + let labelNode = ; + let valueNode =
{displayValue}
; + + return ( +
+ {(props.label) && +
+ {props.label && labelNode} + {labelPosition === 'top' && showValueLabel && valueNode} +
+ } +
+ {children} +
+ {labelPosition === 'side' && +
+ {showValueLabel && valueNode} +
+ } +
); +} + +const _SliderBase = React.forwardRef(SliderBase); +export {_SliderBase as SliderBase}; diff --git a/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx b/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx index e5fd0a78501..f0a03dcfd71 100644 --- a/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx +++ b/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx @@ -11,11 +11,11 @@ */ import {action} from '@storybook/addon-actions'; +import {Provider} from '@adobe/react-spectrum'; import {RangeSlider} from '../'; import React, {useState} from 'react'; import {SpectrumRangeSliderProps} from '@react-types/slider'; import {storiesOf} from '@storybook/react'; -import {Provider} from '@adobe/react-spectrum'; storiesOf('RangeSlider', module) .add( @@ -81,7 +81,9 @@ storiesOf('RangeSlider', module) function render(props: SpectrumRangeSliderProps = {}, width = '200px') { if (props.onChange == null) { - props.onChange = action('change'); + props.onChange = (v) => { + action('change')(v.start, v.end); + }; } return (
From 626d552c87e043880f9a9bebd6d869c70579b55a Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Fri, 18 Sep 2020 12:38:35 +0200 Subject: [PATCH 11/40] WIP: tests --- .../slider/test/Slider.test.js | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/@react-spectrum/slider/test/Slider.test.js b/packages/@react-spectrum/slider/test/Slider.test.js index befb10b254d..a7c2cd3756a 100644 --- a/packages/@react-spectrum/slider/test/Slider.test.js +++ b/packages/@react-spectrum/slider/test/Slider.test.js @@ -10,17 +10,23 @@ * governing permissions and limitations under the License. */ -// import React from 'react'; -// import {render} from '@testing-library/react'; -import Slider from '../'; +import React from 'react'; +import {render} from '@testing-library/react'; +import {Slider} from '../'; describe('Slider', function () { - it.each` - Name | Component | props - ${'Slider'} | ${Slider} | ${{}} - `('$Name handles defaults', function ({Component, props}) { - // let {getByRole, getByText} = render(); + it('supports aria-label', function () { + let {getByRole} = render(); - // expect(true).toBeTruthy(); + let group = getByRole('group'); + expect(group).toHaveAttribute('aria-label', 'The Label'); + }); + + it('supports label', function () { + let {getByRole} = render(); + + let group = getByRole('group'); + let label = document.getElementById(group.getAttribute('aria-labelledby')); + expect(label).toHaveTextContent(/^The Label$/); }); }); From 7ccc4a38cc2c20a7d8d501ed957d047c72607ada Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Mon, 21 Sep 2020 11:09:06 +0200 Subject: [PATCH 12/40] Fix RTL keyboard events --- packages/@react-aria/slider/src/useSlider.ts | 20 ++- .../@react-aria/slider/src/useSliderThumb.ts | 5 +- packages/@react-aria/utils/src/useDrag1D.ts | 127 +++++++++--------- .../slider/src/RangeSlider.tsx | 17 +-- .../@react-spectrum/slider/src/Slider.tsx | 35 ++--- .../@react-spectrum/slider/src/SliderBase.tsx | 46 ++++--- .../slider/test/RangeSlider.test.js | 23 ++++ .../slider/test/Slider.test.js | 56 +++++++- .../slider/src/useSliderState.ts | 2 +- packages/@react-types/slider/src/index.d.ts | 8 +- 10 files changed, 216 insertions(+), 123 deletions(-) create mode 100644 packages/@react-spectrum/slider/test/RangeSlider.test.js diff --git a/packages/@react-aria/slider/src/useSlider.ts b/packages/@react-aria/slider/src/useSlider.ts index 19ed02ddec8..afee1ea4fde 100644 --- a/packages/@react-aria/slider/src/useSlider.ts +++ b/packages/@react-aria/slider/src/useSlider.ts @@ -50,15 +50,17 @@ export function useSlider( // Attach id of the label to the state so it can be accessed by useSliderThumb. sliderIds.set(state, labelProps.id ?? fieldProps.id); + let {direction = 'ltr'} = props; + // When the user clicks or drags the track, we want the motion to set and drag the // closest thumb. Hence we also need to install useDrag1D() on the track element. // Here, we keep track of which index is the "closest" to the drag start point. // It is set onMouseDown; see trackProps below. const realTimeTrackDraggingIndex = useRef(undefined); const isTrackDragging = useRef(false); - const {onMouseDown, onMouseEnter, onMouseOut} = useDrag1D({ + const {onMouseDown, onMouseEnter, onMouseOut, onKeyDown} = useDrag1D({ containerRef: trackRef as any, - reverse: false, + reverse: direction === 'rtl', orientation: 'horizontal', onDrag: (dragging) => { if (realTimeTrackDraggingIndex.current !== undefined) { @@ -79,6 +81,18 @@ export function useSlider( realTimeTrackDraggingIndex.current = undefined; } } + }, + onIncrement() { + state.setThumbValue(state.focusedThumb, state.getThumbValue(state.focusedThumb) + state.step); + }, + onDecrement() { + state.setThumbValue(state.focusedThumb, state.getThumbValue(state.focusedThumb) - state.step); + }, + onIncrementToMax() { + state.setThumbValue(state.focusedThumb, state.getThumbMaxValue(state.focusedThumb)); + }, + onDecrementToMin() { + state.setThumbValue(state.focusedThumb, state.getThumbMinValue(state.focusedThumb)); } }); @@ -127,7 +141,7 @@ export function useSlider( } } }, { - onMouseDown, onMouseEnter, onMouseOut + onMouseDown, onMouseEnter, onMouseOut, onKeyDown }) }; } diff --git a/packages/@react-aria/slider/src/useSliderThumb.ts b/packages/@react-aria/slider/src/useSliderThumb.ts index 0c54d1ee7f6..fbefcca151d 100644 --- a/packages/@react-aria/slider/src/useSliderThumb.ts +++ b/packages/@react-aria/slider/src/useSliderThumb.ts @@ -39,7 +39,8 @@ export function useSliderThumb( isReadOnly, validationState, trackRef, - inputRef + inputRef, + direction = 'ltr' } = opts; let labelId = sliderIds.get(state); @@ -67,7 +68,7 @@ export function useSliderThumb( const draggableProps = useDrag1D({ containerRef: trackRef as any, - reverse: false, + reverse: direction === 'rtl', orientation: 'horizontal', onDrag: (dragging) => { state.setThumbDragging(index, dragging); diff --git a/packages/@react-aria/utils/src/useDrag1D.ts b/packages/@react-aria/utils/src/useDrag1D.ts index 720488f63f8..8c69d88e675 100644 --- a/packages/@react-aria/utils/src/useDrag1D.ts +++ b/packages/@react-aria/utils/src/useDrag1D.ts @@ -39,7 +39,7 @@ const draggingElements: HTMLElement[] = []; // It can also handle either a vertical or horizontal movement, but not both at the same time export function useDrag1D(props: UseDrag1DProps): HTMLAttributes { - let {containerRef, reverse, orientation, onHover, onDrag, onPositionChange, onIncrement, onDecrement, onIncrementToMax, onDecrementToMin, onCollapseToggle} = props; + let {containerRef, reverse, orientation, onHover, onDrag, onPositionChange/* , onIncrement, onDecrement, onIncrementToMax, onDecrementToMin, onCollapseToggle */} = props; let getPosition = (e) => orientation === 'horizontal' ? e.clientX : e.clientY; let getNextOffset = (e: MouseEvent) => { let containerOffset = getOffset(containerRef.current, reverse, orientation); @@ -116,71 +116,66 @@ export function useDrag1D(props: UseDrag1DProps): HTMLAttributes { } }; - let onKeyDown = (e) => { - switch (e.key) { - case 'Left': - case 'ArrowLeft': - e.preventDefault(); - if (orientation === 'horizontal') { - if (onDecrement && !reverse) { - onDecrement(); - } else if (onIncrement && reverse) { - onIncrement(); - } - } - break; - case 'Up': - case 'ArrowUp': - e.preventDefault(); - if (orientation === 'vertical') { - if (onDecrement && !reverse) { - onDecrement(); - } else if (onIncrement && reverse) { - onIncrement(); - } - } - break; - case 'Right': - case 'ArrowRight': - e.preventDefault(); - if (orientation === 'horizontal') { - if (onIncrement && !reverse) { - onIncrement(); - } else if (onDecrement && reverse) { - onDecrement(); - } - } - break; - case 'Down': - case 'ArrowDown': - e.preventDefault(); - if (orientation === 'vertical') { - if (onIncrement && !reverse) { - onIncrement(); - } else if (onDecrement && reverse) { - onDecrement(); - } - } - break; - case 'Home': - e.preventDefault(); - if (onDecrementToMin) { - onDecrementToMin(); - } - break; - case 'End': - e.preventDefault(); - if (onIncrementToMax) { - onIncrementToMax(); - } - break; - case 'Enter': - e.preventDefault(); - if (onCollapseToggle) { - onCollapseToggle(); - } - break; - } + let onKeyDown = (/* e */) => { + // TODO page up/down (= +- 10%). + // Do we actually want to override the native behaviour? (RTL?) + // Seems to be needed for tests at least. + // switch (e.key) { + // case 'Left': + // case 'ArrowLeft': + // e.preventDefault(); + // if (onDecrement && !reverse) { + // onDecrement(); + // } else if (onIncrement && reverse) { + // onIncrement(); + // } + // break; + // case 'Up': + // case 'ArrowUp': + // e.preventDefault(); + // if (onDecrement && reverse) { + // onDecrement(); + // } else if (onIncrement && !reverse) { + // onIncrement(); + // } + // break; + // case 'Right': + // case 'ArrowRight': + // e.preventDefault(); + // if (onIncrement && !reverse) { + // onIncrement(); + // } else if (onDecrement && reverse) { + // onDecrement(); + // } + // break; + // case 'Down': + // case 'ArrowDown': + // e.preventDefault(); + // if (onIncrement && reverse) { + // onIncrement(); + // } else if (onDecrement && !reverse) { + // onDecrement(); + // } + // break; + // case 'Home': + // e.preventDefault(); + // if (onDecrementToMin) { + // onDecrementToMin(); + // } + // break; + // case 'End': + // e.preventDefault(); + // if (onIncrementToMax) { + // onIncrementToMax(); + // } + // break; + // case 'Enter': + // e.preventDefault(); + // if (onCollapseToggle) { + // onCollapseToggle(); + // } + // break; + // } }; return {onMouseDown, onMouseEnter, onMouseOut, onKeyDown}; diff --git a/packages/@react-spectrum/slider/src/RangeSlider.tsx b/packages/@react-spectrum/slider/src/RangeSlider.tsx index 6dd5190870b..3354c19342d 100644 --- a/packages/@react-spectrum/slider/src/RangeSlider.tsx +++ b/packages/@react-spectrum/slider/src/RangeSlider.tsx @@ -14,15 +14,15 @@ import {classNames} from '@react-spectrum/utils'; import {DEFAULT_MAX_VALUE, DEFAULT_MIN_VALUE} from '@react-stately/slider'; import {FocusRing} from '@react-aria/focus'; import React from 'react'; -import {SliderBase, useSliderBase} from './SliderBase'; -import {SliderProps, SpectrumRangeSliderProps} from '@react-types/slider'; +import {SliderBase, useSliderBase, UseSliderBaseInputProps} from './SliderBase'; +import {SpectrumRangeSliderProps} from '@react-types/slider'; import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; import {VisuallyHidden} from '@adobe/react-spectrum'; function RangeSlider(props: SpectrumRangeSliderProps) { let {onChange, value, defaultValue, ...otherProps} = props; - let ariaProps: SliderProps = { + let ariaProps: UseSliderBaseInputProps = { ...otherProps, // Normalize `value: number[]` to `value: number` value: value != null ? [value.start, value.end] : undefined, @@ -39,21 +39,22 @@ function RangeSlider(props: SpectrumRangeSliderProps) { inputProps, ticks, isHovered, ...containerProps} = useSliderBase(2, ariaProps); - let {state} = containerProps; + let {state/* , direction */} = containerProps; + // let isRTL = direction === 'rtl'; - let leftTrack = (
); let middleTrack = (
); - let rightTrack = (
); return ( - {leftTrack} + {lowerTrack} {ticks}
- {rightTrack} + {higherTrack}
); } diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx index 18f9e18dbd2..d3858b323fd 100644 --- a/packages/@react-spectrum/slider/src/Slider.tsx +++ b/packages/@react-spectrum/slider/src/Slider.tsx @@ -10,18 +10,19 @@ * governing permissions and limitations under the License. */ +import {clamp} from '@react-aria/utils'; import {classNames} from '@react-spectrum/utils'; import {FocusRing} from '@react-aria/focus'; import React from 'react'; -import {SliderBase, useSliderBase} from './SliderBase'; -import {SliderProps, SpectrumSliderProps} from '@react-types/slider'; +import {SliderBase, useSliderBase, UseSliderBaseInputProps} from './SliderBase'; +import {SpectrumSliderProps} from '@react-types/slider'; import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; import {VisuallyHidden} from '@adobe/react-spectrum'; function Slider(props: SpectrumSliderProps) { let {onChange, value, defaultValue, isFilled, fillOffset, trackBackground, ...otherProps} = props; - let ariaProps: SliderProps = { + let ariaProps: UseSliderBaseInputProps = { ...otherProps, // Normalize `value: number[]` to `value: number` value: value != null ? [value] : undefined, @@ -32,14 +33,15 @@ function Slider(props: SpectrumSliderProps) { }; let {inputRefs: [inputRef], thumbProps: [thumbProps], inputProps: [inputProps], ticks, isHovered, ...containerProps} = useSliderBase(1, ariaProps); - let {state} = containerProps; + let {state, direction} = containerProps; + let isRTL = direction === 'rtl'; - fillOffset = fillOffset != null ? Math.max(state.getThumbMinValue(0), Math.min(fillOffset, state.getThumbMaxValue(0))) : fillOffset; + fillOffset = fillOffset != null ? clamp(fillOffset, state.getThumbMinValue(0), state.getThumbMaxValue(0)) : fillOffset; let leftTrack = (
); + let handle = (
+ + + +
); let rightTrack = (
-
- - - -
+ {handle} {rightTrack} {filledTrack} diff --git a/packages/@react-spectrum/slider/src/SliderBase.tsx b/packages/@react-spectrum/slider/src/SliderBase.tsx index 1b35a95d7ea..bb07ac01c02 100644 --- a/packages/@react-spectrum/slider/src/SliderBase.tsx +++ b/packages/@react-spectrum/slider/src/SliderBase.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {AriaLabelingProps, DOMRef, LabelableProps, LabelPosition, Orientation} from '@react-types/shared'; +import {AriaLabelingProps, Direction, DOMRef, LabelableProps, LabelPosition, Orientation} from '@react-types/shared'; import {classNames, useDOMRef} from '@react-spectrum/utils'; import {mergeProps} from '@react-aria/utils'; import React, {CSSProperties, HTMLAttributes, MutableRefObject, ReactNode, ReactNodeArray, useRef} from 'react'; @@ -24,19 +24,20 @@ import {useSlider, useSliderThumb} from '@react-aria/slider'; export interface UseSliderBaseContainerProps extends AriaLabelingProps, LabelableProps { state: SliderState, - containerProps: HTMLAttributes, - labelProps: HTMLAttributes, trackRef: MutableRefObject, - trackProps: HTMLAttributes, hoverProps: HTMLAttributes, isDisabled?: boolean, orientation?: Orientation, labelPosition?: LabelPosition, showValueLabel?: boolean, - valueLabel?: ReactNode + valueLabel?: ReactNode, + containerProps: HTMLAttributes, + trackProps: HTMLAttributes, + labelProps: HTMLAttributes, + direction: Direction } -export interface UseSliderBaseInputProps extends SliderProps, SpectrumSliderTicksBase { +export interface UseSliderBaseInputProps extends Omit, SpectrumSliderTicksBase { orientation?: Orientation, labelPosition?: LabelPosition, showValueLabel?: boolean, @@ -47,12 +48,11 @@ export interface UseSliderBaseOutputProps extends UseSliderBaseContainerProps { inputRefs: MutableRefObject[], thumbProps: HTMLAttributes[], inputProps: HTMLAttributes[], - labelProps: HTMLAttributes, isHovered: boolean, ticks: ReactNode } -/** Count musn't change across the livetime! */ +/** Count mustn't change during the lifetime! */ export function useSliderBase(count: number, props: UseSliderBaseInputProps): UseSliderBaseOutputProps { props = useProviderProps(props); let inputRefs = []; @@ -80,8 +80,17 @@ export function useSliderBase(count: number, props: UseSliderBaseInputProps): Us } } - // @ts-ignore + let {direction} = useLocale(); + let trackRef = useRef(); + + let { + containerProps, + trackProps, + labelProps + } = useSlider({...props, direction}, state, trackRef); + + for (let i = 0; i < count; i++) { // eslint-disable-next-line react-hooks/rules-of-hooks inputRefs[i] = useRef(); @@ -91,7 +100,8 @@ export function useSliderBase(count: number, props: UseSliderBaseInputProps): Us isReadOnly: props.isReadOnly, isDisabled: props.isDisabled, trackRef, - inputRef: inputRefs[i] + inputRef: inputRefs[i], + direction }, state); inputProps[i] = v.inputProps; @@ -99,12 +109,6 @@ export function useSliderBase(count: number, props: UseSliderBaseInputProps): Us // TODO do we want to use the thumb's labelProps? } - let { - containerProps, - trackProps, - labelProps - } = useSlider(props, state, trackRef); - let {tickCount, showTickLabels, tickLabels, isDisabled} = props; let ticks = null; @@ -129,8 +133,9 @@ export function useSliderBase(count: number, props: UseSliderBaseInputProps): Us return { ticks, - inputRefs, thumbProps, inputProps, trackRef, state, containerProps, - trackProps, labelProps, hoverProps, isHovered, isDisabled, + inputRefs, thumbProps, inputProps, trackRef, state, + containerProps, trackProps, labelProps, direction, + hoverProps, isHovered, isDisabled, label: props.label, showValueLabel: props.showValueLabel, labelPosition: props.labelPosition, @@ -157,11 +162,10 @@ function SliderBase(props: SliderBaseProps, ref: DOMRef) { // needed? useDOMRef(ref); - let {direction} = useLocale(); - let { state, children, classes, style, - labelProps, containerProps, trackRef, trackProps, hoverProps, isDisabled, + trackRef, hoverProps, isDisabled, + labelProps, direction, containerProps, trackProps, labelPosition = 'top', valueLabel, showValueLabel = !!props.label } = props; diff --git a/packages/@react-spectrum/slider/test/RangeSlider.test.js b/packages/@react-spectrum/slider/test/RangeSlider.test.js new file mode 100644 index 00000000000..10a2676343e --- /dev/null +++ b/packages/@react-spectrum/slider/test/RangeSlider.test.js @@ -0,0 +1,23 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +// import {act, fireEvent, render} from '@testing-library/react'; +// import {Provider} from '@adobe/react-spectrum'; +// import React from 'react'; +// import {Slider} from '../'; +// import {theme} from '@react-spectrum/theme-default'; +// import userEvent from '@testing-library/user-event'; + + +describe('Slider', function () { + it('tabbing works correctls', function () {}); +}); diff --git a/packages/@react-spectrum/slider/test/Slider.test.js b/packages/@react-spectrum/slider/test/Slider.test.js index a7c2cd3756a..b35e73bd90c 100644 --- a/packages/@react-spectrum/slider/test/Slider.test.js +++ b/packages/@react-spectrum/slider/test/Slider.test.js @@ -10,9 +10,32 @@ * governing permissions and limitations under the License. */ +import {act, fireEvent, render} from '@testing-library/react'; +import {Provider} from '@adobe/react-spectrum'; import React from 'react'; -import {render} from '@testing-library/react'; import {Slider} from '../'; +import {theme} from '@react-spectrum/theme-default'; +import userEvent from '@testing-library/user-event'; + +function pressKeyOnButton(key, button) { + act(() => {fireEvent.keyDown(button, {key});}); +} + +function pressArrowRight(button) { + return pressKeyOnButton('ArrowRight', button); +} + +function pressArrowLeft(button) { + return pressKeyOnButton('ArrowLeft', button); +} + +function pressArrowUp(button) { + return pressKeyOnButton('ArrowUp', button); +} + +function pressArrowDown(button) { + return pressKeyOnButton('ArrowDown', button); +} describe('Slider', function () { it('supports aria-label', function () { @@ -26,7 +49,34 @@ describe('Slider', function () { let {getByRole} = render(); let group = getByRole('group'); - let label = document.getElementById(group.getAttribute('aria-labelledby')); - expect(label).toHaveTextContent(/^The Label$/); + let labelId = group.getAttribute('aria-labelledby'); + let slider = getByRole('slider'); + expect(slider.getAttribute('aria-labelledby')).toBe(labelId); + + expect(document.getElementById(labelId)).toHaveTextContent(/^The Label$/); + }); + + // todo: aria-labeledby + + // See comment on onKeyDown in useDrag1D + it.skip.each` + Name | props | run + ${'(left/right arrows, ltr)'} | ${{locale: 'de-DE'}} | ${(slider) => {let v = Number(slider.value); pressArrowRight(slider); expect(slider).toHaveProperty('value', String(v + 1)); pressArrowLeft(slider); expect(slider).toHaveProperty('value', String(v));}} + ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${(slider) => {let v = Number(slider.value); pressArrowRight(slider); expect(slider).toHaveProperty('value', String(v - 1)); pressArrowLeft(slider); expect(slider).toHaveProperty('value', String(v));}} + ${'(up/down arrows, ltr)'} | ${{locale: 'de-DE'}} | ${(slider) => {let v = Number(slider.value); pressArrowUp(slider); expect(slider).toHaveProperty('value', String(v + 1)); pressArrowDown(slider); expect(slider).toHaveProperty('value', String(v));}} + ${'(up/down arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${(slider) => {let v = Number(slider.value); pressArrowUp(slider); expect(slider).toHaveProperty('value', String(v - 1)); pressArrowDown(slider); expect(slider).toHaveProperty('value', String(v));}} + `('$Name shifts button focus in the correct direction on key press', function ({Name, props, run}) { + let tree = render( + + + + ); + + let slider = tree.getByRole('slider'); + + userEvent.tab(); + expect(document.activeElement).toBe(slider); + + run(slider); }); }); diff --git a/packages/@react-stately/slider/src/useSliderState.ts b/packages/@react-stately/slider/src/useSliderState.ts index f1350a21be2..66a9405e3f8 100644 --- a/packages/@react-stately/slider/src/useSliderState.ts +++ b/packages/@react-stately/slider/src/useSliderState.ts @@ -63,7 +63,7 @@ export const DEFAULT_MIN_VALUE = 0; export const DEFAULT_MAX_VALUE = 100; export const DEFAULT_STEP_VALUE = 1; -export function useSliderState(props: SliderProps): SliderState { +export function useSliderState(props: Omit): SliderState { let {isReadOnly, isDisabled, minValue = DEFAULT_MIN_VALUE, maxValue = DEFAULT_MAX_VALUE, formatOptions, step = DEFAULT_STEP_VALUE} = props; const [values, setValues] = useControlledState( diff --git a/packages/@react-types/slider/src/index.d.ts b/packages/@react-types/slider/src/index.d.ts index f189f788b8a..a805f2d300e 100644 --- a/packages/@react-types/slider/src/index.d.ts +++ b/packages/@react-types/slider/src/index.d.ts @@ -1,4 +1,4 @@ -import {AriaLabelingProps, AriaValidationProps, FocusableDOMProps, FocusableProps, LabelableProps, LabelPosition, Orientation, RangeInputBase, RangeValue, Validation, ValueBase} from '@react-types/shared'; +import {AriaLabelingProps, AriaValidationProps, Direction, FocusableDOMProps, FocusableProps, LabelableProps, LabelPosition, Orientation, RangeInputBase, RangeValue, Validation, ValueBase} from '@react-types/shared'; import {ReactNode} from 'react'; export interface BaseSliderProps extends RangeInputBase, LabelableProps, AriaLabelingProps { @@ -8,13 +8,15 @@ export interface BaseSliderProps extends RangeInputBase, LabelableProps, } export interface SliderProps extends BaseSliderProps, ValueBase { - onChangeEnd?: (value: number[]) => void + onChangeEnd?: (value: number[]) => void, + direction?: Direction } export interface SliderThumbProps extends AriaLabelingProps, FocusableDOMProps, FocusableProps, Validation, AriaValidationProps, LabelableProps { isReadOnly?: boolean, isDisabled?: boolean, - index: number + index: number, + direction?: Direction } export interface SpectrumBarSliderBase extends BaseSliderProps, ValueBase { From 43bbd43df2fcf31a0b5bd418600a08390e6634f5 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Mon, 21 Sep 2020 11:55:31 +0200 Subject: [PATCH 13/40] More tests --- .../slider/stories/Slider.stories.tsx | 6 +- .../slider/test/Slider.test.js | 56 +++++++++++++------ 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/packages/@react-spectrum/slider/stories/Slider.stories.tsx b/packages/@react-spectrum/slider/stories/Slider.stories.tsx index 4069e1a86ef..43a7f499843 100644 --- a/packages/@react-spectrum/slider/stories/Slider.stories.tsx +++ b/packages/@react-spectrum/slider/stories/Slider.stories.tsx @@ -28,7 +28,7 @@ storiesOf('Slider', module) ) .add( 'rtl', - () => {render({label: 'فهو يتحدّث بلغة '})} + () => {render({label: 'فهو يتحدّث بلغة'})} ) .add( 'disabled', @@ -62,6 +62,10 @@ storiesOf('Slider', module) 'labelPosition: side', () => render({label: 'Label', labelPosition: 'side'}) ) + .add( + 'rtl labelPosition: side', + () => {render({label: 'فهو يتحدّث بلغة', labelPosition: 'side'})} + ) .add( 'min/max', () => render({label: 'Label', minValue: 30, maxValue: 70}) diff --git a/packages/@react-spectrum/slider/test/Slider.test.js b/packages/@react-spectrum/slider/test/Slider.test.js index b35e73bd90c..bddf76e33e5 100644 --- a/packages/@react-spectrum/slider/test/Slider.test.js +++ b/packages/@react-spectrum/slider/test/Slider.test.js @@ -43,6 +43,9 @@ describe('Slider', function () { let group = getByRole('group'); expect(group).toHaveAttribute('aria-label', 'The Label'); + + // No label/value + expect(group.textContent).toBeFalsy(); }); it('supports label', function () { @@ -54,29 +57,46 @@ describe('Slider', function () { expect(slider.getAttribute('aria-labelledby')).toBe(labelId); expect(document.getElementById(labelId)).toHaveTextContent(/^The Label$/); + + // Shows value as well + expect(group.textContent).toBe('The Label0'); }); - // todo: aria-labeledby + it('supports showValueLabel: false', function () { + let {getByRole} = render(); + let group = getByRole('group'); - // See comment on onKeyDown in useDrag1D - it.skip.each` - Name | props | run - ${'(left/right arrows, ltr)'} | ${{locale: 'de-DE'}} | ${(slider) => {let v = Number(slider.value); pressArrowRight(slider); expect(slider).toHaveProperty('value', String(v + 1)); pressArrowLeft(slider); expect(slider).toHaveProperty('value', String(v));}} - ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${(slider) => {let v = Number(slider.value); pressArrowRight(slider); expect(slider).toHaveProperty('value', String(v - 1)); pressArrowLeft(slider); expect(slider).toHaveProperty('value', String(v));}} - ${'(up/down arrows, ltr)'} | ${{locale: 'de-DE'}} | ${(slider) => {let v = Number(slider.value); pressArrowUp(slider); expect(slider).toHaveProperty('value', String(v + 1)); pressArrowDown(slider); expect(slider).toHaveProperty('value', String(v));}} - ${'(up/down arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${(slider) => {let v = Number(slider.value); pressArrowUp(slider); expect(slider).toHaveProperty('value', String(v - 1)); pressArrowDown(slider); expect(slider).toHaveProperty('value', String(v));}} - `('$Name shifts button focus in the correct direction on key press', function ({Name, props, run}) { - let tree = render( - - - - ); + expect(group.textContent).toBe('The Label'); + }); - let slider = tree.getByRole('slider'); + it('supports disabled', function () { + let {getByRole} = render(); - userEvent.tab(); - expect(document.activeElement).toBe(slider); + let slider = getByRole('slider'); + expect(slider).toBeDisabled(); + }); - run(slider); + describe('interactions', () => { + // See comment on onKeyDown in useDrag1D + it.skip.each` + Name | props | run + ${'(left/right arrows, ltr)'} | ${{locale: 'de-DE'}} | ${(slider) => {let v = Number(slider.value); pressArrowRight(slider); expect(slider).toHaveProperty('value', String(v + 1)); pressArrowLeft(slider); expect(slider).toHaveProperty('value', String(v));}} + ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${(slider) => {let v = Number(slider.value); pressArrowRight(slider); expect(slider).toHaveProperty('value', String(v - 1)); pressArrowLeft(slider); expect(slider).toHaveProperty('value', String(v));}} + ${'(up/down arrows, ltr)'} | ${{locale: 'de-DE'}} | ${(slider) => {let v = Number(slider.value); pressArrowUp(slider); expect(slider).toHaveProperty('value', String(v + 1)); pressArrowDown(slider); expect(slider).toHaveProperty('value', String(v));}} + ${'(up/down arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${(slider) => {let v = Number(slider.value); pressArrowUp(slider); expect(slider).toHaveProperty('value', String(v - 1)); pressArrowDown(slider); expect(slider).toHaveProperty('value', String(v));}} + `('$Name shifts button focus in the correct direction on key press', function ({Name, props, run}) { + let tree = render( + + + + ); + + let slider = tree.getByRole('slider'); + + userEvent.tab(); + expect(document.activeElement).toBe(slider); + + run(slider); + }); }); }); From ec22612c275e64cda33887147cbacb8158646f09 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Mon, 21 Sep 2020 12:53:19 +0200 Subject: [PATCH 14/40] Test/Fix custom formatOptions --- .../@react-spectrum/slider/src/SliderBase.tsx | 6 +- .../slider/test/Slider.test.js | 107 +++++++++++++++++- 2 files changed, 104 insertions(+), 9 deletions(-) diff --git a/packages/@react-spectrum/slider/src/SliderBase.tsx b/packages/@react-spectrum/slider/src/SliderBase.tsx index bb07ac01c02..fb3ba752a23 100644 --- a/packages/@react-spectrum/slider/src/SliderBase.tsx +++ b/packages/@react-spectrum/slider/src/SliderBase.tsx @@ -59,8 +59,6 @@ export function useSliderBase(count: number, props: UseSliderBaseInputProps): Us let thumbProps = []; let inputProps = []; - let state = useSliderState(props); - let {hoverProps, isHovered} = useHover({/* isDisabled */ }); // Assumes that DEFAULT_MIN_VALUE and DEFAULT_MAX_VALUE are both positive, this value needs to be passed to useSliderState, so @@ -80,17 +78,17 @@ export function useSliderBase(count: number, props: UseSliderBaseInputProps): Us } } + let state = useSliderState(props); + let {direction} = useLocale(); let trackRef = useRef(); - let { containerProps, trackProps, labelProps } = useSlider({...props, direction}, state, trackRef); - for (let i = 0; i < count; i++) { // eslint-disable-next-line react-hooks/rules-of-hooks inputRefs[i] = useRef(); diff --git a/packages/@react-spectrum/slider/test/Slider.test.js b/packages/@react-spectrum/slider/test/Slider.test.js index bddf76e33e5..f8256b43f33 100644 --- a/packages/@react-spectrum/slider/test/Slider.test.js +++ b/packages/@react-spectrum/slider/test/Slider.test.js @@ -12,7 +12,7 @@ import {act, fireEvent, render} from '@testing-library/react'; import {Provider} from '@adobe/react-spectrum'; -import React from 'react'; +import React, {useState} from 'react'; import {Slider} from '../'; import {theme} from '@react-spectrum/theme-default'; import userEvent from '@testing-library/user-event'; @@ -76,14 +76,104 @@ describe('Slider', function () { expect(slider).toBeDisabled(); }); - describe('interactions', () => { + it('supports defaultValue', function () { + let {getByRole} = render(); + + let slider = getByRole('slider'); + + expect(slider).toHaveProperty('value', '20'); + fireEvent.change(slider, {target: {value: '40'}}); + expect(slider).toHaveProperty('value', '40'); + }); + + it('can be controlled', function () { + let renders = []; + + function Test() { + let [value, setValue] = useState(50); + renders.push(value); + + return (); + } + + let {getByRole} = render(); + + let slider = getByRole('slider'); + + expect(slider).toHaveProperty('value', '50'); + fireEvent.change(slider, {target: {value: '55'}}); + expect(slider).toHaveProperty('value', '55'); + + expect(renders).toStrictEqual([50, 55]); + }); + + it('supports a custom valueLabel', function () { + function Test() { + let [value, setValue] = useState(50); + return (); + } + + let {getByRole} = render(); + + let group = getByRole('group'); + let slider = getByRole('slider'); + + expect(group.textContent).toBe('The LabelA50B'); + fireEvent.change(slider, {target: {value: '55'}}); + expect(group.textContent).toBe('The LabelA55B'); + }); + + describe('formatOptions', () => { + it('prefixes the value with a plus sign if needed', function () { + let {getByRole} = render( + + ); + + let group = getByRole('group'); + let slider = getByRole('slider'); + + expect(group.textContent).toBe('The Label+10'); + fireEvent.change(slider, {target: {value: '0'}}); + expect(group.textContent).toBe('The Label0'); + }); + + it('supports setting custom formatOptions', function () { + let {getByRole} = render( + + ); + + let group = getByRole('group'); + let slider = getByRole('slider'); + + expect(group.textContent).toBe('The Label20%'); + fireEvent.change(slider, {target: {value: 0.5}}); + expect(group.textContent).toBe('The Label50%'); + }); + }); + + // formatOptions + // min max + + // TODO: assert DOM structure for isFilled/fillOffset, ticks, trackBackground? + + describe('keyboard interactions', () => { // See comment on onKeyDown in useDrag1D + // ${'(up/down arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${(slider) => {let v = Number(slider.value); pressArrowUp(slider); expect(slider).toHaveProperty('value', String(v - 1)); pressArrowDown(slider); expect(slider).toHaveProperty('value', String(v));}} + // ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${(slider) => {let v = Number(slider.value); pressArrowRight(slider); expect(slider).toHaveProperty('value', String(v - 1)); pressArrowLeft(slider); expect(slider).toHaveProperty('value', String(v));}} it.skip.each` - Name | props | run + Name | props | run ${'(left/right arrows, ltr)'} | ${{locale: 'de-DE'}} | ${(slider) => {let v = Number(slider.value); pressArrowRight(slider); expect(slider).toHaveProperty('value', String(v + 1)); pressArrowLeft(slider); expect(slider).toHaveProperty('value', String(v));}} - ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${(slider) => {let v = Number(slider.value); pressArrowRight(slider); expect(slider).toHaveProperty('value', String(v - 1)); pressArrowLeft(slider); expect(slider).toHaveProperty('value', String(v));}} ${'(up/down arrows, ltr)'} | ${{locale: 'de-DE'}} | ${(slider) => {let v = Number(slider.value); pressArrowUp(slider); expect(slider).toHaveProperty('value', String(v + 1)); pressArrowDown(slider); expect(slider).toHaveProperty('value', String(v));}} - ${'(up/down arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${(slider) => {let v = Number(slider.value); pressArrowUp(slider); expect(slider).toHaveProperty('value', String(v - 1)); pressArrowDown(slider); expect(slider).toHaveProperty('value', String(v));}} `('$Name shifts button focus in the correct direction on key press', function ({Name, props, run}) { let tree = render( @@ -98,5 +188,12 @@ describe('Slider', function () { run(slider); }); + + // TODO verify that it's clamped by min/max + + // TODO step }); + + // TODO + // describe('mouse interactions', () => { }); }); From c47a0d95f42dabcf13e81a3a11f2ce63787eeb5b Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Mon, 21 Sep 2020 17:38:34 +0200 Subject: [PATCH 15/40] Make RangeSlider actually work, especially RTL --- packages/@react-aria/slider/src/useSlider.ts | 5 ++- .../slider/src/RangeSlider.tsx | 33 ++++++++--------- .../@react-spectrum/slider/src/Slider.tsx | 9 +++-- .../@react-spectrum/slider/src/SliderBase.tsx | 14 +++++++- .../slider/stories/Slider.stories.tsx | 5 +++ .../slider/test/RangeSlider.test.js | 36 +++++++++++++++---- .../slider/test/Slider.test.js | 26 +++++++++++--- 7 files changed, 94 insertions(+), 34 deletions(-) diff --git a/packages/@react-aria/slider/src/useSlider.ts b/packages/@react-aria/slider/src/useSlider.ts index afee1ea4fde..355af2e2840 100644 --- a/packages/@react-aria/slider/src/useSlider.ts +++ b/packages/@react-aria/slider/src/useSlider.ts @@ -115,7 +115,10 @@ export function useSlider( const clickPosition = e.clientX; const offset = clickPosition - trackPosition; const percent = offset / trackRef.current.offsetWidth; - const value = state.getPercentValue(percent); + let value = state.getPercentValue(percent); + if (direction === 'rtl') { + value = 100 - value; + } // Only compute the diff for thumbs that are editable, as only they can be dragged const minDiff = Math.min(...state.values.map((v, index) => state.isThumbEditable(index) ? Math.abs(v - value) : Number.POSITIVE_INFINITY)); diff --git a/packages/@react-spectrum/slider/src/RangeSlider.tsx b/packages/@react-spectrum/slider/src/RangeSlider.tsx index 3354c19342d..544d26b17d3 100644 --- a/packages/@react-spectrum/slider/src/RangeSlider.tsx +++ b/packages/@react-spectrum/slider/src/RangeSlider.tsx @@ -14,7 +14,7 @@ import {classNames} from '@react-spectrum/utils'; import {DEFAULT_MAX_VALUE, DEFAULT_MIN_VALUE} from '@react-stately/slider'; import {FocusRing} from '@react-aria/focus'; import React from 'react'; -import {SliderBase, useSliderBase, UseSliderBaseInputProps} from './SliderBase'; +import {SliderBase, toPercent, useSliderBase, UseSliderBaseInputProps} from './SliderBase'; import {SpectrumRangeSliderProps} from '@react-types/slider'; import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; import {VisuallyHidden} from '@adobe/react-spectrum'; @@ -39,31 +39,32 @@ function RangeSlider(props: SpectrumRangeSliderProps) { inputProps, ticks, isHovered, ...containerProps} = useSliderBase(2, ariaProps); - let {state/* , direction */} = containerProps; - // let isRTL = direction === 'rtl'; + let {state, direction} = containerProps; - let lowerTrack = (
); + style={{width: toPercent(state.getThumbPercent(leftSliderIndex), direction)}} />); let middleTrack = (
); - let higherTrack = (
); + let rightTrack = (
); + style={{left: toPercent(state.getThumbPercent(rightSliderIndex), direction), width: toPercent(1 - state.getThumbPercent(rightSliderIndex), direction)}} />); return ( - {lowerTrack} + {leftTrack} {ticks}
- +
@@ -71,15 +72,15 @@ function RangeSlider(props: SpectrumRangeSliderProps) {
- +
- {higherTrack} + {rightTrack}
); } diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx index d3858b323fd..ee5c0d44bf8 100644 --- a/packages/@react-spectrum/slider/src/Slider.tsx +++ b/packages/@react-spectrum/slider/src/Slider.tsx @@ -14,7 +14,7 @@ import {clamp} from '@react-aria/utils'; import {classNames} from '@react-spectrum/utils'; import {FocusRing} from '@react-aria/focus'; import React from 'react'; -import {SliderBase, useSliderBase, UseSliderBaseInputProps} from './SliderBase'; +import {SliderBase, toPercent, useSliderBase, UseSliderBaseInputProps} from './SliderBase'; import {SpectrumSliderProps} from '@react-types/slider'; import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; import {VisuallyHidden} from '@adobe/react-spectrum'; @@ -34,14 +34,13 @@ function Slider(props: SpectrumSliderProps) { let {inputRefs: [inputRef], thumbProps: [thumbProps], inputProps: [inputProps], ticks, isHovered, ...containerProps} = useSliderBase(1, ariaProps); let {state, direction} = containerProps; - let isRTL = direction === 'rtl'; fillOffset = fillOffset != null ? clamp(fillOffset, state.getThumbMinValue(0), state.getThumbMaxValue(0)) : fillOffset; let leftTrack = (
); let handle = (
@@ -62,7 +61,7 @@ function Slider(props: SpectrumSliderProps) { let rightTrack = (
, @@ -59,6 +69,7 @@ export function useSliderBase(count: number, props: UseSliderBaseInputProps): Us let thumbProps = []; let inputProps = []; + // TODO the two handles on the range slider should have individual hover effects let {hoverProps, isHovered} = useHover({/* isDisabled */ }); // Assumes that DEFAULT_MIN_VALUE and DEFAULT_MAX_VALUE are both positive, this value needs to be passed to useSliderState, so @@ -94,7 +105,7 @@ export function useSliderBase(count: number, props: UseSliderBaseInputProps): Us inputRefs[i] = useRef(); // eslint-disable-next-line react-hooks/rules-of-hooks let v = useSliderThumb({ - index: 0, + index: i, isReadOnly: props.isReadOnly, isDisabled: props.isDisabled, trackRef, @@ -185,6 +196,7 @@ function SliderBase(props: SliderBaseProps, ref: DOMRef) { throw new Error('Only sliders with 1 or 2 handles are supported!'); } } + let labelNode = ; let valueNode =
{displayValue}
; diff --git a/packages/@react-spectrum/slider/stories/Slider.stories.tsx b/packages/@react-spectrum/slider/stories/Slider.stories.tsx index 43a7f499843..ce54883c284 100644 --- a/packages/@react-spectrum/slider/stories/Slider.stories.tsx +++ b/packages/@react-spectrum/slider/stories/Slider.stories.tsx @@ -86,6 +86,11 @@ storiesOf('Slider', module) 'showTickLabels: true', () => render({label: 'Label', tickCount: 4, showTickLabels: true}) ) + .add( + 'showTickLabels, custom formatOptions', + // @ts-ignore TODO why is "unit" even missing? How well is it supported? + () => render({label: 'Label', tickCount: 5, showTickLabels: true, minValue: -10, maxValue: 10, formatOptions: {style: 'unit', unit: 'centimeter'}}) + ) .add( 'tickLabels', () => render({label: 'Label', tickCount: 3, showTickLabels: true, tickLabels: ['A', 'B', 'C']}) diff --git a/packages/@react-spectrum/slider/test/RangeSlider.test.js b/packages/@react-spectrum/slider/test/RangeSlider.test.js index 10a2676343e..62e9c65f7c8 100644 --- a/packages/@react-spectrum/slider/test/RangeSlider.test.js +++ b/packages/@react-spectrum/slider/test/RangeSlider.test.js @@ -10,14 +10,36 @@ * governing permissions and limitations under the License. */ -// import {act, fireEvent, render} from '@testing-library/react'; -// import {Provider} from '@adobe/react-spectrum'; -// import React from 'react'; -// import {Slider} from '../'; -// import {theme} from '@react-spectrum/theme-default'; -// import userEvent from '@testing-library/user-event'; +import {RangeSlider} from '../'; +import React from 'react'; +import {render} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; describe('Slider', function () { - it('tabbing works correctls', function () {}); + it('can be focused', function () { + let {getAllByRole} = render(
+ + + +
); + + let [sliderLeft, sliderRight] = getAllByRole('slider'); + let [buttonA, buttonB] = getAllByRole('button'); + + userEvent.tab(); + expect(document.activeElement).toBe(buttonA); + userEvent.tab(); + expect(document.activeElement).toBe(sliderLeft); + userEvent.tab(); + expect(document.activeElement).toBe(sliderRight); + userEvent.tab(); + expect(document.activeElement).toBe(buttonB); + userEvent.tab({shift: true}); + expect(document.activeElement).toBe(sliderRight); + userEvent.tab({shift: true}); + expect(document.activeElement).toBe(sliderLeft); + userEvent.tab({shift: true}); + expect(document.activeElement).toBe(buttonA); + }); }); diff --git a/packages/@react-spectrum/slider/test/Slider.test.js b/packages/@react-spectrum/slider/test/Slider.test.js index f8256b43f33..1ed224228b0 100644 --- a/packages/@react-spectrum/slider/test/Slider.test.js +++ b/packages/@react-spectrum/slider/test/Slider.test.js @@ -76,6 +76,27 @@ describe('Slider', function () { expect(slider).toBeDisabled(); }); + it('can be focused', function () { + let {getByRole, getAllByRole} = render(
+ + + +
); + + let slider = getByRole('slider'); + let [buttonA, buttonB] = getAllByRole('button'); + act(() => { + slider.focus(); + }); + + expect(document.activeElement).toBe(slider); + userEvent.tab(); + expect(document.activeElement).toBe(buttonB); + userEvent.tab({shift: true}); + userEvent.tab({shift: true}); + expect(document.activeElement).toBe(buttonA); + }); + it('supports defaultValue', function () { let {getByRole} = render(); @@ -161,9 +182,6 @@ describe('Slider', function () { }); }); - // formatOptions - // min max - // TODO: assert DOM structure for isFilled/fillOffset, ticks, trackBackground? describe('keyboard interactions', () => { @@ -194,6 +212,6 @@ describe('Slider', function () { // TODO step }); - // TODO + // TODO ??? // describe('mouse interactions', () => { }); }); From dd81c83057d49579ca03b422d2594e550a47b259 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Mon, 21 Sep 2020 18:47:33 +0200 Subject: [PATCH 16/40] Fixup RTL, useDrag1D keyboard listener --- packages/@react-aria/utils/src/useDrag1D.ts | 127 +++++++++--------- .../slider/src/RangeSlider.tsx | 12 +- .../@react-spectrum/slider/src/SliderBase.tsx | 8 +- 3 files changed, 74 insertions(+), 73 deletions(-) diff --git a/packages/@react-aria/utils/src/useDrag1D.ts b/packages/@react-aria/utils/src/useDrag1D.ts index 8c69d88e675..d9149a82940 100644 --- a/packages/@react-aria/utils/src/useDrag1D.ts +++ b/packages/@react-aria/utils/src/useDrag1D.ts @@ -39,7 +39,7 @@ const draggingElements: HTMLElement[] = []; // It can also handle either a vertical or horizontal movement, but not both at the same time export function useDrag1D(props: UseDrag1DProps): HTMLAttributes { - let {containerRef, reverse, orientation, onHover, onDrag, onPositionChange/* , onIncrement, onDecrement, onIncrementToMax, onDecrementToMin, onCollapseToggle */} = props; + let {containerRef, reverse, orientation, onHover, onDrag, onPositionChange, onIncrement, onDecrement, onIncrementToMax, onDecrementToMin, onCollapseToggle} = props; let getPosition = (e) => orientation === 'horizontal' ? e.clientX : e.clientY; let getNextOffset = (e: MouseEvent) => { let containerOffset = getOffset(containerRef.current, reverse, orientation); @@ -116,66 +116,71 @@ export function useDrag1D(props: UseDrag1DProps): HTMLAttributes { } }; - let onKeyDown = (/* e */) => { - // TODO page up/down (= +- 10%). - // Do we actually want to override the native behaviour? (RTL?) - // Seems to be needed for tests at least. - // switch (e.key) { - // case 'Left': - // case 'ArrowLeft': - // e.preventDefault(); - // if (onDecrement && !reverse) { - // onDecrement(); - // } else if (onIncrement && reverse) { - // onIncrement(); - // } - // break; - // case 'Up': - // case 'ArrowUp': - // e.preventDefault(); - // if (onDecrement && reverse) { - // onDecrement(); - // } else if (onIncrement && !reverse) { - // onIncrement(); - // } - // break; - // case 'Right': - // case 'ArrowRight': - // e.preventDefault(); - // if (onIncrement && !reverse) { - // onIncrement(); - // } else if (onDecrement && reverse) { - // onDecrement(); - // } - // break; - // case 'Down': - // case 'ArrowDown': - // e.preventDefault(); - // if (onIncrement && reverse) { - // onIncrement(); - // } else if (onDecrement && !reverse) { - // onDecrement(); - // } - // break; - // case 'Home': - // e.preventDefault(); - // if (onDecrementToMin) { - // onDecrementToMin(); - // } - // break; - // case 'End': - // e.preventDefault(); - // if (onIncrementToMax) { - // onIncrementToMax(); - // } - // break; - // case 'Enter': - // e.preventDefault(); - // if (onCollapseToggle) { - // onCollapseToggle(); - // } - // break; - // } + let onKeyDown = (e) => { + switch (e.key) { + case 'Left': + case 'ArrowLeft': + if (orientation === 'horizontal') { + e.preventDefault(); + if (onDecrement && !reverse) { + onDecrement(); + } else if (onIncrement && reverse) { + onIncrement(); + } + } + break; + case 'Up': + case 'ArrowUp': + if (orientation === 'vertical') { + e.preventDefault(); + if (onDecrement && !reverse) { + onDecrement(); + } else if (onIncrement && reverse) { + onIncrement(); + } + } + break; + case 'Right': + case 'ArrowRight': + if (orientation === 'horizontal') { + e.preventDefault(); + if (onIncrement && !reverse) { + onIncrement(); + } else if (onDecrement && reverse) { + onDecrement(); + } + } + break; + case 'Down': + case 'ArrowDown': + if (orientation === 'vertical') { + e.preventDefault(); + if (onIncrement && !reverse) { + onIncrement(); + } else if (onDecrement && reverse) { + onDecrement(); + } + } + break; + case 'Home': + e.preventDefault(); + if (onDecrementToMin) { + onDecrementToMin(); + } + break; + case 'End': + e.preventDefault(); + if (onIncrementToMax) { + onIncrementToMax(); + } + break; + case 'Enter': + e.preventDefault(); + if (onCollapseToggle) { + onCollapseToggle(); + } + break; + } }; return {onMouseDown, onMouseEnter, onMouseOut, onKeyDown}; diff --git a/packages/@react-spectrum/slider/src/RangeSlider.tsx b/packages/@react-spectrum/slider/src/RangeSlider.tsx index 544d26b17d3..5fee357bb33 100644 --- a/packages/@react-spectrum/slider/src/RangeSlider.tsx +++ b/packages/@react-spectrum/slider/src/RangeSlider.tsx @@ -60,11 +60,11 @@ function RangeSlider(props: SpectrumRangeSliderProps) {
- +
@@ -72,11 +72,11 @@ function RangeSlider(props: SpectrumRangeSliderProps) {
- +
diff --git a/packages/@react-spectrum/slider/src/SliderBase.tsx b/packages/@react-spectrum/slider/src/SliderBase.tsx index 9f9f7b8385d..ed00935c25b 100644 --- a/packages/@react-spectrum/slider/src/SliderBase.tsx +++ b/packages/@react-spectrum/slider/src/SliderBase.tsx @@ -174,7 +174,7 @@ function SliderBase(props: SliderBaseProps, ref: DOMRef) { let { state, children, classes, style, trackRef, hoverProps, isDisabled, - labelProps, direction, containerProps, trackProps, + labelProps, containerProps, trackProps, labelPosition = 'top', valueLabel, showValueLabel = !!props.label } = props; @@ -186,11 +186,7 @@ function SliderBase(props: SliderBaseProps, ref: DOMRef) { break; case 2: // This should really use the NumberFormat#formatRange proposal - if (direction === 'ltr') { - displayValue = `${state.getThumbValueLabel(0)} - ${state.getThumbValueLabel(1)}`; - } else { - displayValue = `${state.getThumbValueLabel(1)} - ${state.getThumbValueLabel(0)}`; - } + displayValue = `${state.getThumbValueLabel(0)} - ${state.getThumbValueLabel(1)}`; break; default: throw new Error('Only sliders with 1 or 2 handles are supported!'); From d5b2ecf881d7769ca51178d4ad6924d211899456 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Mon, 21 Sep 2020 19:07:02 +0200 Subject: [PATCH 17/40] Finish up keyboard tests for Slider --- .../slider/test/Slider.test.js | 107 ++++++++++++------ 1 file changed, 75 insertions(+), 32 deletions(-) diff --git a/packages/@react-spectrum/slider/test/Slider.test.js b/packages/@react-spectrum/slider/test/Slider.test.js index 1ed224228b0..93164cf84f3 100644 --- a/packages/@react-spectrum/slider/test/Slider.test.js +++ b/packages/@react-spectrum/slider/test/Slider.test.js @@ -20,22 +20,12 @@ import userEvent from '@testing-library/user-event'; function pressKeyOnButton(key, button) { act(() => {fireEvent.keyDown(button, {key});}); } - -function pressArrowRight(button) { - return pressKeyOnButton('ArrowRight', button); -} - -function pressArrowLeft(button) { - return pressKeyOnButton('ArrowLeft', button); -} - -function pressArrowUp(button) { - return pressKeyOnButton('ArrowUp', button); -} - -function pressArrowDown(button) { - return pressKeyOnButton('ArrowDown', button); -} +const press = { + ArrowRight: (button) => pressKeyOnButton('ArrowRight', button), + ArrowLeft: (button) => pressKeyOnButton('ArrowLeft', button), + Home: (button) => pressKeyOnButton('Home', button), + End: (button) => pressKeyOnButton('End', button) +}; describe('Slider', function () { it('supports aria-label', function () { @@ -182,36 +172,89 @@ describe('Slider', function () { }); }); - // TODO: assert DOM structure for isFilled/fillOffset, ticks, trackBackground? - describe('keyboard interactions', () => { - // See comment on onKeyDown in useDrag1D - // ${'(up/down arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${(slider) => {let v = Number(slider.value); pressArrowUp(slider); expect(slider).toHaveProperty('value', String(v - 1)); pressArrowDown(slider); expect(slider).toHaveProperty('value', String(v));}} - // ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${(slider) => {let v = Number(slider.value); pressArrowRight(slider); expect(slider).toHaveProperty('value', String(v - 1)); pressArrowLeft(slider); expect(slider).toHaveProperty('value', String(v));}} - it.skip.each` + // Can't test arrow/page up/down arrows because they are handled by the browser and JSDOM doesn't feel like it. + + it.each` Name | props | run - ${'(left/right arrows, ltr)'} | ${{locale: 'de-DE'}} | ${(slider) => {let v = Number(slider.value); pressArrowRight(slider); expect(slider).toHaveProperty('value', String(v + 1)); pressArrowLeft(slider); expect(slider).toHaveProperty('value', String(v));}} - ${'(up/down arrows, ltr)'} | ${{locale: 'de-DE'}} | ${(slider) => {let v = Number(slider.value); pressArrowUp(slider); expect(slider).toHaveProperty('value', String(v + 1)); pressArrowDown(slider); expect(slider).toHaveProperty('value', String(v));}} - `('$Name shifts button focus in the correct direction on key press', function ({Name, props, run}) { + ${'(left/right arrows, ltr)'} | ${{locale: 'de-DE'}} | ${(slider) => {let v = Number(slider.value); press.ArrowRight(slider); expect(slider).toHaveProperty('value', String(v + 1)); press.ArrowLeft(slider); expect(slider).toHaveProperty('value', String(v));}} + ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${(slider) => {let v = Number(slider.value); press.ArrowRight(slider); expect(slider).toHaveProperty('value', String(v - 1)); press.ArrowLeft(slider); expect(slider).toHaveProperty('value', String(v));}} + ${'(home/end, ltr)'} | ${{locale: 'de-DE'}} | ${(slider) => {press.Home(slider); expect(slider).toHaveProperty('value', '0'); press.End(slider); expect(slider).toHaveProperty('value', '100');}} + ${'(home/end, rtl)'} | ${{locale: 'ar-AE'}} | ${(slider) => {press.Home(slider); expect(slider).toHaveProperty('value', '0'); press.End(slider); expect(slider).toHaveProperty('value', '100');}} + `('$Name moves the slider in the correct direction', function ({Name, props, run}) { let tree = render( - + ); - let slider = tree.getByRole('slider'); + userEvent.tab(); + expect(document.activeElement).toBe(slider); + run(slider); + }); + it.each` + Name | props | run + ${'(left/right arrows, ltr)'} | ${{locale: 'de-DE'}} | ${(slider) => {let v = Number(slider.value); press.ArrowRight(slider); expect(slider).toHaveProperty('value', String(v + 10)); press.ArrowLeft(slider); expect(slider).toHaveProperty('value', String(v));}} + ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${(slider) => {let v = Number(slider.value); press.ArrowRight(slider); expect(slider).toHaveProperty('value', String(v - 10)); press.ArrowLeft(slider); expect(slider).toHaveProperty('value', String(v));}} + `('$Name respects the step size', function ({Name, props, run}) { + let tree = render( + + + + ); + let slider = tree.getByRole('slider'); userEvent.tab(); expect(document.activeElement).toBe(slider); + run(slider); + }); + it.each` + Name | props | run + ${'(left/right arrows, ltr)'} | ${{locale: 'de-DE'}} | ${(slider) => { + let v = Number(slider.value); + press.ArrowRight(slider); + expect(slider).toHaveProperty('value', String(v + 1)); + press.ArrowRight(slider); + expect(slider).toHaveProperty('value', String(v + 1)); + press.ArrowLeft(slider); + press.ArrowLeft(slider); + expect(slider).toHaveProperty('value', String(v - 1)); + press.ArrowLeft(slider); + expect(slider).toHaveProperty('value', String(v - 1)); + }} + ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${(slider) => { + let v = Number(slider.value); + press.ArrowRight(slider); + expect(slider).toHaveProperty('value', String(v - 1)); + press.ArrowRight(slider); + expect(slider).toHaveProperty('value', String(v - 1)); + press.ArrowLeft(slider); + press.ArrowLeft(slider); + expect(slider).toHaveProperty('value', String(v + 1)); + press.ArrowLeft(slider); + expect(slider).toHaveProperty('value', String(v + 1)); + }} + `('$Name is clamped by min/max', function ({Name, props, run}) { + let tree = render( + + + + ); + let slider = tree.getByRole('slider'); + userEvent.tab(); + expect(document.activeElement).toBe(slider); run(slider); }); + }); - // TODO verify that it's clamped by min/max + describe('mouse interactions', () => { + it.skip('can click and drag handle', () => { - // TODO step - }); + }); + + it.skip('can click on track to move handle', () => { - // TODO ??? - // describe('mouse interactions', () => { }); + }); + }); }); From 7590416a4883527088ac2316f4df999cdcd4d395 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Tue, 22 Sep 2020 13:10:36 +0200 Subject: [PATCH 18/40] Finish tests --- .../slider/test/RangeSlider.test.js | 45 --- .../slider/test/RangeSlider.test.tsx | 365 ++++++++++++++++++ .../slider/test/Slider.test.js | 260 ------------- .../slider/test/Slider.test.tsx | 364 +++++++++++++++++ packages/@react-spectrum/slider/test/utils.ts | 42 ++ 5 files changed, 771 insertions(+), 305 deletions(-) delete mode 100644 packages/@react-spectrum/slider/test/RangeSlider.test.js create mode 100644 packages/@react-spectrum/slider/test/RangeSlider.test.tsx delete mode 100644 packages/@react-spectrum/slider/test/Slider.test.js create mode 100644 packages/@react-spectrum/slider/test/Slider.test.tsx create mode 100644 packages/@react-spectrum/slider/test/utils.ts diff --git a/packages/@react-spectrum/slider/test/RangeSlider.test.js b/packages/@react-spectrum/slider/test/RangeSlider.test.js deleted file mode 100644 index 62e9c65f7c8..00000000000 --- a/packages/@react-spectrum/slider/test/RangeSlider.test.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -import {RangeSlider} from '../'; -import React from 'react'; -import {render} from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - - -describe('Slider', function () { - it('can be focused', function () { - let {getAllByRole} = render(
- - - -
); - - let [sliderLeft, sliderRight] = getAllByRole('slider'); - let [buttonA, buttonB] = getAllByRole('button'); - - userEvent.tab(); - expect(document.activeElement).toBe(buttonA); - userEvent.tab(); - expect(document.activeElement).toBe(sliderLeft); - userEvent.tab(); - expect(document.activeElement).toBe(sliderRight); - userEvent.tab(); - expect(document.activeElement).toBe(buttonB); - userEvent.tab({shift: true}); - expect(document.activeElement).toBe(sliderRight); - userEvent.tab({shift: true}); - expect(document.activeElement).toBe(sliderLeft); - userEvent.tab({shift: true}); - expect(document.activeElement).toBe(buttonA); - }); -}); diff --git a/packages/@react-spectrum/slider/test/RangeSlider.test.tsx b/packages/@react-spectrum/slider/test/RangeSlider.test.tsx new file mode 100644 index 00000000000..26edc49b0d8 --- /dev/null +++ b/packages/@react-spectrum/slider/test/RangeSlider.test.tsx @@ -0,0 +1,365 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {fireEvent, render} from '@testing-library/react'; +import {press, testKeypresses} from './utils'; +import {Provider} from '@adobe/react-spectrum'; +import {RangeSlider} from '../'; +import React, {useState} from 'react'; +import {theme} from '@react-spectrum/theme-default'; +import userEvent from '@testing-library/user-event'; + + +describe('Slider', function () { + it('supports aria-label', function () { + let {getByRole} = render(); + + let group = getByRole('group'); + expect(group).toHaveAttribute('aria-label', 'The Label'); + + // No label/value + expect(group.textContent).toBeFalsy(); + }); + + it('supports label', function () { + let {getAllByRole, getByRole} = render(); + + let group = getByRole('group'); + let labelId = group.getAttribute('aria-labelledby'); + let [leftSlider, rightSlider] = getAllByRole('slider'); + expect(leftSlider.getAttribute('aria-labelledby')).toBe(labelId); + expect(rightSlider.getAttribute('aria-labelledby')).toBe(labelId); + + expect(document.getElementById(labelId)).toHaveTextContent(/^The Label$/); + + // Shows value as well + expect(group.textContent).toBe('The Label0 - 100'); + }); + + it('supports showValueLabel: false', function () { + let {getByRole} = render(); + let group = getByRole('group'); + + expect(group.textContent).toBe('The Label'); + }); + + it('supports disabled', function () { + let {getAllByRole} = render(
+ + + +
); + + let [leftSlider, rightSlider] = getAllByRole('slider'); + expect(leftSlider).toBeDisabled(); + expect(rightSlider).toBeDisabled(); + let [buttonA, buttonB] = getAllByRole('button'); + + userEvent.tab(); + expect(document.activeElement).toBe(buttonA); + userEvent.tab(); + expect(document.activeElement).toBe(buttonB); + }); + + it('can be focused', function () { + let {getAllByRole} = render(
+ + + +
); + + let [sliderLeft, sliderRight] = getAllByRole('slider'); + let [buttonA, buttonB] = getAllByRole('button'); + + userEvent.tab(); + expect(document.activeElement).toBe(buttonA); + userEvent.tab(); + expect(document.activeElement).toBe(sliderLeft); + userEvent.tab(); + expect(document.activeElement).toBe(sliderRight); + userEvent.tab(); + expect(document.activeElement).toBe(buttonB); + userEvent.tab({shift: true}); + expect(document.activeElement).toBe(sliderRight); + userEvent.tab({shift: true}); + expect(document.activeElement).toBe(sliderLeft); + userEvent.tab({shift: true}); + expect(document.activeElement).toBe(buttonA); + }); + + it('supports defaultValue', function () { + let {getAllByRole} = render(); + + let [sliderLeft, sliderRight] = getAllByRole('slider'); + + expect(sliderLeft).toHaveProperty('value', '20'); + expect(sliderRight).toHaveProperty('value', '40'); + fireEvent.change(sliderLeft, {target: {value: '30'}}); + expect(sliderLeft).toHaveProperty('value', '30'); + fireEvent.change(sliderRight, {target: {value: '50'}}); + expect(sliderRight).toHaveProperty('value', '50'); + }); + + it('can be controlled', function () { + let renders = []; + + function Test() { + let [value, setValue] = useState({start: 20, end: 40}); + renders.push(value); + + return (); + } + + let {getAllByRole} = render(); + let [sliderLeft, sliderRight] = getAllByRole('slider'); + + expect(sliderLeft).toHaveProperty('value', '20'); + expect(sliderLeft).toHaveAttribute('aria-valuetext', '20'); + expect(sliderRight).toHaveProperty('value', '40'); + expect(sliderRight).toHaveAttribute('aria-valuetext', '40'); + fireEvent.change(sliderLeft, {target: {value: '30'}}); + expect(sliderLeft).toHaveProperty('value', '30'); + expect(sliderLeft).toHaveAttribute('aria-valuetext', '30'); + fireEvent.change(sliderRight, {target: {value: '50'}}); + expect(sliderRight).toHaveProperty('value', '50'); + expect(sliderRight).toHaveAttribute('aria-valuetext', '50'); + + expect(renders).toStrictEqual([{start: 20, end: 40}, {start: 30, end: 40}, {start: 30, end: 50}]); + }); + + it('supports a custom valueLabel', function () { + function Test() { + let [value, setValue] = useState({start: 10, end: 40}); + return (); + } + + let {getAllByRole, getByRole} = render(); + + let group = getByRole('group'); + let [sliderLeft, sliderRight] = getAllByRole('slider'); + + expect(group.textContent).toBe('The LabelA10B40C'); + // TODO should aria-valuetext be formatted as well? + expect(sliderLeft).toHaveAttribute('aria-valuetext', '10'); + expect(sliderRight).toHaveAttribute('aria-valuetext', '40'); + fireEvent.change(sliderLeft, {target: {value: '5'}}); + expect(sliderLeft).toHaveAttribute('aria-valuetext', '5'); + expect(group.textContent).toBe('The LabelA5B40C'); + fireEvent.change(sliderRight, {target: {value: '60'}}); + expect(group.textContent).toBe('The LabelA5B60C'); + expect(sliderRight).toHaveAttribute('aria-valuetext', '60'); + }); + + describe('formatOptions', () => { + it('prefixes the value with a plus sign if needed', function () { + let {getAllByRole, getByRole} = render( + + ); + + let group = getByRole('group'); + let [sliderLeft, sliderRight] = getAllByRole('slider'); + + expect(group.textContent).toBe('The Label+10 - +20'); + expect(sliderLeft).toHaveAttribute('aria-valuetext', '+10'); + expect(sliderRight).toHaveAttribute('aria-valuetext', '+20'); + fireEvent.change(sliderLeft, {target: {value: '-35'}}); + expect(sliderLeft).toHaveAttribute('aria-valuetext', '-35'); + expect(group.textContent).toBe('The Label-35 - +20'); + fireEvent.change(sliderRight, {target: {value: '0'}}); + expect(group.textContent).toBe('The Label-35 - 0'); + expect(sliderRight).toHaveAttribute('aria-valuetext', '0'); + }); + + it('supports setting custom formatOptions', function () { + let {getAllByRole, getByRole} = render( + + ); + + let group = getByRole('group'); + let [sliderLeft, sliderRight] = getAllByRole('slider'); + + expect(group.textContent).toBe('The Label20% - 60%'); + expect(sliderLeft).toHaveAttribute('aria-valuetext', '20%'); + expect(sliderRight).toHaveAttribute('aria-valuetext', '60%'); + fireEvent.change(sliderLeft, {target: {value: '0.3'}}); + expect(group.textContent).toBe('The Label30% - 60%'); + expect(sliderLeft).toHaveAttribute('aria-valuetext', '30%'); + fireEvent.change(sliderRight, {target: {value: '0.7'}}); + expect(group.textContent).toBe('The Label30% - 70%'); + expect(sliderRight).toHaveAttribute('aria-valuetext', '70%'); + }); + }); + + describe('keyboard interactions', () => { + // Can't test arrow/page up/down arrows because they are handled by the browser and JSDOM doesn't feel like it. + + it.each` + Name | props | commands + ${'(left/right arrows, ltr)'} | ${{locale: 'de-DE'}} | ${[{left: press.ArrowRight, result: +1}, {left: press.ArrowLeft, result: -1}, {right: press.ArrowRight, result: +1}, {right: press.ArrowLeft, result: -1}]} + ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${[{left: press.ArrowRight, result: -1}, {left: press.ArrowLeft, result: +1}, {right: press.ArrowRight, result: -1}, {right: press.ArrowLeft, result: +1}]} + ${'(home/end, ltr)'} | ${{locale: 'de-DE'}} | ${[{left: press.End, result: '50'}, {left: press.Home, result: '0'}, {left: press.ArrowRight, result: '1'}, {right: press.Home, result: '1'}, {right: press.End, result: '100'}]} + ${'(home/end, rtl)'} | ${{locale: 'ar-AE'}} | ${[{left: press.End, result: '50'}, {left: press.Home, result: '0'}, {left: press.ArrowLeft, result: '1'}, {right: press.Home, result: '1'}, {right: press.End, result: '100'}]} + `('$Name moves the slider in the correct direction', function ({props, commands}) { + let tree = render( + + + + ); + let sliders = tree.getAllByRole('slider') as [HTMLInputElement, HTMLInputElement]; + testKeypresses(sliders, commands); + }); + + it.each` + Name | props | commands + ${'(left/right arrows, ltr)'} | ${{locale: 'de-DE'}} | ${[{left: press.ArrowRight, result: +10}, {left: press.ArrowLeft, result: -10}, {right: press.ArrowRight, result: +10}, {right: press.ArrowLeft, result: -10}]} + ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${[{left: press.ArrowRight, result: -10}, {left: press.ArrowLeft, result: +10}, {right: press.ArrowRight, result: -10}, {right: press.ArrowLeft, result: +10}]} + `('$Name respects the step size', function ({props, commands}) { + let tree = render( + + + + ); + let sliders = tree.getAllByRole('slider') as [HTMLInputElement, HTMLInputElement]; + testKeypresses(sliders, commands); + }); + + it.each` + Name | props | commands + ${'(left/right arrows, ltr)'} | ${{locale: 'de-DE'}} | ${[{left: press.ArrowLeft, result: -1}, {left: press.ArrowLeft, result: 0}, {right: press.ArrowRight, result: +1}, {right: press.ArrowRight, result: 0}]} + ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${[{left: press.ArrowRight, result: -1}, {left: press.ArrowRight, result: 0}, {right: press.ArrowLeft, result: +1}, {right: press.ArrowLeft, result: 0}]} + `('$Name is clamped by min/max', function ({props, commands}) { + let tree = render( + + + + ); + let sliders = tree.getAllByRole('slider') as [HTMLInputElement, HTMLInputElement]; + testKeypresses(sliders, commands); + }); + }); + + describe('mouse interactions', () => { + beforeAll(() => { + jest.spyOn(window.HTMLElement.prototype, 'offsetWidth', 'get').mockImplementation(() => 100); + }); + afterAll(() => { + // @ts-ignore + window.HTMLElement.prototype.offsetWidth.mockReset(); + }); + + it('can click and drag handle', () => { + let onChangeSpy = jest.fn(); + let {getAllByRole} = render( + + ); + + let [sliderLeft, sliderRight] = getAllByRole('slider'); + let [thumbLeft, thumbRight] = [sliderLeft.parentElement.parentElement, sliderRight.parentElement.parentElement]; + + fireEvent.mouseDown(thumbLeft, {clientX: 20}); + expect(onChangeSpy).not.toHaveBeenCalled(); + expect(document.activeElement).toBe(sliderLeft); + fireEvent.mouseMove(thumbLeft, {clientX: 10}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy).toHaveBeenLastCalledWith({start: 10, end: 50}); + fireEvent.mouseMove(thumbLeft, {clientX: -10}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeSpy).toHaveBeenLastCalledWith({start: 0, end: 50}); + fireEvent.mouseMove(thumbLeft, {clientX: 120}); + expect(onChangeSpy).toHaveBeenCalledTimes(3); + expect(onChangeSpy).toHaveBeenLastCalledWith({start: 50, end: 50}); + fireEvent.mouseUp(thumbLeft, {clientX: 120}); + expect(onChangeSpy).toHaveBeenCalledTimes(3); + + onChangeSpy.mockClear(); + + fireEvent.mouseDown(thumbRight, {clientX: 50}); + expect(onChangeSpy).not.toHaveBeenCalled(); + expect(document.activeElement).toBe(sliderRight); + fireEvent.mouseMove(thumbRight, {clientX: 60}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy).toHaveBeenLastCalledWith({start: 50, end: 60}); + fireEvent.mouseMove(thumbRight, {clientX: -10}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeSpy).toHaveBeenLastCalledWith({start: 50, end: 50}); + fireEvent.mouseMove(thumbRight, {clientX: 120}); + expect(onChangeSpy).toHaveBeenCalledTimes(3); + expect(onChangeSpy).toHaveBeenLastCalledWith({start: 50, end: 100}); + fireEvent.mouseUp(thumbRight, {clientX: 120}); + expect(onChangeSpy).toHaveBeenCalledTimes(3); + }); + + it('can click on track to move nearest handle', () => { + let onChangeSpy = jest.fn(); + let {getAllByRole} = render( + + ); + + let [sliderLeft, sliderRight] = getAllByRole('slider'); + let [thumbLeft, thumbRight] = [sliderLeft.parentElement.parentElement, sliderRight.parentElement.parentElement]; + + // @ts-ignore + let [leftTrack, middleTrack, rightTrack] = [...thumbLeft.parentElement.children].filter(c => c !== thumbLeft && c !== thumbRight); + + // left track + fireEvent.mouseDown(leftTrack, {clientX: 20}); + expect(document.activeElement).toBe(sliderLeft); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy).toHaveBeenLastCalledWith({start: 20, end: 70}); + fireEvent.mouseUp(thumbLeft, {clientX: 20}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + + // middle track, near left slider + onChangeSpy.mockClear(); + fireEvent.mouseDown(middleTrack, {clientX: 40}); + expect(document.activeElement).toBe(sliderLeft); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy).toHaveBeenLastCalledWith({start: 40, end: 70}); + fireEvent.mouseUp(thumbLeft, {clientX: 40}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + + // middle track, near right slider + onChangeSpy.mockClear(); + fireEvent.mouseDown(middleTrack, {clientX: 60}); + expect(document.activeElement).toBe(sliderRight); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy).toHaveBeenLastCalledWith({start: 40, end: 60}); + fireEvent.mouseUp(thumbRight, {clientX: 60}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + + // right track + onChangeSpy.mockClear(); + fireEvent.mouseDown(rightTrack, {clientX: 90}); + expect(document.activeElement).toBe(sliderRight); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy).toHaveBeenLastCalledWith({start: 40, end: 90}); + fireEvent.mouseUp(thumbRight, {clientX: 90}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/@react-spectrum/slider/test/Slider.test.js b/packages/@react-spectrum/slider/test/Slider.test.js deleted file mode 100644 index 93164cf84f3..00000000000 --- a/packages/@react-spectrum/slider/test/Slider.test.js +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright 2020 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -import {act, fireEvent, render} from '@testing-library/react'; -import {Provider} from '@adobe/react-spectrum'; -import React, {useState} from 'react'; -import {Slider} from '../'; -import {theme} from '@react-spectrum/theme-default'; -import userEvent from '@testing-library/user-event'; - -function pressKeyOnButton(key, button) { - act(() => {fireEvent.keyDown(button, {key});}); -} -const press = { - ArrowRight: (button) => pressKeyOnButton('ArrowRight', button), - ArrowLeft: (button) => pressKeyOnButton('ArrowLeft', button), - Home: (button) => pressKeyOnButton('Home', button), - End: (button) => pressKeyOnButton('End', button) -}; - -describe('Slider', function () { - it('supports aria-label', function () { - let {getByRole} = render(); - - let group = getByRole('group'); - expect(group).toHaveAttribute('aria-label', 'The Label'); - - // No label/value - expect(group.textContent).toBeFalsy(); - }); - - it('supports label', function () { - let {getByRole} = render(); - - let group = getByRole('group'); - let labelId = group.getAttribute('aria-labelledby'); - let slider = getByRole('slider'); - expect(slider.getAttribute('aria-labelledby')).toBe(labelId); - - expect(document.getElementById(labelId)).toHaveTextContent(/^The Label$/); - - // Shows value as well - expect(group.textContent).toBe('The Label0'); - }); - - it('supports showValueLabel: false', function () { - let {getByRole} = render(); - let group = getByRole('group'); - - expect(group.textContent).toBe('The Label'); - }); - - it('supports disabled', function () { - let {getByRole} = render(); - - let slider = getByRole('slider'); - expect(slider).toBeDisabled(); - }); - - it('can be focused', function () { - let {getByRole, getAllByRole} = render(
- - - -
); - - let slider = getByRole('slider'); - let [buttonA, buttonB] = getAllByRole('button'); - act(() => { - slider.focus(); - }); - - expect(document.activeElement).toBe(slider); - userEvent.tab(); - expect(document.activeElement).toBe(buttonB); - userEvent.tab({shift: true}); - userEvent.tab({shift: true}); - expect(document.activeElement).toBe(buttonA); - }); - - it('supports defaultValue', function () { - let {getByRole} = render(); - - let slider = getByRole('slider'); - - expect(slider).toHaveProperty('value', '20'); - fireEvent.change(slider, {target: {value: '40'}}); - expect(slider).toHaveProperty('value', '40'); - }); - - it('can be controlled', function () { - let renders = []; - - function Test() { - let [value, setValue] = useState(50); - renders.push(value); - - return (); - } - - let {getByRole} = render(); - - let slider = getByRole('slider'); - - expect(slider).toHaveProperty('value', '50'); - fireEvent.change(slider, {target: {value: '55'}}); - expect(slider).toHaveProperty('value', '55'); - - expect(renders).toStrictEqual([50, 55]); - }); - - it('supports a custom valueLabel', function () { - function Test() { - let [value, setValue] = useState(50); - return (); - } - - let {getByRole} = render(); - - let group = getByRole('group'); - let slider = getByRole('slider'); - - expect(group.textContent).toBe('The LabelA50B'); - fireEvent.change(slider, {target: {value: '55'}}); - expect(group.textContent).toBe('The LabelA55B'); - }); - - describe('formatOptions', () => { - it('prefixes the value with a plus sign if needed', function () { - let {getByRole} = render( - - ); - - let group = getByRole('group'); - let slider = getByRole('slider'); - - expect(group.textContent).toBe('The Label+10'); - fireEvent.change(slider, {target: {value: '0'}}); - expect(group.textContent).toBe('The Label0'); - }); - - it('supports setting custom formatOptions', function () { - let {getByRole} = render( - - ); - - let group = getByRole('group'); - let slider = getByRole('slider'); - - expect(group.textContent).toBe('The Label20%'); - fireEvent.change(slider, {target: {value: 0.5}}); - expect(group.textContent).toBe('The Label50%'); - }); - }); - - describe('keyboard interactions', () => { - // Can't test arrow/page up/down arrows because they are handled by the browser and JSDOM doesn't feel like it. - - it.each` - Name | props | run - ${'(left/right arrows, ltr)'} | ${{locale: 'de-DE'}} | ${(slider) => {let v = Number(slider.value); press.ArrowRight(slider); expect(slider).toHaveProperty('value', String(v + 1)); press.ArrowLeft(slider); expect(slider).toHaveProperty('value', String(v));}} - ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${(slider) => {let v = Number(slider.value); press.ArrowRight(slider); expect(slider).toHaveProperty('value', String(v - 1)); press.ArrowLeft(slider); expect(slider).toHaveProperty('value', String(v));}} - ${'(home/end, ltr)'} | ${{locale: 'de-DE'}} | ${(slider) => {press.Home(slider); expect(slider).toHaveProperty('value', '0'); press.End(slider); expect(slider).toHaveProperty('value', '100');}} - ${'(home/end, rtl)'} | ${{locale: 'ar-AE'}} | ${(slider) => {press.Home(slider); expect(slider).toHaveProperty('value', '0'); press.End(slider); expect(slider).toHaveProperty('value', '100');}} - `('$Name moves the slider in the correct direction', function ({Name, props, run}) { - let tree = render( - - - - ); - let slider = tree.getByRole('slider'); - userEvent.tab(); - expect(document.activeElement).toBe(slider); - run(slider); - }); - - it.each` - Name | props | run - ${'(left/right arrows, ltr)'} | ${{locale: 'de-DE'}} | ${(slider) => {let v = Number(slider.value); press.ArrowRight(slider); expect(slider).toHaveProperty('value', String(v + 10)); press.ArrowLeft(slider); expect(slider).toHaveProperty('value', String(v));}} - ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${(slider) => {let v = Number(slider.value); press.ArrowRight(slider); expect(slider).toHaveProperty('value', String(v - 10)); press.ArrowLeft(slider); expect(slider).toHaveProperty('value', String(v));}} - `('$Name respects the step size', function ({Name, props, run}) { - let tree = render( - - - - ); - let slider = tree.getByRole('slider'); - userEvent.tab(); - expect(document.activeElement).toBe(slider); - run(slider); - }); - - it.each` - Name | props | run - ${'(left/right arrows, ltr)'} | ${{locale: 'de-DE'}} | ${(slider) => { - let v = Number(slider.value); - press.ArrowRight(slider); - expect(slider).toHaveProperty('value', String(v + 1)); - press.ArrowRight(slider); - expect(slider).toHaveProperty('value', String(v + 1)); - press.ArrowLeft(slider); - press.ArrowLeft(slider); - expect(slider).toHaveProperty('value', String(v - 1)); - press.ArrowLeft(slider); - expect(slider).toHaveProperty('value', String(v - 1)); - }} - ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${(slider) => { - let v = Number(slider.value); - press.ArrowRight(slider); - expect(slider).toHaveProperty('value', String(v - 1)); - press.ArrowRight(slider); - expect(slider).toHaveProperty('value', String(v - 1)); - press.ArrowLeft(slider); - press.ArrowLeft(slider); - expect(slider).toHaveProperty('value', String(v + 1)); - press.ArrowLeft(slider); - expect(slider).toHaveProperty('value', String(v + 1)); - }} - `('$Name is clamped by min/max', function ({Name, props, run}) { - let tree = render( - - - - ); - let slider = tree.getByRole('slider'); - userEvent.tab(); - expect(document.activeElement).toBe(slider); - run(slider); - }); - }); - - describe('mouse interactions', () => { - it.skip('can click and drag handle', () => { - - }); - - it.skip('can click on track to move handle', () => { - - }); - }); -}); diff --git a/packages/@react-spectrum/slider/test/Slider.test.tsx b/packages/@react-spectrum/slider/test/Slider.test.tsx new file mode 100644 index 00000000000..82656bf22ba --- /dev/null +++ b/packages/@react-spectrum/slider/test/Slider.test.tsx @@ -0,0 +1,364 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {act, fireEvent, render} from '@testing-library/react'; +import {press, testKeypresses} from './utils'; +import {Provider} from '@adobe/react-spectrum'; +import React, {useState} from 'react'; +import {Slider} from '../'; +import {theme} from '@react-spectrum/theme-default'; +import userEvent from '@testing-library/user-event'; + +describe('Slider', function () { + it('supports aria-label', function () { + let {getByRole} = render(); + + let group = getByRole('group'); + expect(group).toHaveAttribute('aria-label', 'The Label'); + + // No label/value + expect(group.textContent).toBeFalsy(); + + let slider = getByRole('slider'); + expect(slider).toHaveAttribute('aria-valuetext', '0'); + }); + + it('supports label', function () { + let {getByRole} = render(); + + let group = getByRole('group'); + let labelId = group.getAttribute('aria-labelledby'); + let slider = getByRole('slider'); + expect(slider.getAttribute('aria-labelledby')).toBe(labelId); + + expect(document.getElementById(labelId)).toHaveTextContent(/^The Label$/); + + // Shows value as well + expect(group.textContent).toBe('The Label0'); + expect(slider).toHaveAttribute('aria-valuetext', '0'); + }); + + it('supports showValueLabel: false', function () { + let {getByRole} = render(); + let group = getByRole('group'); + expect(group.textContent).toBe('The Label'); + + let slider = getByRole('slider'); + expect(slider).toHaveAttribute('aria-valuetext', '0'); + }); + + it('supports disabled', function () { + let {getByRole, getAllByRole} = render(
+ + + +
); + + let slider = getByRole('slider'); + let [buttonA, buttonB] = getAllByRole('button'); + expect(slider).toBeDisabled(); + + userEvent.tab(); + expect(document.activeElement).toBe(buttonA); + userEvent.tab(); + expect(document.activeElement).toBe(buttonB); + }); + + it('can be focused', function () { + let {getByRole, getAllByRole} = render(
+ + + +
); + + let slider = getByRole('slider'); + let [buttonA, buttonB] = getAllByRole('button'); + act(() => { + slider.focus(); + }); + + expect(document.activeElement).toBe(slider); + userEvent.tab(); + expect(document.activeElement).toBe(buttonB); + userEvent.tab({shift: true}); + userEvent.tab({shift: true}); + expect(document.activeElement).toBe(buttonA); + }); + + it('supports defaultValue', function () { + let {getByRole} = render(); + + let slider = getByRole('slider'); + + expect(slider).toHaveProperty('value', '20'); + expect(slider).toHaveAttribute('aria-valuetext', '20'); + fireEvent.change(slider, {target: {value: '40'}}); + expect(slider).toHaveProperty('value', '40'); + expect(slider).toHaveAttribute('aria-valuetext', '40'); + }); + + it('can be controlled', function () { + let renders = []; + + function Test() { + let [value, setValue] = useState(50); + renders.push(value); + + return (); + } + + let {getByRole} = render(); + + let slider = getByRole('slider'); + + expect(slider).toHaveProperty('value', '50'); + expect(slider).toHaveAttribute('aria-valuetext', '50'); + fireEvent.change(slider, {target: {value: '55'}}); + expect(slider).toHaveProperty('value', '55'); + expect(slider).toHaveAttribute('aria-valuetext', '55'); + + expect(renders).toStrictEqual([50, 55]); + }); + + it('supports a custom valueLabel', function () { + function Test() { + let [value, setValue] = useState(50); + return (); + } + + let {getByRole} = render(); + + let group = getByRole('group'); + let slider = getByRole('slider'); + + expect(group.textContent).toBe('The LabelA50B'); + // TODO should aria-valuetext be formatted as well? + expect(slider).toHaveAttribute('aria-valuetext', '50'); + fireEvent.change(slider, {target: {value: '55'}}); + expect(group.textContent).toBe('The LabelA55B'); + expect(slider).toHaveAttribute('aria-valuetext', '55'); + }); + + describe('formatOptions', () => { + it('prefixes the value with a plus sign if needed', function () { + let {getByRole} = render( + + ); + + let group = getByRole('group'); + let slider = getByRole('slider'); + + expect(group.textContent).toBe('The Label+10'); + expect(slider).toHaveAttribute('aria-valuetext', '+10'); + fireEvent.change(slider, {target: {value: '0'}}); + expect(group.textContent).toBe('The Label0'); + expect(slider).toHaveAttribute('aria-valuetext', '0'); + }); + + it('supports setting custom formatOptions', function () { + let {getByRole} = render( + + ); + + let group = getByRole('group'); + let slider = getByRole('slider'); + + expect(group.textContent).toBe('The Label20%'); + expect(slider).toHaveAttribute('aria-valuetext', '20%'); + fireEvent.change(slider, {target: {value: 0.5}}); + expect(group.textContent).toBe('The Label50%'); + expect(slider).toHaveAttribute('aria-valuetext', '50%'); + }); + }); + + describe('keyboard interactions', () => { + // Can't test arrow/page up/down arrows because they are handled by the browser and JSDOM doesn't feel like it. + + it.each` + Name | props | commands + ${'(left/right arrows, ltr)'} | ${{locale: 'de-DE'}} | ${[{left: press.ArrowRight, result: +1}, {left: press.ArrowLeft, result: -1}]} + ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${[{left: press.ArrowRight, result: -1}, {left: press.ArrowLeft, result: +1}]} + ${'(home/end, ltr)'} | ${{locale: 'de-DE'}} | ${[{left: press.End, result: '100'}, {left: press.Home, result: '0'}]} + ${'(home/end, rtl)'} | ${{locale: 'ar-AE'}} | ${[{left: press.End, result: '100'}, {left: press.Home, result: '0'}]} + ${'(left/right arrows, disabled)'} | ${{locale: 'de-DE', isDisabled: true}}| ${[{left: press.ArrowRight, result: 0}, {left: press.ArrowLeft, result: 0}]} + ${'(home/end, disabled)'} | ${{locale: 'de-DE', isDisabled: true}}| ${[{left: press.End, result: 0}, {left: press.Home, result: 0}]} + `('$Name moves the slider in the correct direction', function ({props, commands}) { + let tree = render( + + + + ); + let slider = tree.getByRole('slider'); + testKeypresses([slider, slider], commands); + }); + + it.each` + Name | props | commands + ${'(left/right arrows, ltr)'} | ${{locale: 'de-DE'}} | ${[{left: press.ArrowRight, result: +10}, {left: press.ArrowLeft, result: -10}]} + ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${[{left: press.ArrowRight, result: -10}, {left: press.ArrowLeft, result: +10}]} + `('$Name respects the step size', function ({props, commands}) { + let tree = render( + + + + ); + let slider = tree.getByRole('slider'); + testKeypresses([slider, slider], commands); + }); + + it.each` + Name | props | commands + ${'(left/right arrows, ltr)'} | ${{locale: 'de-DE'}} | ${[{left: press.ArrowLeft, result: -1}, {left: press.ArrowLeft, result: 0}]} + ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${[{left: press.ArrowRight, result: -1}, {left: press.ArrowRight, result: 0}]} + `('$Name is clamped by min/max', function ({props, commands}) { + let tree = render( + + + + ); + let slider = tree.getByRole('slider'); + testKeypresses([slider, slider], commands); + }); + }); + + describe('mouse interactions', () => { + beforeAll(() => { + jest.spyOn(window.HTMLElement.prototype, 'offsetWidth', 'get').mockImplementation(() => 100); + }); + afterAll(() => { + // @ts-ignore + window.HTMLElement.prototype.offsetWidth.mockReset(); + }); + + it('can click and drag handle', () => { + let onChangeSpy = jest.fn(); + let {getByRole} = render( + + ); + + let slider = getByRole('slider'); + let thumb = slider.parentElement; + fireEvent.mouseDown(thumb, {clientX: 50}); + expect(onChangeSpy).not.toHaveBeenCalled(); + expect(document.activeElement).toBe(slider); + + fireEvent.mouseMove(thumb, {clientX: 10}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy).toHaveBeenLastCalledWith(10); + fireEvent.mouseMove(thumb, {clientX: -10}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeSpy).toHaveBeenLastCalledWith(0); + fireEvent.mouseMove(thumb, {clientX: 120}); + expect(onChangeSpy).toHaveBeenCalledTimes(3); + expect(onChangeSpy).toHaveBeenLastCalledWith(100); + fireEvent.mouseUp(thumb, {clientX: 120}); + expect(onChangeSpy).toHaveBeenCalledTimes(3); + }); + + it('cannot click and drag handle when disabled', () => { + let onChangeSpy = jest.fn(); + let {getByRole} = render( + + ); + + let slider = getByRole('slider'); + let thumb = slider.parentElement; + fireEvent.mouseDown(thumb, {clientX: 50}); + expect(onChangeSpy).not.toHaveBeenCalled(); + expect(document.activeElement).not.toBe(slider); + fireEvent.mouseMove(thumb, {clientX: 10}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + fireEvent.mouseUp(thumb, {clientX: 10}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + }); + + it('can click on track to move handle', () => { + let onChangeSpy = jest.fn(); + let {getByRole} = render( + + ); + + let slider = getByRole('slider'); + let thumb = slider.parentElement.parentElement; + // @ts-ignore + let [leftTrack, rightTrack] = [...thumb.parentElement.children].filter(c => c !== thumb); + + // left track + fireEvent.mouseDown(leftTrack, {clientX: 20}); + expect(document.activeElement).toBe(slider); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy).toHaveBeenLastCalledWith(20); + fireEvent.mouseUp(thumb, {clientX: 20}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + + // right track + onChangeSpy.mockClear(); + fireEvent.mouseDown(rightTrack, {clientX: 70}); + expect(document.activeElement).toBe(slider); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy).toHaveBeenLastCalledWith(70); + fireEvent.mouseUp(thumb, {clientX: 70}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + }); + + it('cannot click on track to move handle when disabled', () => { + let onChangeSpy = jest.fn(); + let {getByRole} = render( + + ); + + let slider = getByRole('slider'); + let thumb = slider.parentElement.parentElement; + // @ts-ignore + let [leftTrack, rightTrack] = [...thumb.parentElement.children].filter(c => c !== thumb); + + // left track + fireEvent.mouseDown(leftTrack, {clientX: 20}); + expect(document.activeElement).not.toBe(slider); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + fireEvent.mouseUp(thumb, {clientX: 20}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + + // right track + onChangeSpy.mockClear(); + fireEvent.mouseDown(rightTrack, {clientX: 70}); + expect(document.activeElement).not.toBe(slider); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + fireEvent.mouseUp(thumb, {clientX: 70}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + }); + }); +}); diff --git a/packages/@react-spectrum/slider/test/utils.ts b/packages/@react-spectrum/slider/test/utils.ts new file mode 100644 index 00000000000..6558eb0fb81 --- /dev/null +++ b/packages/@react-spectrum/slider/test/utils.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {act, fireEvent} from '@testing-library/react'; + +function pressKeyOnButton(key, button) { + act(() => {fireEvent.keyDown(button, {key});}); +} +export const press = { + ArrowRight: (button: HTMLElement) => pressKeyOnButton('ArrowRight', button), + ArrowLeft: (button: HTMLElement) => pressKeyOnButton('ArrowLeft', button), + Home: (button: HTMLElement) => pressKeyOnButton('Home', button), + End: (button: HTMLElement) => pressKeyOnButton('End', button) +}; + +export function testKeypresses([sliderLeft, sliderRight], commands: any[]) { + for (let command of commands) { + let c = command.left ?? command.right; + let result = command.result; + let slider = command.left ? sliderLeft : sliderRight; + let oldValue = Number(slider.value); + act(() => {slider.focus();}); + c(slider); + + if (typeof result === 'string') { + // absolute + expect(slider).toHaveProperty('value', result); + } else { + // number, relative + expect(slider).toHaveProperty('value', String(oldValue + result)); + } + } +} From 6beeffabc8fe39193e6727774ac1bd5608824197 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Tue, 22 Sep 2020 13:22:24 +0200 Subject: [PATCH 19/40] Remove docs template --- .../@react-spectrum/slider/docs/Slider.mdx | 72 ------------------- 1 file changed, 72 deletions(-) delete mode 100644 packages/@react-spectrum/slider/docs/Slider.mdx diff --git a/packages/@react-spectrum/slider/docs/Slider.mdx b/packages/@react-spectrum/slider/docs/Slider.mdx deleted file mode 100644 index 2e504b71cc2..00000000000 --- a/packages/@react-spectrum/slider/docs/Slider.mdx +++ /dev/null @@ -1,72 +0,0 @@ - - -import {Layout} from '@react-spectrum/docs'; -export default Layout; - -import docs from 'docs:@react-spectrum/slider'; -import {HeaderInfo, PropTable} from '@react-spectrum/docs'; -import packageData from '@react-spectrum/slider/package.json'; - -```jsx import -import {Slider} from '@react-spectrum/slider'; -``` - ---- -category: Category Name -keywords: [] ---- - -# Slider - -

{docs.exports.Slider.description}

- - - -## Example - -```tsx example -Button -``` - -## Content - -*If the component has a children prop that accepts any type of content (e.g. `ReactNode`), include this section. Please include a note about how to internationalize the content.* - -## Value - -*If the component displays or allows a user to input a value, include this section.* - -## Labeling - -*If the component supports a label prop, include this section. Please include a note about how to internationalize the content.* - -## Events - -*If the component supports event props, include this section. Only cover the events that are important to the main functionality of the component.* - -## Validation - -*If the component supports validation props, include this section.* - -## Props - - - -## Visual options - -*Show examples of all variants and visual props here with links to the design website for more usage details. Examples can be grouped together for conciseness.* - -### Sample Option -[View guidelines](https://spectrum.adobe.com/page/text-field/#Width) From c5c3fca0ce17a2221d6edd2cb56bcd5d35749991 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Tue, 22 Sep 2020 15:31:22 +0200 Subject: [PATCH 20/40] Hover effect only on handles --- .../slider/src/RangeSlider.tsx | 42 +++++++++---------- .../@react-spectrum/slider/src/Slider.tsx | 29 +++++++------ .../@react-spectrum/slider/src/SliderBase.tsx | 13 ++---- 3 files changed, 40 insertions(+), 44 deletions(-) diff --git a/packages/@react-spectrum/slider/src/RangeSlider.tsx b/packages/@react-spectrum/slider/src/RangeSlider.tsx index 5fee357bb33..7a18d411012 100644 --- a/packages/@react-spectrum/slider/src/RangeSlider.tsx +++ b/packages/@react-spectrum/slider/src/RangeSlider.tsx @@ -13,10 +13,12 @@ import {classNames} from '@react-spectrum/utils'; import {DEFAULT_MAX_VALUE, DEFAULT_MIN_VALUE} from '@react-stately/slider'; import {FocusRing} from '@react-aria/focus'; +import {mergeProps} from '@react-aria/utils'; import React from 'react'; import {SliderBase, toPercent, useSliderBase, UseSliderBaseInputProps} from './SliderBase'; import {SpectrumRangeSliderProps} from '@react-types/slider'; import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; +import {useHover} from '@react-aria/interactions'; import {VisuallyHidden} from '@adobe/react-spectrum'; function RangeSlider(props: SpectrumRangeSliderProps) { @@ -24,7 +26,6 @@ function RangeSlider(props: SpectrumRangeSliderProps) { let ariaProps: UseSliderBaseInputProps = { ...otherProps, - // Normalize `value: number[]` to `value: number` value: value != null ? [value.start, value.end] : undefined, defaultValue: defaultValue != null ? [defaultValue.start, defaultValue.end] : // make sure that useSliderState knows we have two handles @@ -34,13 +35,16 @@ function RangeSlider(props: SpectrumRangeSliderProps) { } }; - let {inputRefs, + let { + inputRefs, thumbProps, inputProps, ticks, - isHovered, ...containerProps} = useSliderBase(2, ariaProps); - + ...containerProps + } = useSliderBase(2, ariaProps); let {state, direction} = containerProps; + let hovers = [useHover({}), useHover({})]; + let [leftSliderIndex, rightSliderIndex] = direction === 'ltr' ? [0, 1] : [1, 0]; let leftTrack = (
); + let handles = [0, 1].map(i => (
+ + + +
)); + return ( {leftTrack} {ticks} -
- - - -
+ {handles[0]}
{middleTrack} -
- - - -
+ {handles[1]}
{rightTrack}
); diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx index ee5c0d44bf8..a549d4708c7 100644 --- a/packages/@react-spectrum/slider/src/Slider.tsx +++ b/packages/@react-spectrum/slider/src/Slider.tsx @@ -10,13 +10,14 @@ * governing permissions and limitations under the License. */ -import {clamp} from '@react-aria/utils'; +import {clamp, mergeProps} from '@react-aria/utils'; import {classNames} from '@react-spectrum/utils'; import {FocusRing} from '@react-aria/focus'; import React from 'react'; import {SliderBase, toPercent, useSliderBase, UseSliderBaseInputProps} from './SliderBase'; import {SpectrumSliderProps} from '@react-types/slider'; import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; +import {useHover} from '@react-aria/interactions'; import {VisuallyHidden} from '@adobe/react-spectrum'; function Slider(props: SpectrumSliderProps) { @@ -32,7 +33,9 @@ function Slider(props: SpectrumSliderProps) { } }; - let {inputRefs: [inputRef], thumbProps: [thumbProps], inputProps: [inputProps], ticks, isHovered, ...containerProps} = useSliderBase(1, ariaProps); + let {isHovered, hoverProps} = useHover({}); + + let {inputRefs: [inputRef], thumbProps: [thumbProps], inputProps: [inputProps], ticks, ...containerProps} = useSliderBase(1, ariaProps); let {state, direction} = containerProps; fillOffset = fillOffset != null ? clamp(fillOffset, state.getThumbMinValue(0), state.getThumbMaxValue(0)) : fillOffset; @@ -49,15 +52,6 @@ function Slider(props: SpectrumSliderProps) { // @ts-ignore '--spectrum-track-background-size': `${(1 / state.getThumbPercent(0)) * 100}%` }} />); - let handle = (
- - - -
); let rightTrack = (
); + let handle = (
+ + + +
); + let filledTrack = null; if (isFilled && fillOffset != null) { let width = state.getThumbPercent(0) - state.getValuePercent(fillOffset); @@ -98,7 +102,8 @@ function Slider(props: SpectrumSliderProps) { {rightTrack} {filledTrack} - ); + + ); } // TODO forwardRef? diff --git a/packages/@react-spectrum/slider/src/SliderBase.tsx b/packages/@react-spectrum/slider/src/SliderBase.tsx index ed00935c25b..36452ca944a 100644 --- a/packages/@react-spectrum/slider/src/SliderBase.tsx +++ b/packages/@react-spectrum/slider/src/SliderBase.tsx @@ -12,12 +12,10 @@ import {AriaLabelingProps, Direction, DOMRef, LabelableProps, LabelPosition, Orientation} from '@react-types/shared'; import {classNames, useDOMRef} from '@react-spectrum/utils'; -import {mergeProps} from '@react-aria/utils'; import React, {CSSProperties, HTMLAttributes, MutableRefObject, ReactNode, ReactNodeArray, useRef} from 'react'; import {SliderProps, SpectrumSliderTicksBase} from '@react-types/slider'; import {SliderState, useSliderState} from '@react-stately/slider'; import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; -import {useHover} from '@react-aria/interactions'; import {useLocale} from '@react-aria/i18n'; import {useProviderProps} from '@react-spectrum/provider'; import {useSlider, useSliderThumb} from '@react-aria/slider'; @@ -35,7 +33,6 @@ export function toPercent(value: number, direction: Direction = 'ltr'): string { export interface UseSliderBaseContainerProps extends AriaLabelingProps, LabelableProps { state: SliderState, trackRef: MutableRefObject, - hoverProps: HTMLAttributes, isDisabled?: boolean, orientation?: Orientation, labelPosition?: LabelPosition, @@ -58,7 +55,6 @@ export interface UseSliderBaseOutputProps extends UseSliderBaseContainerProps { inputRefs: MutableRefObject[], thumbProps: HTMLAttributes[], inputProps: HTMLAttributes[], - isHovered: boolean, ticks: ReactNode } @@ -69,9 +65,6 @@ export function useSliderBase(count: number, props: UseSliderBaseInputProps): Us let thumbProps = []; let inputProps = []; - // TODO the two handles on the range slider should have individual hover effects - let {hoverProps, isHovered} = useHover({/* isDisabled */ }); - // Assumes that DEFAULT_MIN_VALUE and DEFAULT_MAX_VALUE are both positive, this value needs to be passed to useSliderState, so // getThumbMinValue/getThumbMaxValue cannot be used here. // `Math.abs(Math.sign(a) - Math.sign(b)) === 2` is true if the values have a different sign and neither is null. @@ -144,7 +137,7 @@ export function useSliderBase(count: number, props: UseSliderBaseInputProps): Us ticks, inputRefs, thumbProps, inputProps, trackRef, state, containerProps, trackProps, labelProps, direction, - hoverProps, isHovered, isDisabled, + isDisabled, label: props.label, showValueLabel: props.showValueLabel, labelPosition: props.labelPosition, @@ -173,7 +166,7 @@ function SliderBase(props: SliderBaseProps, ref: DOMRef) { let { state, children, classes, style, - trackRef, hoverProps, isDisabled, + trackRef, isDisabled, labelProps, containerProps, trackProps, labelPosition = 'top', valueLabel, showValueLabel = !!props.label } = props; @@ -213,7 +206,7 @@ function SliderBase(props: SliderBaseProps, ref: DOMRef) { {labelPosition === 'top' && showValueLabel && valueNode}
} -
+
{children}
{labelPosition === 'side' && From 6fd3cb8ccf8ba3976b9db24affd948f7032caff3 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Tue, 22 Sep 2020 15:39:14 +0200 Subject: [PATCH 21/40] Cleanup --- .../slider/src/RangeSlider.tsx | 2 +- .../@react-spectrum/slider/src/Slider.tsx | 2 +- .../@react-spectrum/slider/src/SliderBase.tsx | 21 +++++++++---------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/@react-spectrum/slider/src/RangeSlider.tsx b/packages/@react-spectrum/slider/src/RangeSlider.tsx index 7a18d411012..3dea1f50188 100644 --- a/packages/@react-spectrum/slider/src/RangeSlider.tsx +++ b/packages/@react-spectrum/slider/src/RangeSlider.tsx @@ -82,5 +82,5 @@ function RangeSlider(props: SpectrumRangeSliderProps) { ); } -// TODO forwardRef? +// TODO forwardref? export {RangeSlider}; diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx index a549d4708c7..9b6a490da7a 100644 --- a/packages/@react-spectrum/slider/src/Slider.tsx +++ b/packages/@react-spectrum/slider/src/Slider.tsx @@ -106,5 +106,5 @@ function Slider(props: SpectrumSliderProps) { ); } -// TODO forwardRef? +// TODO forwardref? export {Slider}; diff --git a/packages/@react-spectrum/slider/src/SliderBase.tsx b/packages/@react-spectrum/slider/src/SliderBase.tsx index 36452ca944a..16709d8ef4d 100644 --- a/packages/@react-spectrum/slider/src/SliderBase.tsx +++ b/packages/@react-spectrum/slider/src/SliderBase.tsx @@ -10,8 +10,8 @@ * governing permissions and limitations under the License. */ -import {AriaLabelingProps, Direction, DOMRef, LabelableProps, LabelPosition, Orientation} from '@react-types/shared'; -import {classNames, useDOMRef} from '@react-spectrum/utils'; +import {AriaLabelingProps, Direction, LabelableProps, LabelPosition, Orientation} from '@react-types/shared'; +import {classNames} from '@react-spectrum/utils'; import React, {CSSProperties, HTMLAttributes, MutableRefObject, ReactNode, ReactNodeArray, useRef} from 'react'; import {SliderProps, SpectrumSliderTicksBase} from '@react-types/slider'; import {SliderState, useSliderState} from '@react-stately/slider'; @@ -108,7 +108,6 @@ export function useSliderBase(count: number, props: UseSliderBaseInputProps): Us inputProps[i] = v.inputProps; thumbProps[i] = v.thumbProps; - // TODO do we want to use the thumb's labelProps? } let {tickCount, showTickLabels, tickLabels, isDisabled} = props; @@ -160,10 +159,7 @@ export interface SliderBaseProps extends UseSliderBaseContainerProps, LabelableP style?: CSSProperties } -function SliderBase(props: SliderBaseProps, ref: DOMRef) { - // needed? - useDOMRef(ref); - +function SliderBase(props: SliderBaseProps) { let { state, children, classes, style, trackRef, isDisabled, @@ -178,7 +174,9 @@ function SliderBase(props: SliderBaseProps, ref: DOMRef) { displayValue = state.getThumbValueLabel(0); break; case 2: - // This should really use the NumberFormat#formatRange proposal + // This should really use the NumberFormat#formatRange proposal... + // https://github.com/tc39/ecma402/issues/393 + // https://github.com/tc39/proposal-intl-numberformat-v3#formatrange-ecma-402-393 displayValue = `${state.getThumbValueLabel(0)} - ${state.getThumbValueLabel(1)}`; break; default: @@ -214,8 +212,9 @@ function SliderBase(props: SliderBaseProps, ref: DOMRef) { {showValueLabel && valueNode}
} -
); +
+ ); } -const _SliderBase = React.forwardRef(SliderBase); -export {_SliderBase as SliderBase}; +// TODO forwardref? +export {SliderBase}; From c239749f916d16b71399043e05baffa9ceac9a57 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Tue, 22 Sep 2020 17:46:34 +0200 Subject: [PATCH 22/40] Update CI to newer Node 12 --- .circleci/config.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 415ec72c870..d6147e60f40 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,7 +4,7 @@ orbs: jobs: install: docker: - - image: circleci/node:12.10 + - image: circleci/node:12 resource_class: large working_directory: ~/react-spectrum @@ -40,7 +40,7 @@ jobs: test: docker: - - image: circleci/node:12.10 + - image: circleci/node:12 working_directory: ~/react-spectrum steps: @@ -53,7 +53,7 @@ jobs: test_17: docker: - - image: circleci/node:12.10 + - image: circleci/node:12 working_directory: ~/react-spectrum steps: @@ -66,7 +66,7 @@ jobs: lint: docker: - - image: circleci/node:12.10 + - image: circleci/node:12 working_directory: ~/react-spectrum steps: @@ -79,7 +79,7 @@ jobs: storybook: docker: - - image: circleci/node:12.10 + - image: circleci/node:12 resource_class: large working_directory: ~/react-spectrum @@ -98,7 +98,7 @@ jobs: storybook-17: docker: - - image: circleci/node:12.10 + - image: circleci/node:12 resource_class: large working_directory: ~/react-spectrum @@ -117,7 +117,7 @@ jobs: docs: docker: - - image: circleci/node:12.10 + - image: circleci/node:12 resource_class: xlarge working_directory: ~/react-spectrum @@ -136,7 +136,7 @@ jobs: docs-production: docker: - - image: circleci/node:12.10 + - image: circleci/node:12 resource_class: large working_directory: ~/react-spectrum @@ -176,7 +176,7 @@ jobs: comment: docker: - - image: circleci/node:12.10 + - image: circleci/node:12 working_directory: ~/react-spectrum steps: - checkout @@ -192,7 +192,7 @@ jobs: publish-nightly: docker: - - image: circleci/node:12.10 + - image: circleci/node:12 resource_class: xlarge working_directory: ~/react-spectrum steps: From 1ad5b73c8df424736d984d711b1702b67d32a578 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Wed, 23 Sep 2020 09:56:15 +0200 Subject: [PATCH 23/40] Update Spectrum CSS, do RTL properly --- .../components/slider/index.css | 240 ++++++++++-------- .../components/slider/skin.css | 4 +- .../slider/src/RangeSlider.tsx | 20 +- .../@react-spectrum/slider/src/Slider.tsx | 24 +- .../@react-spectrum/slider/src/SliderBase.tsx | 10 - 5 files changed, 156 insertions(+), 142 deletions(-) diff --git a/packages/@adobe/spectrum-css-temp/components/slider/index.css b/packages/@adobe/spectrum-css-temp/components/slider/index.css index 493bc910e3e..a913fa67637 100644 --- a/packages/@adobe/spectrum-css-temp/components/slider/index.css +++ b/packages/@adobe/spectrum-css-temp/components/slider/index.css @@ -53,8 +53,8 @@ governing permissions and limitations under the License. /* Don't let z-index'd child elements float above other things on the page */ z-index: 1; display: block; - min-height: var(--spectrum-slider-height); - min-width: var(--spectrum-slider-min-width); + min-block-size: var(--spectrum-slider-height); + min-inline-size: var(--spectrum-slider-min-width); user-select: none; } @@ -67,27 +67,28 @@ governing permissions and limitations under the License. z-index: auto; /* These calculations prevent the track from spilling outside of the control */ - width: calc(100% - calc(var(--spectrum-slider-controls-margin) * 2)); - margin-left: var(--spectrum-slider-controls-margin); - min-height: var(--spectrum-slider-height); + inline-size: calc(100% - calc(var(--spectrum-slider-controls-margin) * 2)); + margin-inline-start: var(--spectrum-slider-controls-margin); + min-block-size: var(--spectrum-slider-height); vertical-align: top; } + .spectrum-Slider-track, .spectrum-Slider-buffer, .spectrum-Slider-ramp, .spectrum-Slider-fill { - height: var(--spectrum-slider-track-height); + block-size: var(--spectrum-slider-track-height); box-sizing: border-box; position: absolute; z-index: 1; - top: calc(var(--spectrum-slider-height) / 2); - left: 0; - right: auto; + inset-block-start: calc(var(--spectrum-slider-height) / 2); + inset-inline-start: 0; + inset-inline-end: auto; - margin-top: calc(var(--spectrum-slider-fill-track-height) / -2); + margin-block-start: calc(var(--spectrum-slider-fill-track-height) / -2); pointer-events: none; } @@ -95,43 +96,49 @@ governing permissions and limitations under the License. .spectrum-Slider-track, .spectrum-Slider-buffer, .spectrum-Slider-fill { - padding: 0 var(--spectrum-slider-track-handleoffset) 0 0; - margin-left: var(--spectrum-slider-track-margin-offset); + padding-block: 0; + padding-inline: 0 var(--spectrum-slider-track-handleoffset); + margin-inline-start: var(--spectrum-slider-track-margin-offset); &::before { content: ''; display: block; - height: 100%; + block-size: 100%; border-radius: var(--spectrum-slider-track-border-radius); } } .spectrum-Slider-fill { - margin-left: 0; - padding: 0 0 0 calc(var(--spectrum-slider-controls-margin) + var(--spectrum-slider-track-handleoffset)); + margin-inline-start: 0; + padding-block: 0; + padding-inline: calc(var(--spectrum-slider-controls-margin) + var(--spectrum-slider-track-handleoffset)) 0; } .spectrum-Slider-fill--right { - padding: 0 calc(var(--spectrum-slider-controls-margin) + var(--spectrum-slider-track-handleoffset)) 0 0; + padding-block: 0; + padding-inline: 0 calc(var(--spectrum-slider-controls-margin) + var(--spectrum-slider-track-handleoffset)); } .spectrum-Slider-buffer { - padding: 0 var(--spectrum-slider-track-handleoffset) 0 0; + padding-block: 0; + padding-inline: 0 var(--spectrum-slider-track-handleoffset); } .spectrum-Slider-track ~ .spectrum-Slider-track, .spectrum-Slider-buffer ~ .spectrum-Slider-buffer { - left: auto; - right: var(--spectrum-slider-range-track-reset); - padding: 0 0 0 var(--spectrum-slider-track-handleoffset); - margin-left: var(--spectrum-slider-range-track-reset); - margin-right: var(--spectrum-slider-track-margin-offset); + inset-inline-start: auto; + inset-inline-end: var(--spectrum-slider-range-track-reset); + padding-block: 0; + padding-inline: var(--spectrum-slider-track-handleoffset) 0; + margin-inline-start: var(--spectrum-slider-range-track-reset); + margin-inline-end: var(--spectrum-slider-track-margin-offset); } .spectrum-Slider-buffer ~ .spectrum-Slider-buffer { - margin-right: var(--spectrum-slider-range-track-reset); - padding: 0 0 0 var(--spectrum-slider-track-middle-handleoffset); + margin-inline-end: var(--spectrum-slider-range-track-reset); + padding-block: 0; + padding-inline: var(--spectrum-slider-track-middle-handleoffset) 0; &:after { display: none; @@ -145,22 +152,29 @@ governing permissions and limitations under the License. .spectrum-Slider-track { &:first-of-type { - padding: 0 var(--spectrum-slider-track-handleoffset) 0 0; - left: var(--spectrum-slider-range-track-reset); - right: auto; - margin-left: var(--spectrum-slider-track-margin-offset); + padding-block: 0; + padding-inline: 0 var(--spectrum-slider-track-handleoffset); + inset-inline-start: var(--spectrum-slider-range-track-reset); + inset-inline-end: auto; + margin-inline-start: var(--spectrum-slider-track-margin-offset); } - & { - padding: 0 var(--spectrum-slider-track-middle-handleoffset) 0 var(--spectrum-slider-track-middle-handleoffset); - left: auto; - right: auto; - margin: var(--spectrum-slider-range-track-reset); + + /* Force specificity otherwise the ~ rules above override */ + &:dir(ltr), + &:dir(rtl) { + padding-block: 0; + padding-inline: var(--spectrum-slider-track-middle-handleoffset) var(--spectrum-slider-track-middle-handleoffset); + inset-inline-start: auto; + inset-inline-end: auto; + margin-inline: var(--spectrum-slider-range-track-reset); + margin-block: var(--spectrum-slider-range-track-reset); } &:last-of-type { - padding: 0 0 0 var(--spectrum-slider-track-handleoffset); - left: auto; - right: var(--spectrum-slider-range-track-reset); - margin-right: var(--spectrum-slider-track-margin-offset); + padding-block: 0; + padding-inline: var(--spectrum-slider-track-handleoffset) 0; + inset-inline-start: auto; + inset-inline-end: var(--spectrum-slider-range-track-reset); + margin-inline-end: var(--spectrum-slider-track-margin-offset); } } } @@ -171,33 +185,37 @@ governing permissions and limitations under the License. } .spectrum-Slider-ramp { - margin-top: var(--spectrum-slider-ramp-margin-top); - height: var(--spectrum-slider-ramp-track-height); + margin-block-start: var(--spectrum-slider-ramp-margin-top); + block-size: var(--spectrum-slider-ramp-track-height); position: absolute; - left: var(--spectrum-slider-track-margin-offset); - right: var(--spectrum-slider-track-margin-offset); - top: calc(var(--spectrum-slider-ramp-track-height) / 2); + inset-inline-start: var(--spectrum-slider-track-margin-offset); + inset-inline-end: var(--spectrum-slider-track-margin-offset); + inset-block-start: calc(var(--spectrum-slider-ramp-track-height) / 2); svg { - width: 100%; - height: 100%; + inline-size: 100%; + block-size: 100%; + + /* Flip the ramp automatically for RTL */ + transform: logical rotate(0deg); } } .spectrum-Slider-handle { position: absolute; - left: 0; - top: calc(var(--spectrum-slider-height) / 2); + inset-inline-start: 0; + inset-block-start: calc(var(--spectrum-slider-height) / 2); z-index: 2; display: inline-block; box-sizing: border-box; - width: var(--spectrum-slider-handle-width); - height: var(--spectrum-slider-handle-height); + inline-size: var(--spectrum-slider-handle-width); + block-size: var(--spectrum-slider-handle-height); - margin: var(--spectrum-slider-handle-margin-top) 0 0 calc(var(--spectrum-slider-handle-width) / -2); + margin-block: var(--spectrum-slider-handle-margin-top) 0; + margin-inline: calc(var(--spectrum-slider-handle-width) / -2) 0; border-width: var(--spectrum-slider-handle-border-size); border-style: solid; @@ -256,12 +274,12 @@ governing permissions and limitations under the License. /* Remove the margin for input in Firefox and Safari. */ margin: 0; - width: var(--spectrum-slider-handle-width); - height: var(--spectrum-slider-handle-height); + inline-size: var(--spectrum-slider-handle-width); + block-size: var(--spectrum-slider-handle-height); padding: 0; position: absolute; - top: var(--spectrum-slider-input-top); - left: var(--spectrum-slider-input-left); + inset-block-start: var(--spectrum-slider-input-top); + inset-inline-start: var(--spectrum-slider-input-left); overflow: hidden; opacity: .000001; cursor: default; @@ -278,9 +296,9 @@ governing permissions and limitations under the License. display: flex; position: relative; - width: auto; + inline-size: auto; - padding-top: var(--spectrum-fieldlabel-padding-top); + padding-block-start: var(--spectrum-fieldlabel-padding-top); font-size: var(--spectrum-text-size-text-label); line-height: var(--spectrum-line-height-text-label); @@ -288,19 +306,19 @@ governing permissions and limitations under the License. .spectrum-Slider-label, .spectrum-Dial-label { - padding-left: 0; + padding-inline-start: 0; flex-grow: 1; } .spectrum-Slider-value, .spectrum-Dial-value { flex-grow: 0; - padding-right: 0; + padding-inline-end: 0; cursor: default; } .spectrum-Slider-value { - margin-left: var(--spectrum-slider-label-gap-x); + margin-inline-start: var(--spectrum-slider-label-gap-x); } .spectrum-Slider-ticks { @@ -310,22 +328,22 @@ governing permissions and limitations under the License. z-index: 0; margin: 0 var(--spectrum-slider-track-margin-offset); - margin-top: calc(var(--spectrum-slider-tick-mark-height) + calc(var(--spectrum-slider-track-height) / 2)); + margin-block-start: calc(var(--spectrum-slider-tick-mark-height) + calc(var(--spectrum-slider-track-height) / 2)); } .spectrum-Slider-tick { position: relative; - width: var(--spectrum-slider-tick-mark-width); + inline-size: var(--spectrum-slider-tick-mark-width); &:after { display: block; position: absolute; - top: 0; - left: calc(50% - calc(var(--spectrum-slider-tick-mark-width) / 2)); + inset-block-start: 0; + inset-inline-start: calc(50% - calc(var(--spectrum-slider-tick-mark-width) / 2)); content: ''; - width: var(--spectrum-slider-tick-mark-width); - height: var(--spectrum-slider-tick-mark-height); + inline-size: var(--spectrum-slider-tick-mark-width); + block-size: var(--spectrum-slider-tick-mark-height); border-radius: var(--spectrum-slider-tick-mark-border-radius); } @@ -335,7 +353,8 @@ governing permissions and limitations under the License. align-items: center; justify-content: center; - margin: var(--spectrum-slider-label-gap-x) calc(var(--spectrum-slider-label-gap-x) * -1) 0 calc(var(--spectrum-slider-label-gap-x) * -1); + margin-block: var(--spectrum-slider-label-gap-x) 0; + margin-inline: calc(var(--spectrum-slider-label-gap-x) * -1) calc(var(--spectrum-slider-label-gap-x) * -1); font-size: var(--spectrum-text-size-text-label); line-height: var(--spectrum-line-height-text-label); @@ -346,34 +365,35 @@ governing permissions and limitations under the License. .spectrum-Slider-tickLabel { display: block; position: absolute; - margin: var(--spectrum-slider-label-gap-x) 0 0 0; + margin-block: var(--spectrum-slider-label-gap-x) 0; + margin-inline: 0; } } &:first-of-type { .spectrum-Slider-tickLabel { - left: 0; + inset-inline-start: 0; } } &:last-of-type { .spectrum-Slider-tickLabel { - right: 0; + inset-inline-end: 0; } } } .spectrum-Slider--color { .spectrum-Slider-labelContainer { - padding-bottom: var(--spectrum-fieldlabel-padding-bottom); + padding-block-end: var(--spectrum-fieldlabel-padding-bottom); } .spectrum-Slider-controls, .spectrum-Slider-controls::before, .spectrum-Slider-track { - min-height: var(--spectrum-slider-color-min-height); - height: var(--spectrum-slider-color-track-height); - margin-left: var(--spectrum-slider-color-track-margin-left); - width: 100%; + min-block-size: var(--spectrum-slider-color-min-height); + block-size: var(--spectrum-slider-color-track-height); + margin-inline-start: var(--spectrum-slider-color-track-margin-left); + inline-size: 100%; } .spectrum-Slider-controls::before { display: block; @@ -381,14 +401,14 @@ governing permissions and limitations under the License. } .spectrum-Slider-controls::before, .spectrum-Slider-track { - top: var(--spectrum-slider-color-track-top); + inset-block-start: var(--spectrum-slider-color-track-top); padding: var(--spectrum-slider-color-track-padding); - margin-top: var(--spectrum-slider-color-track-margin-top); - margin-right: var(--spectrum-slider-color-track-margin-right); + margin-block-start: var(--spectrum-slider-color-track-margin-top); + margin-inline-end: var(--spectrum-slider-color-track-margin-right); border-radius: var(--spectrum-alias-border-radius-regular); } .spectrum-Slider-handle { - top: var(--spectrum-slider-color-handle-top); + inset-block-start: var(--spectrum-slider-color-handle-top); } } @@ -397,15 +417,15 @@ governing permissions and limitations under the License. display: inline-flex; flex-direction: column; - height: auto; - min-width: var(--spectrum-dial-min-height); - min-height: var(--spectrum-dial-min-height); - width: var(--spectrum-dial-container-width); + block-size: auto; + min-inline-size: var(--spectrum-dial-min-height); + min-block-size: var(--spectrum-dial-min-height); + inline-size: var(--spectrum-dial-container-width); } .spectrum-Dial-labelContainer { @inherit: .spectrum-Slider-labelContainer; - margin-bottom: var(--spectrum-dial-label-gap-y); + margin-block-end: var(--spectrum-dial-label-gap-y); } .spectrum-Dial-label { @@ -417,9 +437,9 @@ governing permissions and limitations under the License. .spectrum-Dial-controls { @inherit: .spectrum-Slider-controls; - width: var(--spectrum-dial-width); - height: var(--spectrum-dial-width); - min-height: var(--spectrum-dial-controls-min-height); + inline-size: var(--spectrum-dial-width); + block-size: var(--spectrum-dial-width); + min-block-size: var(--spectrum-dial-controls-min-height); border-radius: var(--spectrum-dial-border-radius); position: relative; @@ -432,37 +452,37 @@ governing permissions and limitations under the License. &::before, &::after { content: ''; - width: calc(var(--spectrum-slider-tick-mark-width) * 2); - height: var(--spectrum-slider-tick-mark-width); + inline-size: calc(var(--spectrum-slider-tick-mark-width) * 2); + block-size: var(--spectrum-slider-tick-mark-width); border-radius: var(--spectrum-slider-tick-mark-border-radius); position: absolute; - bottom: 0; + inset-block-end: 0; } &::before { - left: auto; - right: calc(var(--spectrum-slider-tick-mark-width) * -1); + inset-inline-start: auto; + inset-inline-end: calc(var(--spectrum-slider-tick-mark-width) * -1); transform: rotate(var(--spectrum-dial-min-max-tick-angles)); } &::after { - left: calc(var(--spectrum-slider-tick-mark-width) * -1); - transform: rotate(calc(var(--spectrum-dial-min-max-tick-angles) * -1)); + inset-inline-start: calc(var(--spectrum-slider-tick-mark-width) * -1); + transform: rotate(calc(-1 * var(--spectrum-dial-min-max-tick-angles))); } } .spectrum-Dial-handle { @inherit: .spectrum-Slider-handle; - width: var(--spectrum-dial-handle-size); - height: var(--spectrum-dial-handle-size); + inline-size: var(--spectrum-dial-handle-size); + block-size: var(--spectrum-dial-handle-size); border-width: var(--spectrum-slider-handle-border-size); box-shadow: none; position: absolute; - top: var(--spectrum-dial-handle-position); - left: var(--spectrum-dial-handle-position); - right: var(--spectrum-dial-handle-position); - bottom: var(--spectrum-dial-handle-position); + inset-block-start: var(--spectrum-dial-handle-position); + inset-inline-start: var(--spectrum-dial-handle-position); + inset-inline-end: var(--spectrum-dial-handle-position); + inset-block-end: var(--spectrum-dial-handle-position); border-radius: var(--spectrum-dial-border-radius); - transform: rotate(calc(var(--spectrum-dial-min-max-tick-angles) * -1)); - cursor: default; + transform: rotate(calc(-1 * var(--spectrum-dial-min-max-tick-angles))); + cursor: pointer; cursor: grab; transition: background-color var(--spectrum-slider-animation-duration) ease-in-out; @@ -470,10 +490,10 @@ governing permissions and limitations under the License. &::after { content: ''; position: absolute; - top: 50%; - left: calc(var(--spectrum-slider-tick-mark-width) * -1); - width: var(--spectrum-dial-handle-marker-width); - height: var(--spectrum-dial-handle-marker-height); + inset-block-start: 50%; + inset-inline-start: calc(var(--spectrum-slider-tick-mark-width) * -1); + inline-size: var(--spectrum-dial-handle-marker-width); + block-size: var(--spectrum-dial-handle-marker-height); border-radius: var(--spectrum-dial-handle-marker-border-radius); transform: translateY(-50%); transition: background-color var(--spectrum-slider-animation-duration) ease-in-out; @@ -490,16 +510,16 @@ governing permissions and limitations under the License. .spectrum-Dial-input { @inherit: .spectrum-Slider-input; - width: var(--spectrum-dial-handle-size); - height: var(--spectrum-dial-handle-size); - left: 0; - top: 0; + inline-size: var(--spectrum-dial-handle-size); + block-size: var(--spectrum-dial-handle-size); + inset-inline-start: 0; + inset-block-start: 0; } .spectrum-Dial--small { .spectrum-Dial-controls { - width: var(--spectrum-dial-small-width); - height: var(--spectrum-dial-small-height); + inline-size: var(--spectrum-dial-small-width); + block-size: var(--spectrum-dial-small-height); } } diff --git a/packages/@adobe/spectrum-css-temp/components/slider/skin.css b/packages/@adobe/spectrum-css-temp/components/slider/skin.css index ef1d9805a84..6f4158afef1 100644 --- a/packages/@adobe/spectrum-css-temp/components/slider/skin.css +++ b/packages/@adobe/spectrum-css-temp/components/slider/skin.css @@ -128,8 +128,8 @@ governing permissions and limitations under the License. background-position: 0 0, 0 var(--spectrum-global-dimension-static-size-100), - var(--spectrum-global-dimension-static-size-100) calc(var(--spectrum-global-dimension-static-size-100) * -1), - calc(var(--spectrum-global-dimension-static-size-100) * -1) 0; + var(--spectrum-global-dimension-static-size-100) calc(-1 * var(--spectrum-global-dimension-static-size-100)), + calc(-1 * var(--spectrum-global-dimension-static-size-100)) 0; z-index: 0; } .spectrum-Slider-track { diff --git a/packages/@react-spectrum/slider/src/RangeSlider.tsx b/packages/@react-spectrum/slider/src/RangeSlider.tsx index 3dea1f50188..07ac5448db0 100644 --- a/packages/@react-spectrum/slider/src/RangeSlider.tsx +++ b/packages/@react-spectrum/slider/src/RangeSlider.tsx @@ -15,7 +15,7 @@ import {DEFAULT_MAX_VALUE, DEFAULT_MIN_VALUE} from '@react-stately/slider'; import {FocusRing} from '@react-aria/focus'; import {mergeProps} from '@react-aria/utils'; import React from 'react'; -import {SliderBase, toPercent, useSliderBase, UseSliderBaseInputProps} from './SliderBase'; +import {SliderBase, useSliderBase, UseSliderBaseInputProps} from './SliderBase'; import {SpectrumRangeSliderProps} from '@react-types/slider'; import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; import {useHover} from '@react-aria/interactions'; @@ -45,21 +45,21 @@ function RangeSlider(props: SpectrumRangeSliderProps) { let hovers = [useHover({}), useHover({})]; - let [leftSliderIndex, rightSliderIndex] = direction === 'ltr' ? [0, 1] : [1, 0]; + let cssDirection = direction === 'rtl' ? 'right' : 'left'; - let leftTrack = (
); + style={{width: `${state.getThumbPercent(0) * 100}%`}} />); let middleTrack = (
); - let rightTrack = (
); + let upperTrack = (
); + style={{[cssDirection]: `${state.getThumbPercent(1) * 100}%`, width: `${(1 - state.getThumbPercent(1)) * 100}%`}} />); let handles = [0, 1].map(i => (
@@ -69,7 +69,7 @@ function RangeSlider(props: SpectrumRangeSliderProps) { return ( - {leftTrack} + {lowerTrack} {ticks} {handles[0]} @@ -78,7 +78,7 @@ function RangeSlider(props: SpectrumRangeSliderProps) { {handles[1]} - {rightTrack} + {upperTrack} ); } diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx index 9b6a490da7a..b08dadda071 100644 --- a/packages/@react-spectrum/slider/src/Slider.tsx +++ b/packages/@react-spectrum/slider/src/Slider.tsx @@ -14,7 +14,7 @@ import {clamp, mergeProps} from '@react-aria/utils'; import {classNames} from '@react-spectrum/utils'; import {FocusRing} from '@react-aria/focus'; import React from 'react'; -import {SliderBase, toPercent, useSliderBase, UseSliderBaseInputProps} from './SliderBase'; +import {SliderBase, useSliderBase, UseSliderBaseInputProps} from './SliderBase'; import {SpectrumSliderProps} from '@react-types/slider'; import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; import {useHover} from '@react-aria/interactions'; @@ -40,10 +40,12 @@ function Slider(props: SpectrumSliderProps) { fillOffset = fillOffset != null ? clamp(fillOffset, state.getThumbMinValue(0), state.getThumbMaxValue(0)) : fillOffset; - let leftTrack = (
); - let rightTrack = (
@@ -75,12 +79,12 @@ function Slider(props: SpectrumSliderProps) { if (isFilled && fillOffset != null) { let width = state.getThumbPercent(0) - state.getValuePercent(fillOffset); let isRightOfOffset = width > 0; - let left = isRightOfOffset ? state.getValuePercent(fillOffset) : state.getThumbPercent(0); + let offset = isRightOfOffset ? state.getValuePercent(fillOffset) : state.getThumbPercent(0); filledTrack = (
); } @@ -95,12 +99,12 @@ function Slider(props: SpectrumSliderProps) { // @ts-ignore {'--spectrum-slider-track-color': trackBackground} }> - {leftTrack} + {lowerTrack} {ticks} {handle} - {rightTrack} + {upperTrack} {filledTrack} ); diff --git a/packages/@react-spectrum/slider/src/SliderBase.tsx b/packages/@react-spectrum/slider/src/SliderBase.tsx index 16709d8ef4d..015cda71cc4 100644 --- a/packages/@react-spectrum/slider/src/SliderBase.tsx +++ b/packages/@react-spectrum/slider/src/SliderBase.tsx @@ -20,16 +20,6 @@ import {useLocale} from '@react-aria/i18n'; import {useProviderProps} from '@react-spectrum/provider'; import {useSlider, useSliderThumb} from '@react-aria/slider'; -/** - * Convert a number 0-1 into a CSS percentage value, mirroring it for rtl. - */ -export function toPercent(value: number, direction: Direction = 'ltr'): string { - if (direction === 'rtl') { - value = 1 - value; - } - return `${value * 100}%`; -} - export interface UseSliderBaseContainerProps extends AriaLabelingProps, LabelableProps { state: SliderState, trackRef: MutableRefObject, From 3a257191d166ad0a460c74379d992f4d463b6cf9 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Wed, 23 Sep 2020 10:20:13 +0200 Subject: [PATCH 24/40] Try to make value label constant width --- .../components/slider/index.css | 2 ++ .../components/slider/skin.css | 4 +++ .../@react-spectrum/slider/src/SliderBase.tsx | 26 ++++++++++++++++--- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/@adobe/spectrum-css-temp/components/slider/index.css b/packages/@adobe/spectrum-css-temp/components/slider/index.css index a913fa67637..1b3f7717647 100644 --- a/packages/@adobe/spectrum-css-temp/components/slider/index.css +++ b/packages/@adobe/spectrum-css-temp/components/slider/index.css @@ -315,6 +315,8 @@ governing permissions and limitations under the License. flex-grow: 0; padding-inline-end: 0; cursor: default; + font-feature-settings: "tnum"; + text-align: end; } .spectrum-Slider-value { diff --git a/packages/@adobe/spectrum-css-temp/components/slider/skin.css b/packages/@adobe/spectrum-css-temp/components/slider/skin.css index 6f4158afef1..a7cce7a682b 100644 --- a/packages/@adobe/spectrum-css-temp/components/slider/skin.css +++ b/packages/@adobe/spectrum-css-temp/components/slider/skin.css @@ -318,4 +318,8 @@ governing permissions and limitations under the License. padding-top: 0; flex-shrink: 0; } + + & .spectrum-Slider-label { + margin-inline-end: var(--spectrum-slider-label-gap-x); + } } diff --git a/packages/@react-spectrum/slider/src/SliderBase.tsx b/packages/@react-spectrum/slider/src/SliderBase.tsx index 015cda71cc4..2fe0b046020 100644 --- a/packages/@react-spectrum/slider/src/SliderBase.tsx +++ b/packages/@react-spectrum/slider/src/SliderBase.tsx @@ -16,7 +16,7 @@ import React, {CSSProperties, HTMLAttributes, MutableRefObject, ReactNode, React import {SliderProps, SpectrumSliderTicksBase} from '@react-types/slider'; import {SliderState, useSliderState} from '@react-stately/slider'; import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; -import {useLocale} from '@react-aria/i18n'; +import {useLocale, useNumberFormatter} from '@react-aria/i18n'; import {useProviderProps} from '@react-spectrum/provider'; import {useSlider, useSliderThumb} from '@react-aria/slider'; @@ -31,7 +31,8 @@ export interface UseSliderBaseContainerProps extends AriaLabelingProps, Labelabl containerProps: HTMLAttributes, trackProps: HTMLAttributes, labelProps: HTMLAttributes, - direction: Direction + direction: Direction, + formatOptions?: Intl.NumberFormatOptions } export interface UseSliderBaseInputProps extends Omit, SpectrumSliderTicksBase { @@ -128,6 +129,7 @@ export function useSliderBase(count: number, props: UseSliderBaseInputProps): Us containerProps, trackProps, labelProps, direction, isDisabled, label: props.label, + formatOptions: props.formatOptions, showValueLabel: props.showValueLabel, labelPosition: props.labelPosition, orientation: props.orientation, @@ -154,11 +156,16 @@ function SliderBase(props: SliderBaseProps) { state, children, classes, style, trackRef, isDisabled, labelProps, containerProps, trackProps, - labelPosition = 'top', valueLabel, showValueLabel = !!props.label + labelPosition = 'top', valueLabel, showValueLabel = !!props.label, + formatOptions } = props; + let formatter = useNumberFormatter(formatOptions); + let displayValue = valueLabel; + let maxLabelLength = undefined; if (!displayValue) { + maxLabelLength = Math.max([...formatter.format(state.getThumbMinValue(0))].length, [...formatter.format(state.getThumbMaxValue(0))].length); switch (state.values.length) { case 1: displayValue = state.getThumbValueLabel(0); @@ -168,6 +175,9 @@ function SliderBase(props: SliderBaseProps) { // https://github.com/tc39/ecma402/issues/393 // https://github.com/tc39/proposal-intl-numberformat-v3#formatrange-ecma-402-393 displayValue = `${state.getThumbValueLabel(0)} - ${state.getThumbValueLabel(1)}`; + + // The `${start} ${separator} ${end}` label can be wrapped into multiple lines. + maxLabelLength = Math.max(maxLabelLength, [...formatter.format(state.getThumbMinValue(1))].length, [...formatter.format(state.getThumbMaxValue(1))].length); break; default: throw new Error('Only sliders with 1 or 2 handles are supported!'); @@ -175,7 +185,15 @@ function SliderBase(props: SliderBaseProps) { } let labelNode = ; - let valueNode =
{displayValue}
; + let valueNode = (
+ {displayValue} +
); return (
Date: Wed, 23 Sep 2020 10:27:18 +0200 Subject: [PATCH 25/40] Remove dragging cursor, active style --- .../components/slider/index.css | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/@adobe/spectrum-css-temp/components/slider/index.css b/packages/@adobe/spectrum-css-temp/components/slider/index.css index 1b3f7717647..6506cf5639c 100644 --- a/packages/@adobe/spectrum-css-temp/components/slider/index.css +++ b/packages/@adobe/spectrum-css-temp/components/slider/index.css @@ -225,15 +225,19 @@ governing permissions and limitations under the License. transition: border-width var(--spectrum-slider-animation-duration) ease-in-out; outline: none; - cursor: pointer; - cursor: grab; + /* cursor: pointer; */ + /* cursor: grab; */ &:active, - &.is-focused, &.is-dragged { - /*border-width: var(--spectrum-slider-handle-border-size-down);*/ - cursor: ns-resize; - cursor: grabbing; + border-width: var(--spectrum-slider-handle-border-size-down); + /* cursor: ns-resize; */ + /* cursor: grabbing; */ + } + + &:active, + $.is-dragged { + } &:active, From 951d1bfea7f8d5b41746ee5f5a6d070e8a64af0a Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Wed, 23 Sep 2020 10:48:39 +0200 Subject: [PATCH 26/40] Remove storybook: textAlign: 'left' --- .chromatic/config.js | 2 +- .storybook/config.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.chromatic/config.js b/.chromatic/config.js index 1315a38aad9..4e76b4ecf79 100644 --- a/.chromatic/config.js +++ b/.chromatic/config.js @@ -22,7 +22,7 @@ addParameters({ addDecorator(withA11y); addDecorator(story => ( - + {story()} )); diff --git a/.storybook/config.js b/.storybook/config.js index 1a40f7308f1..a1dd6b88ca6 100644 --- a/.storybook/config.js +++ b/.storybook/config.js @@ -22,7 +22,7 @@ addParameters({ addDecorator(withA11y); addDecorator(story => ( - + {story()} )); From 514a64f13e1902ca18bd649b5b2d939c04308bd1 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Wed, 23 Sep 2020 13:18:23 +0200 Subject: [PATCH 27/40] Polyfill for signDisplay :tada: --- .../i18n/src/useNumberFormatter.ts | 18 ++++++++- packages/@react-aria/i18n/src/utils.ts | 33 +++++++++++++++ .../numberFormatSignDisplayPolyfill.test.js | 40 +++++++++++++++++++ .../slider/stories/RangeSlider.stories.tsx | 5 --- .../slider/stories/Slider.stories.tsx | 13 ------ 5 files changed, 89 insertions(+), 20 deletions(-) create mode 100644 packages/@react-aria/i18n/test/numberFormatSignDisplayPolyfill.test.js diff --git a/packages/@react-aria/i18n/src/useNumberFormatter.ts b/packages/@react-aria/i18n/src/useNumberFormatter.ts index ea64ce6fcd7..dbe6d1ea994 100644 --- a/packages/@react-aria/i18n/src/useNumberFormatter.ts +++ b/packages/@react-aria/i18n/src/useNumberFormatter.ts @@ -10,10 +10,14 @@ * governing permissions and limitations under the License. */ +import {numberFormatSignDisplayPolyfill} from './utils'; import {useLocale} from './context'; let formatterCache = new Map(); +// @ts-ignore +const supportsSignDisplay = (new Intl.NumberFormat('de-DE', {signDisplay: 'exceptZero'})).resolvedOptions().signDisplay === 'exceptZero'; + /** * Provides localized number formatting for the current locale. Automatically updates when the locale changes, * and handles caching of the number formatter for performance. @@ -21,13 +25,23 @@ let formatterCache = new Map(); */ export function useNumberFormatter(options?: Intl.NumberFormatOptions): Intl.NumberFormat { let {locale} = useLocale(); - + let cacheKey = locale + (options ? Object.entries(options).sort((a, b) => a[0] < b[0] ? -1 : 1).join() : ''); if (formatterCache.has(cacheKey)) { return formatterCache.get(cacheKey); } let numberFormatter = new Intl.NumberFormat(locale, options); - formatterCache.set(cacheKey, numberFormatter); + // @ts-ignore + let {signDisplay} = options || {}; + formatterCache.set(cacheKey, (!supportsSignDisplay && signDisplay != null) ? new Proxy(numberFormatter, { + get(target, property) { + if (property === 'format') { + return (v) => numberFormatSignDisplayPolyfill(numberFormatter, signDisplay, v); + } else { + return target[property]; + } + } + }) : numberFormatter); return numberFormatter; } diff --git a/packages/@react-aria/i18n/src/utils.ts b/packages/@react-aria/i18n/src/utils.ts index 9a2e5b0285e..70cfcdfd72b 100644 --- a/packages/@react-aria/i18n/src/utils.ts +++ b/packages/@react-aria/i18n/src/utils.ts @@ -31,3 +31,36 @@ export function isRTL(locale: string) { let lang = locale.split('-')[0]; return RTL_LANGS.has(lang); } + +export function numberFormatSignDisplayPolyfill(numberFormat: Intl.NumberFormat, signDisplay: 'always' | 'exceptZero' | 'auto' | 'never', num: number) { + if (signDisplay === 'auto') { + return numberFormat.format(num); + } else if (signDisplay === 'never') { + return numberFormat.format(Math.abs(num)); + } else { + let needsPositiveSign = false; + if (signDisplay === 'always') { + needsPositiveSign = num > 0 || Object.is(num, 0); + } else if (signDisplay === 'exceptZero') { + if (Object.is(num, -0) || Object.is(num, 0)) { + num = Math.abs(num); + } else { + needsPositiveSign = num > 0; + } + } + + if (needsPositiveSign) { + let negative = numberFormat.format(-num); + let noSign = numberFormat.format(num); + // ignore RTL/LTR marker character + let minus = negative.replace(noSign, '').replace(/\u200e|\u061C/, ''); + if ([...minus].length !== 1) { + console.warn('@react-aria/i18n polyfill for NumberFormat signDisplay: Unsupported case'); + } + let positive = negative.replace(noSign, '!!!').replace(minus, '+').replace('!!!', noSign); + return positive; + } else { + return numberFormat.format(num); + } + } +} diff --git a/packages/@react-aria/i18n/test/numberFormatSignDisplayPolyfill.test.js b/packages/@react-aria/i18n/test/numberFormatSignDisplayPolyfill.test.js new file mode 100644 index 00000000000..be2b1ea5564 --- /dev/null +++ b/packages/@react-aria/i18n/test/numberFormatSignDisplayPolyfill.test.js @@ -0,0 +1,40 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {numberFormatSignDisplayPolyfill} from '../src/utils'; + +function verify(locale, options, signDisplay, v) { + let a = new Intl.NumberFormat(locale, options); + let b = new Intl.NumberFormat(locale, {...options, signDisplay}); + + expect(b.format(v)).toBe(numberFormatSignDisplayPolyfill(a, signDisplay, v)); +} + +let signDisplayValues = ['always', 'auto', 'never', 'exceptZero']; +let localeValues = ['de-DE', 'ar-AE', 'fa', 'he-IL']; +let optionsValues = [{}, {style: 'unit', unit: 'celsius'}, {style: 'currency', currency: 'USD', currencyDisplay: 'name'}]; +let numValues = [-123, -1, -0, 0, +0, 1, 123]; + +describe('numberFormatSignDisplayPolyfill', () => { + for (let signDisplay of signDisplayValues) { + for (let locale of localeValues) { + for (let options of optionsValues) { + for (let num of numValues) { + // eslint-disable-next-line no-nested-ternary + it(`${locale} - ${signDisplay} - ${JSON.stringify(options)} - ${Object.is(num, +0) ? '+0' : Object.is(num, -0) ? '-0' : num}`, () => { + verify(locale, options, signDisplay, num); + }); + } + } + } + } +}); diff --git a/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx b/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx index f0a03dcfd71..71ca62c1134 100644 --- a/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx +++ b/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx @@ -11,7 +11,6 @@ */ import {action} from '@storybook/addon-actions'; -import {Provider} from '@adobe/react-spectrum'; import {RangeSlider} from '../'; import React, {useState} from 'react'; import {SpectrumRangeSliderProps} from '@react-types/slider'; @@ -26,10 +25,6 @@ storiesOf('RangeSlider', module) 'label', () => render({label: 'Label'}) ) - .add( - 'rtl', - () => {render({label: 'فهو يتحدّث بلغة '})} - ) .add( 'disabled', () => render({label: 'Label', defaultValue: {start: 30, end: 50}, isDisabled: true}) diff --git a/packages/@react-spectrum/slider/stories/Slider.stories.tsx b/packages/@react-spectrum/slider/stories/Slider.stories.tsx index ce54883c284..d1eb6bf8397 100644 --- a/packages/@react-spectrum/slider/stories/Slider.stories.tsx +++ b/packages/@react-spectrum/slider/stories/Slider.stories.tsx @@ -11,7 +11,6 @@ */ import {action} from '@storybook/addon-actions'; -import {Provider} from '@adobe/react-spectrum'; import React, {useState} from 'react'; import {Slider} from '../'; import {SpectrumSliderProps} from '@react-types/slider'; @@ -26,10 +25,6 @@ storiesOf('Slider', module) 'label', () => render({label: 'Label'}) ) - .add( - 'rtl', - () => {render({label: 'فهو يتحدّث بلغة'})} - ) .add( 'disabled', () => render({label: 'Label', defaultValue: 50, isDisabled: true}) @@ -62,10 +57,6 @@ storiesOf('Slider', module) 'labelPosition: side', () => render({label: 'Label', labelPosition: 'side'}) ) - .add( - 'rtl labelPosition: side', - () => {render({label: 'فهو يتحدّث بلغة', labelPosition: 'side'})} - ) .add( 'min/max', () => render({label: 'Label', minValue: 30, maxValue: 70}) @@ -95,10 +86,6 @@ storiesOf('Slider', module) 'tickLabels', () => render({label: 'Label', tickCount: 3, showTickLabels: true, tickLabels: ['A', 'B', 'C']}) ) - .add( - 'rtl trackBackground', - () => {render({label: 'فهو يتحدّث بلغة ', trackBackground: 'linear-gradient(to right, blue, red)'})} - ) .add( 'trackBackground', () => render({label: 'Label', trackBackground: 'linear-gradient(to right, blue, red)'}) From 2dd62cb25365a960590cb64e2aa5ae3859b472e9 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Thu, 24 Sep 2020 14:52:02 +0200 Subject: [PATCH 28/40] Add is-dragged to handles for track clicking --- packages/@react-spectrum/slider/src/RangeSlider.tsx | 2 +- packages/@react-spectrum/slider/src/Slider.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@react-spectrum/slider/src/RangeSlider.tsx b/packages/@react-spectrum/slider/src/RangeSlider.tsx index 07ac5448db0..7e5748e1dee 100644 --- a/packages/@react-spectrum/slider/src/RangeSlider.tsx +++ b/packages/@react-spectrum/slider/src/RangeSlider.tsx @@ -58,7 +58,7 @@ function RangeSlider(props: SpectrumRangeSliderProps) { style={{[cssDirection]: `${state.getThumbPercent(1) * 100}%`, width: `${(1 - state.getThumbPercent(1)) * 100}%`}} />); let handles = [0, 1].map(i => (
diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx index b08dadda071..cd1a094a345 100644 --- a/packages/@react-spectrum/slider/src/Slider.tsx +++ b/packages/@react-spectrum/slider/src/Slider.tsx @@ -64,7 +64,7 @@ function Slider(props: SpectrumSliderProps) { }} />); let handle = (
Date: Thu, 24 Sep 2020 15:03:55 +0200 Subject: [PATCH 29/40] Switch to trackGradient, fix RTL --- .../@react-spectrum/slider/src/Slider.tsx | 10 ++++++---- .../slider/stories/Slider.stories.tsx | 8 ++++---- packages/@react-types/slider/src/index.d.ts | 19 +++++++++++++++---- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx index cd1a094a345..84db5e2667e 100644 --- a/packages/@react-spectrum/slider/src/Slider.tsx +++ b/packages/@react-spectrum/slider/src/Slider.tsx @@ -21,7 +21,7 @@ import {useHover} from '@react-aria/interactions'; import {VisuallyHidden} from '@adobe/react-spectrum'; function Slider(props: SpectrumSliderProps) { - let {onChange, value, defaultValue, isFilled, fillOffset, trackBackground, ...otherProps} = props; + let {onChange, value, defaultValue, isFilled, fillOffset, trackGradient, ...otherProps} = props; let ariaProps: UseSliderBaseInputProps = { ...otherProps, @@ -52,7 +52,8 @@ function Slider(props: SpectrumSliderProps) { // width: calc(var(--width) * 1%)M // } // @ts-ignore - '--spectrum-track-background-size': `${(1 / state.getThumbPercent(0)) * 100}%` + '--spectrum-track-background-size': `${(1 / state.getThumbPercent(0)) * 100}%`, + '--spectrum-track-background-position': direction === 'ltr' ? '0' : '100%' }} />); let upperTrack = (
); let handle = (
); } + return ( {lowerTrack} {ticks} diff --git a/packages/@react-spectrum/slider/stories/Slider.stories.tsx b/packages/@react-spectrum/slider/stories/Slider.stories.tsx index d1eb6bf8397..d30cbf2f3ac 100644 --- a/packages/@react-spectrum/slider/stories/Slider.stories.tsx +++ b/packages/@react-spectrum/slider/stories/Slider.stories.tsx @@ -87,12 +87,12 @@ storiesOf('Slider', module) () => render({label: 'Label', tickCount: 3, showTickLabels: true, tickLabels: ['A', 'B', 'C']}) ) .add( - 'trackBackground', - () => render({label: 'Label', trackBackground: 'linear-gradient(to right, blue, red)'}) + 'trackGradient', + () => render({label: 'Label', trackGradient: ['blue', 'red']}) ) .add( - 'trackBackground with fillOffset', - () => render({label: 'Label', trackBackground: 'linear-gradient(to right, blue, red)', isFilled: true, fillOffset: 50}) + 'trackGradient with fillOffset', + () => render({label: 'Label', trackGradient: ['blue', 'red'], isFilled: true, fillOffset: 50}) ) .add( '* orientation: vertical', diff --git a/packages/@react-types/slider/src/index.d.ts b/packages/@react-types/slider/src/index.d.ts index a805f2d300e..1c48993fe6e 100644 --- a/packages/@react-types/slider/src/index.d.ts +++ b/packages/@react-types/slider/src/index.d.ts @@ -42,12 +42,23 @@ export interface SpectrumSliderTicksBase { } export interface SpectrumSliderProps extends SpectrumBarSliderBase, SpectrumSliderTicksBase { - /** Whether a fill color is shown between the start of the slider and the current value. See https://spectrum.adobe.com/page/slider/#Fill. */ + /** + * Whether a fill color is shown between the start of the slider and the current value. + * @see https://spectrum.adobe.com/page/slider/#Fill. + */ isFilled?: boolean, - /** The offset from which to start the fill. See https://spectrum.adobe.com/page/slider/#Fill-start. */ + /** + * The offset from which to start the fill. + * @see https://spectrum.adobe.com/page/slider/#Fill-start. + */ fillOffset?: number, - /** The background of the track, e.g. a CSS linear-gradient(). See https://spectrum.adobe.com/page/slider/#Gradient. */ - trackBackground?: string + /** + * The background of the track, specified as the stops for a CSS background: `linear-gradient(to right/left, ...trackGradient)`. + * @example trackGradient={['red', 'green']} + * @example trackGradient={['red 20%', 'green 40%']} + * @see https://spectrum.adobe.com/page/slider/#Gradient. + */ + trackGradient?: string[] } export interface SpectrumRangeSliderProps extends SpectrumBarSliderBase>, SpectrumSliderTicksBase { } From 7957abcde2ec0e9ea35519ee656b04867ba5b89f Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Thu, 24 Sep 2020 16:04:56 +0200 Subject: [PATCH 30/40] Refactor: remove useSliderBase --- .../slider/src/RangeSlider.tsx | 81 ++++++----- .../@react-spectrum/slider/src/Slider.tsx | 127 ++++++++-------- .../@react-spectrum/slider/src/SliderBase.tsx | 136 +++++++----------- .../slider/stories/RangeSlider.stories.tsx | 20 ++- .../slider/stories/Slider.stories.tsx | 22 +-- packages/@react-types/slider/src/index.d.ts | 4 +- 6 files changed, 182 insertions(+), 208 deletions(-) diff --git a/packages/@react-spectrum/slider/src/RangeSlider.tsx b/packages/@react-spectrum/slider/src/RangeSlider.tsx index 7e5748e1dee..46f0ae732d3 100644 --- a/packages/@react-spectrum/slider/src/RangeSlider.tsx +++ b/packages/@react-spectrum/slider/src/RangeSlider.tsx @@ -15,17 +15,19 @@ import {DEFAULT_MAX_VALUE, DEFAULT_MIN_VALUE} from '@react-stately/slider'; import {FocusRing} from '@react-aria/focus'; import {mergeProps} from '@react-aria/utils'; import React from 'react'; -import {SliderBase, useSliderBase, UseSliderBaseInputProps} from './SliderBase'; +import {SliderBase, SliderBaseChildArguments, SliderBaseProps} from './SliderBase'; import {SpectrumRangeSliderProps} from '@react-types/slider'; import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; import {useHover} from '@react-aria/interactions'; +import {useLocale} from '@react-aria/i18n'; import {VisuallyHidden} from '@adobe/react-spectrum'; function RangeSlider(props: SpectrumRangeSliderProps) { let {onChange, value, defaultValue, ...otherProps} = props; - let ariaProps: UseSliderBaseInputProps = { + let baseProps: Omit = { ...otherProps, + count: 2, value: value != null ? [value.start, value.end] : undefined, defaultValue: defaultValue != null ? [defaultValue.start, defaultValue.end] : // make sure that useSliderState knows we have two handles @@ -35,50 +37,47 @@ function RangeSlider(props: SpectrumRangeSliderProps) { } }; - let { - inputRefs, - thumbProps, - inputProps, ticks, - ...containerProps - } = useSliderBase(2, ariaProps); - let {state, direction} = containerProps; - + let {direction} = useLocale(); let hovers = [useHover({}), useHover({})]; - let cssDirection = direction === 'rtl' ? 'right' : 'left'; + return ( + + {({state, thumbProps, inputRefs, inputProps, ticks}: SliderBaseChildArguments) => { + let cssDirection = direction === 'rtl' ? 'right' : 'left'; + + let lowerTrack = (
); + let middleTrack = (
); + let upperTrack = (
); - let lowerTrack = (
); - let middleTrack = (
); - let upperTrack = (
); + let handles = [0, 1].map(i => (
+ + + +
)); - let handles = [0, 1].map(i => (
- - - -
)); + return (<> + {lowerTrack} + {ticks} + + {handles[0]} + + {middleTrack} + + {handles[1]} + + {upperTrack}); + }} - return ( - - {lowerTrack} - {ticks} - - {handles[0]} - - {middleTrack} - - {handles[1]} - - {upperTrack} ); } diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx index 84db5e2667e..cfb0a527259 100644 --- a/packages/@react-spectrum/slider/src/Slider.tsx +++ b/packages/@react-spectrum/slider/src/Slider.tsx @@ -14,17 +14,19 @@ import {clamp, mergeProps} from '@react-aria/utils'; import {classNames} from '@react-spectrum/utils'; import {FocusRing} from '@react-aria/focus'; import React from 'react'; -import {SliderBase, useSliderBase, UseSliderBaseInputProps} from './SliderBase'; +import {SliderBase, SliderBaseChildArguments, SliderBaseProps} from './SliderBase'; import {SpectrumSliderProps} from '@react-types/slider'; import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; import {useHover} from '@react-aria/interactions'; +import {useLocale} from '@react-aria/i18n'; import {VisuallyHidden} from '@adobe/react-spectrum'; function Slider(props: SpectrumSliderProps) { let {onChange, value, defaultValue, isFilled, fillOffset, trackGradient, ...otherProps} = props; - let ariaProps: UseSliderBaseInputProps = { + let baseProps: Omit = { ...otherProps, + count: 1, // Normalize `value: number[]` to `value: number` value: value != null ? [value] : undefined, defaultValue: defaultValue != null ? [defaultValue] : undefined, @@ -33,67 +35,12 @@ function Slider(props: SpectrumSliderProps) { } }; + let {direction} = useLocale(); let {isHovered, hoverProps} = useHover({}); - let {inputRefs: [inputRef], thumbProps: [thumbProps], inputProps: [inputProps], ticks, ...containerProps} = useSliderBase(1, ariaProps); - let {state, direction} = containerProps; - - fillOffset = fillOffset != null ? clamp(fillOffset, state.getThumbMinValue(0), state.getThumbMaxValue(0)) : fillOffset; - - let cssDirection = direction === 'rtl' ? 'right' : 'left'; - - let lowerTrack = (
); - let upperTrack = (
); - - let handle = (
- - - -
); - - let filledTrack = null; - if (isFilled && fillOffset != null) { - let width = state.getThumbPercent(0) - state.getValuePercent(fillOffset); - let isRightOfOffset = width > 0; - let offset = isRightOfOffset ? state.getValuePercent(fillOffset) : state.getThumbPercent(0); - filledTrack = - (
); - } - - return ( - {lowerTrack} - {ticks} - - {handle} - - {upperTrack} - {filledTrack} + {({inputRefs: [inputRef], thumbProps: [thumbProps], inputProps: [inputProps], ticks, state}: SliderBaseChildArguments) => { + fillOffset = fillOffset != null ? clamp(fillOffset, state.getThumbMinValue(0), state.getThumbMaxValue(0)) : fillOffset; + + let cssDirection = direction === 'rtl' ? 'right' : 'left'; + + let lowerTrack = (
); + let upperTrack = (
); + + let handle = (
+ + + +
); + + let filledTrack = null; + if (isFilled && fillOffset != null) { + let width = state.getThumbPercent(0) - state.getValuePercent(fillOffset); + let isRightOfOffset = width > 0; + let offset = isRightOfOffset ? state.getValuePercent(fillOffset) : state.getThumbPercent(0); + filledTrack = (
); + } + return (<>{lowerTrack}{ticks} + {handle} + {upperTrack}{filledTrack}); + }} ); } diff --git a/packages/@react-spectrum/slider/src/SliderBase.tsx b/packages/@react-spectrum/slider/src/SliderBase.tsx index 2fe0b046020..d71ad7a5096 100644 --- a/packages/@react-spectrum/slider/src/SliderBase.tsx +++ b/packages/@react-spectrum/slider/src/SliderBase.tsx @@ -10,73 +10,67 @@ * governing permissions and limitations under the License. */ -import {AriaLabelingProps, Direction, LabelableProps, LabelPosition, Orientation} from '@react-types/shared'; -import {classNames} from '@react-spectrum/utils'; -import React, {CSSProperties, HTMLAttributes, MutableRefObject, ReactNode, ReactNodeArray, useRef} from 'react'; -import {SliderProps, SpectrumSliderTicksBase} from '@react-types/slider'; +import {BaseSliderProps, SpectrumSliderTicksBase} from '@react-types/slider'; +import {classNames, useStyleProps} from '@react-spectrum/utils'; +import {LabelPosition, Orientation, StyleProps, ValueBase} from '@react-types/shared'; +import React, {CSSProperties, HTMLAttributes, MutableRefObject, ReactNode, useRef} from 'react'; import {SliderState, useSliderState} from '@react-stately/slider'; import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; import {useLocale, useNumberFormatter} from '@react-aria/i18n'; import {useProviderProps} from '@react-spectrum/provider'; import {useSlider, useSliderThumb} from '@react-aria/slider'; -export interface UseSliderBaseContainerProps extends AriaLabelingProps, LabelableProps { - state: SliderState, - trackRef: MutableRefObject, - isDisabled?: boolean, - orientation?: Orientation, - labelPosition?: LabelPosition, - showValueLabel?: boolean, - valueLabel?: ReactNode, - containerProps: HTMLAttributes, - trackProps: HTMLAttributes, - labelProps: HTMLAttributes, - direction: Direction, - formatOptions?: Intl.NumberFormatOptions +export interface SliderBaseChildArguments { + inputRefs: MutableRefObject[], + thumbProps: HTMLAttributes[], + inputProps: HTMLAttributes[], + ticks: ReactNode, + state: SliderState } -export interface UseSliderBaseInputProps extends Omit, SpectrumSliderTicksBase { +export interface SliderBaseProps extends BaseSliderProps, ValueBase, SpectrumSliderTicksBase, StyleProps { + children: (SliderBaseChildArguments) => ReactNode, + formatOptions?: Intl.NumberFormatOptions, + classes?: string[] | Object, + style?: CSSProperties, + count: 1 | 2, + // SpectrumBarSliderBase: orientation?: Orientation, labelPosition?: LabelPosition, - showValueLabel?: boolean, - valueLabel?: ReactNode -} - -export interface UseSliderBaseOutputProps extends UseSliderBaseContainerProps { - inputRefs: MutableRefObject[], - thumbProps: HTMLAttributes[], - inputProps: HTMLAttributes[], - ticks: ReactNode + valueLabel?: ReactNode, + showValueLabel?: boolean } -/** Count mustn't change during the lifetime! */ -export function useSliderBase(count: number, props: UseSliderBaseInputProps): UseSliderBaseOutputProps { +function SliderBase(props: SliderBaseProps) { props = useProviderProps(props); - let inputRefs = []; - let thumbProps = []; - let inputProps = []; + let { + tickCount, showTickLabels, tickLabels, isDisabled, count, + children, classes, style, + labelPosition = 'top', valueLabel, showValueLabel = !!props.label, + formatOptions, + ...otherProps + } = props; + + let {styleProps} = useStyleProps(otherProps); + let {direction} = useLocale(); // Assumes that DEFAULT_MIN_VALUE and DEFAULT_MAX_VALUE are both positive, this value needs to be passed to useSliderState, so // getThumbMinValue/getThumbMaxValue cannot be used here. // `Math.abs(Math.sign(a) - Math.sign(b)) === 2` is true if the values have a different sign and neither is null. let alwaysDisplaySign = props.minValue != null && props.maxValue != null && Math.abs(Math.sign(props.minValue) - Math.sign(props.maxValue)) === 2; - if (alwaysDisplaySign) { - if (props.formatOptions != null) { - if (!('signDisplay' in props.formatOptions)) { + if (formatOptions != null) { + if (!('signDisplay' in formatOptions)) { // @ts-ignore - props.formatOptions.signDisplay = 'exceptZero'; + formatOptions.signDisplay = 'exceptZero'; } } else { // @ts-ignore - props.formatOptions = {signDisplay: 'exceptZero'}; + formatOptions = {signDisplay: 'exceptZero'}; } } - let state = useSliderState(props); - - let {direction} = useLocale(); - + let state = useSliderState({...props, formatOptions}); let trackRef = useRef(); let { containerProps, @@ -84,6 +78,9 @@ export function useSliderBase(count: number, props: UseSliderBaseInputProps): Us labelProps } = useSlider({...props, direction}, state, trackRef); + let inputRefs = []; + let thumbProps = []; + let inputProps = []; for (let i = 0; i < count; i++) { // eslint-disable-next-line react-hooks/rules-of-hooks inputRefs[i] = useRef(); @@ -101,8 +98,6 @@ export function useSliderBase(count: number, props: UseSliderBaseInputProps): Us thumbProps[i] = v.thumbProps; } - let {tickCount, showTickLabels, tickLabels, isDisabled} = props; - let ticks = null; if (tickCount > 0) { let tickList = []; @@ -123,43 +118,6 @@ export function useSliderBase(count: number, props: UseSliderBaseInputProps): Us
); } - return { - ticks, - inputRefs, thumbProps, inputProps, trackRef, state, - containerProps, trackProps, labelProps, direction, - isDisabled, - label: props.label, - formatOptions: props.formatOptions, - showValueLabel: props.showValueLabel, - labelPosition: props.labelPosition, - orientation: props.orientation, - valueLabel: props.valueLabel, - 'aria-label': props['aria-label'], - 'aria-labelledby': props['aria-labelledby'], - 'aria-describedby': props['aria-describedby'], - 'aria-details': props['aria-details'] - }; -} - -export interface SliderBaseProps extends UseSliderBaseContainerProps, LabelableProps, AriaLabelingProps { - children: ReactNodeArray, - orientation?: Orientation, - labelPosition?: LabelPosition, - valueLabel?: ReactNode, - formatOptions?: Intl.NumberFormatOptions, - classes?: string[] | Object, - style?: CSSProperties -} - -function SliderBase(props: SliderBaseProps) { - let { - state, children, classes, style, - trackRef, isDisabled, - labelProps, containerProps, trackProps, - labelPosition = 'top', valueLabel, showValueLabel = !!props.label, - formatOptions - } = props; - let formatter = useNumberFormatter(formatOptions); let displayValue = valueLabel; @@ -176,7 +134,7 @@ function SliderBase(props: SliderBaseProps) { // https://github.com/tc39/proposal-intl-numberformat-v3#formatrange-ecma-402-393 displayValue = `${state.getThumbValueLabel(0)} - ${state.getThumbValueLabel(1)}`; - // The `${start} ${separator} ${end}` label can be wrapped into multiple lines. + // The `${start} ${separator} ${end}` label can be wrapped into multiple lines, no need to make it twice as wide. maxLabelLength = Math.max(maxLabelLength, [...formatter.format(state.getThumbMinValue(1))].length, [...formatter.format(state.getThumbMaxValue(1))].length); break; default: @@ -203,8 +161,12 @@ function SliderBase(props: SliderBaseProps) { 'spectrum-Slider--label-side': labelPosition === 'side', 'is-disabled': isDisabled }, - classes)} - style={style} + classes, + styleProps.className)} + style={{ + ...style, + ...styleProps.style + }} {...containerProps}> {(props.label) &&
@@ -213,7 +175,13 @@ function SliderBase(props: SliderBaseProps) {
}
- {children} + {children({ + inputRefs, + thumbProps, + inputProps, + ticks, + state + })}
{labelPosition === 'side' &&
diff --git a/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx b/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx index 71ca62c1134..1071cb4b3e4 100644 --- a/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx +++ b/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx @@ -26,12 +26,20 @@ storiesOf('RangeSlider', module) () => render({label: 'Label'}) ) .add( - 'disabled', - () => render({label: 'Label', defaultValue: {start: 30, end: 50}, isDisabled: true}) + 'isDisabled', + () => render({label: 'Label', defaultValue: {start: 30, end: 70}, isDisabled: true}) + ) + .add( + 'isReadOnly', + () => render({label: 'Label', defaultValue: {start: 30, end: 70}, isReadOnly: true}) + ) + .add( + 'custom width', + () => render({label: 'Label', maxValue: 1000, width: '200px'}) ) .add( 'label overflow', - () => render({label: 'This is a rather long label for this narrow slider element.', maxValue: 1000}, '100px') + () => render({label: 'This is a rather long label for this narrow slider element.', maxValue: 1000, width: '100px'}) ) .add( 'showValueLabel: false', @@ -74,13 +82,11 @@ storiesOf('RangeSlider', module) () => render({label: 'Label', tickCount: 3, showTickLabels: true, tickLabels: ['A', 'B', 'C']}) ); -function render(props: SpectrumRangeSliderProps = {}, width = '200px') { +function render(props: SpectrumRangeSliderProps = {}) { if (props.onChange == null) { props.onChange = (v) => { action('change')(v.start, v.end); }; } - return (
- -
); + return ; } diff --git a/packages/@react-spectrum/slider/stories/Slider.stories.tsx b/packages/@react-spectrum/slider/stories/Slider.stories.tsx index d30cbf2f3ac..e102d664419 100644 --- a/packages/@react-spectrum/slider/stories/Slider.stories.tsx +++ b/packages/@react-spectrum/slider/stories/Slider.stories.tsx @@ -26,12 +26,20 @@ storiesOf('Slider', module) () => render({label: 'Label'}) ) .add( - 'disabled', + 'isDisabled', () => render({label: 'Label', defaultValue: 50, isDisabled: true}) ) + .add( + 'isReadOnly', + () => render({label: 'Label', defaultValue: 50, isReadOnly: true}) + ) + .add( + 'custom width', + () => render({label: 'Label', width: '200px'}) + ) .add( 'label overflow', - () => render({label: 'This is a rather long label for this narrow slider element.', maxValue: 1000}, '100px') + () => render({label: 'This is a rather long label for this narrow slider element.', maxValue: 1000, width: '100px'}) ) .add( 'showValueLabel: false', @@ -79,8 +87,8 @@ storiesOf('Slider', module) ) .add( 'showTickLabels, custom formatOptions', - // @ts-ignore TODO why is "unit" even missing? How well is it supported? - () => render({label: 'Label', tickCount: 5, showTickLabels: true, minValue: -10, maxValue: 10, formatOptions: {style: 'unit', unit: 'centimeter'}}) + // @ts-ignore + () => render({label: 'Label', tickCount: 5, showTickLabels: true, minValue: -10, maxValue: 10, width: '200px', formatOptions: {style: 'unit', unit: 'centimeter'}}) ) .add( 'tickLabels', @@ -99,11 +107,9 @@ storiesOf('Slider', module) () => render({label: 'Label', orientation: 'vertical'}) ); -function render(props: SpectrumSliderProps = {}, width = '200px') { +function render(props: SpectrumSliderProps = {}) { if (props.onChange == null) { props.onChange = action('change'); } - return (
- -
); + return ; } diff --git a/packages/@react-types/slider/src/index.d.ts b/packages/@react-types/slider/src/index.d.ts index 1c48993fe6e..840f45f4f6a 100644 --- a/packages/@react-types/slider/src/index.d.ts +++ b/packages/@react-types/slider/src/index.d.ts @@ -1,4 +1,4 @@ -import {AriaLabelingProps, AriaValidationProps, Direction, FocusableDOMProps, FocusableProps, LabelableProps, LabelPosition, Orientation, RangeInputBase, RangeValue, Validation, ValueBase} from '@react-types/shared'; +import {AriaLabelingProps, AriaValidationProps, Direction, FocusableDOMProps, FocusableProps, LabelableProps, LabelPosition, Orientation, RangeInputBase, RangeValue, StyleProps, Validation, ValueBase} from '@react-types/shared'; import {ReactNode} from 'react'; export interface BaseSliderProps extends RangeInputBase, LabelableProps, AriaLabelingProps { @@ -19,7 +19,7 @@ export interface SliderThumbProps extends AriaLabelingProps, FocusableDOMProps, direction?: Direction } -export interface SpectrumBarSliderBase extends BaseSliderProps, ValueBase { +export interface SpectrumBarSliderBase extends BaseSliderProps, ValueBase, StyleProps { orientation?: Orientation, labelPosition?: LabelPosition, /** Whether the value's label is displayed. True by default if there's a `label`, false by default if not. */ From 9cfe118efad7f7cc431e147be85d4a56af2a91af Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Thu, 24 Sep 2020 16:15:27 +0200 Subject: [PATCH 31/40] Fix side label style --- .../@adobe/spectrum-css-temp/components/slider/index.css | 5 +++-- packages/@react-spectrum/slider/src/SliderBase.tsx | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/@adobe/spectrum-css-temp/components/slider/index.css b/packages/@adobe/spectrum-css-temp/components/slider/index.css index 6506cf5639c..83c0134c6e3 100644 --- a/packages/@adobe/spectrum-css-temp/components/slider/index.css +++ b/packages/@adobe/spectrum-css-temp/components/slider/index.css @@ -53,8 +53,6 @@ governing permissions and limitations under the License. /* Don't let z-index'd child elements float above other things on the page */ z-index: 1; display: block; - min-block-size: var(--spectrum-slider-height); - min-inline-size: var(--spectrum-slider-min-width); user-select: none; } @@ -66,6 +64,9 @@ governing permissions and limitations under the License. position: relative; z-index: auto; + min-block-size: var(--spectrum-slider-height); + min-inline-size: var(--spectrum-slider-min-width); + /* These calculations prevent the track from spilling outside of the control */ inline-size: calc(100% - calc(var(--spectrum-slider-controls-margin) * 2)); margin-inline-start: var(--spectrum-slider-controls-margin); diff --git a/packages/@react-spectrum/slider/src/SliderBase.tsx b/packages/@react-spectrum/slider/src/SliderBase.tsx index d71ad7a5096..f396d154ede 100644 --- a/packages/@react-spectrum/slider/src/SliderBase.tsx +++ b/packages/@react-spectrum/slider/src/SliderBase.tsx @@ -134,8 +134,10 @@ function SliderBase(props: SliderBaseProps) { // https://github.com/tc39/proposal-intl-numberformat-v3#formatrange-ecma-402-393 displayValue = `${state.getThumbValueLabel(0)} - ${state.getThumbValueLabel(1)}`; - // The `${start} ${separator} ${end}` label can be wrapped into multiple lines, no need to make it twice as wide. - maxLabelLength = Math.max(maxLabelLength, [...formatter.format(state.getThumbMinValue(1))].length, [...formatter.format(state.getThumbMaxValue(1))].length); + maxLabelLength = 2 + 2 * Math.max( + maxLabelLength, + [...formatter.format(state.getThumbMinValue(1))].length, [...formatter.format(state.getThumbMaxValue(1))].length + ); break; default: throw new Error('Only sliders with 1 or 2 handles are supported!'); @@ -149,7 +151,7 @@ function SliderBase(props: SliderBaseProps) { role="textbox" aria-readonly="true" aria-labelledby={labelProps.id} - style={{minWidth: maxLabelLength && `${maxLabelLength}ch`}}> + style={{width: maxLabelLength && `${maxLabelLength}ch`}}> {displayValue}
); From 40456dda284cd38077273eda170a7eda20afe9af Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Thu, 24 Sep 2020 16:25:00 +0200 Subject: [PATCH 32/40] Add missing test for readonly/disabled --- .../@react-aria/slider/src/useSliderThumb.ts | 6 +- .../slider/test/RangeSlider.test.tsx | 200 +++++++++++++++++- .../slider/test/Slider.test.tsx | 83 ++++++-- 3 files changed, 265 insertions(+), 24 deletions(-) diff --git a/packages/@react-aria/slider/src/useSliderThumb.ts b/packages/@react-aria/slider/src/useSliderThumb.ts index fbefcca151d..48872d2bedb 100644 --- a/packages/@react-aria/slider/src/useSliderThumb.ts +++ b/packages/@react-aria/slider/src/useSliderThumb.ts @@ -114,13 +114,13 @@ export function useSliderThumb( state.setThumbValue(index, parseFloat(e.target.value)); } }), - thumbProps: isEditable ? mergeProps({ + thumbProps: mergeProps(isEditable ? { onMouseDown: draggableProps.onMouseDown, onMouseEnter: draggableProps.onMouseEnter, onMouseOut: draggableProps.onMouseOut - }, { + } : {}, isDisabled ? {} : { onMouseDown: focusInput - }) : {}, + }), labelProps }; } diff --git a/packages/@react-spectrum/slider/test/RangeSlider.test.tsx b/packages/@react-spectrum/slider/test/RangeSlider.test.tsx index 26edc49b0d8..04735535fca 100644 --- a/packages/@react-spectrum/slider/test/RangeSlider.test.tsx +++ b/packages/@react-spectrum/slider/test/RangeSlider.test.tsx @@ -213,14 +213,18 @@ describe('Slider', function () { // Can't test arrow/page up/down arrows because they are handled by the browser and JSDOM doesn't feel like it. it.each` - Name | props | commands - ${'(left/right arrows, ltr)'} | ${{locale: 'de-DE'}} | ${[{left: press.ArrowRight, result: +1}, {left: press.ArrowLeft, result: -1}, {right: press.ArrowRight, result: +1}, {right: press.ArrowLeft, result: -1}]} - ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${[{left: press.ArrowRight, result: -1}, {left: press.ArrowLeft, result: +1}, {right: press.ArrowRight, result: -1}, {right: press.ArrowLeft, result: +1}]} - ${'(home/end, ltr)'} | ${{locale: 'de-DE'}} | ${[{left: press.End, result: '50'}, {left: press.Home, result: '0'}, {left: press.ArrowRight, result: '1'}, {right: press.Home, result: '1'}, {right: press.End, result: '100'}]} - ${'(home/end, rtl)'} | ${{locale: 'ar-AE'}} | ${[{left: press.End, result: '50'}, {left: press.Home, result: '0'}, {left: press.ArrowLeft, result: '1'}, {right: press.Home, result: '1'}, {right: press.End, result: '100'}]} + Name | props | commands + ${'(left/right arrows, ltr)'} | ${{locale: 'de-DE'}} | ${[{left: press.ArrowRight, result: +1}, {left: press.ArrowLeft, result: -1}, {right: press.ArrowRight, result: +1}, {right: press.ArrowLeft, result: -1}]} + ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${[{left: press.ArrowRight, result: -1}, {left: press.ArrowLeft, result: +1}, {right: press.ArrowRight, result: -1}, {right: press.ArrowLeft, result: +1}]} + ${'(home/end, ltr)'} | ${{locale: 'de-DE'}} | ${[{left: press.End, result: '50'}, {left: press.Home, result: '0'}, {left: press.ArrowRight, result: '1'}, {right: press.Home, result: '1'}, {right: press.End, result: '100'}]} + ${'(home/end, rtl)'} | ${{locale: 'ar-AE'}} | ${[{left: press.End, result: '50'}, {left: press.Home, result: '0'}, {left: press.ArrowLeft, result: '1'}, {right: press.Home, result: '1'}, {right: press.End, result: '100'}]} + ${'(left/right arrows, isDisabled)'} | ${{locale: 'de-DE', isDisabled: true}}| ${[{left: press.ArrowRight, result: 0}, {left: press.ArrowLeft, result: 0}, {right: press.ArrowRight, result: 0}, {right: press.ArrowLeft, result: 0}]} + ${'(home/end, isDisabled)'} | ${{locale: 'de-DE', isDisabled: true}}| ${[{left: press.End, result: 0}, {left: press.Home, result: 0}, {right: press.End, result: 0}, {right: press.Home, result: 0}]} + ${'(left/right arrows, isReadOnly)'} | ${{locale: 'de-DE', isReadOnly: true}}| ${[{left: press.ArrowRight, result: 0}, {left: press.ArrowLeft, result: 0}, {right: press.ArrowRight, result: 0}, {right: press.ArrowLeft, result: 0}]} + ${'(home/end, isReadOnly)'} | ${{locale: 'de-DE', isReadOnly: true}}| ${[{left: press.End, result: 0}, {left: press.Home, result: 0}, {right: press.End, result: 0}, {right: press.Home, result: 0}]} `('$Name moves the slider in the correct direction', function ({props, commands}) { let tree = render( - + ); @@ -234,7 +238,7 @@ describe('Slider', function () { ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${[{left: press.ArrowRight, result: -10}, {left: press.ArrowLeft, result: +10}, {right: press.ArrowRight, result: -10}, {right: press.ArrowLeft, result: +10}]} `('$Name respects the step size', function ({props, commands}) { let tree = render( - + ); @@ -248,7 +252,7 @@ describe('Slider', function () { ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${[{left: press.ArrowRight, result: -1}, {left: press.ArrowRight, result: 0}, {right: press.ArrowLeft, result: +1}, {right: press.ArrowLeft, result: 0}]} `('$Name is clamped by min/max', function ({props, commands}) { let tree = render( - + ); @@ -311,6 +315,86 @@ describe('Slider', function () { expect(onChangeSpy).toHaveBeenCalledTimes(3); }); + it('cannot click and drag handle when disabled', () => { + let onChangeSpy = jest.fn(); + let {getAllByRole} = render( + + ); + + let [sliderLeft, sliderRight] = getAllByRole('slider'); + let [thumbLeft, thumbRight] = [sliderLeft.parentElement.parentElement, sliderRight.parentElement.parentElement]; + + fireEvent.mouseDown(thumbLeft, {clientX: 20}); + expect(onChangeSpy).not.toHaveBeenCalled(); + expect(document.activeElement).not.toBe(sliderLeft); + fireEvent.mouseMove(thumbLeft, {clientX: 10}); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseMove(thumbLeft, {clientX: -10}); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseMove(thumbLeft, {clientX: 120}); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseUp(thumbLeft, {clientX: 120}); + expect(onChangeSpy).not.toHaveBeenCalled(); + + onChangeSpy.mockClear(); + + fireEvent.mouseDown(thumbRight, {clientX: 50}); + expect(onChangeSpy).not.toHaveBeenCalled(); + expect(document.activeElement).not.toBe(sliderRight); + fireEvent.mouseMove(thumbRight, {clientX: 60}); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseMove(thumbRight, {clientX: -10}); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseMove(thumbRight, {clientX: 120}); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseUp(thumbRight, {clientX: 120}); + expect(onChangeSpy).not.toHaveBeenCalled(); + }); + + it('cannot click and drag handle when read only', () => { + let onChangeSpy = jest.fn(); + let {getAllByRole} = render( + + ); + + let [sliderLeft, sliderRight] = getAllByRole('slider'); + let [thumbLeft, thumbRight] = [sliderLeft.parentElement.parentElement, sliderRight.parentElement.parentElement]; + + fireEvent.mouseDown(thumbLeft, {clientX: 20}); + expect(onChangeSpy).not.toHaveBeenCalled(); + expect(document.activeElement).toBe(sliderLeft); + fireEvent.mouseMove(thumbLeft, {clientX: 10}); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseMove(thumbLeft, {clientX: -10}); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseMove(thumbLeft, {clientX: 120}); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseUp(thumbLeft, {clientX: 120}); + expect(onChangeSpy).not.toHaveBeenCalled(); + + onChangeSpy.mockClear(); + + fireEvent.mouseDown(thumbRight, {clientX: 50}); + expect(onChangeSpy).not.toHaveBeenCalled(); + expect(document.activeElement).toBe(sliderRight); + fireEvent.mouseMove(thumbRight, {clientX: 60}); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseMove(thumbRight, {clientX: -10}); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseMove(thumbRight, {clientX: 120}); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseUp(thumbRight, {clientX: 120}); + expect(onChangeSpy).not.toHaveBeenCalled(); + }); + it('can click on track to move nearest handle', () => { let onChangeSpy = jest.fn(); let {getAllByRole} = render( @@ -361,5 +445,105 @@ describe('Slider', function () { fireEvent.mouseUp(thumbRight, {clientX: 90}); expect(onChangeSpy).toHaveBeenCalledTimes(1); }); + + it('cannot click on track to move nearest handle when disabled', () => { + let onChangeSpy = jest.fn(); + let {getAllByRole} = render( + + ); + + let [sliderLeft, sliderRight] = getAllByRole('slider'); + let [thumbLeft, thumbRight] = [sliderLeft.parentElement.parentElement, sliderRight.parentElement.parentElement]; + + // @ts-ignore + let [leftTrack, middleTrack, rightTrack] = [...thumbLeft.parentElement.children].filter(c => c !== thumbLeft && c !== thumbRight); + + // left track + fireEvent.mouseDown(leftTrack, {clientX: 20}); + expect(document.activeElement).not.toBe(sliderLeft); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseUp(thumbLeft, {clientX: 20}); + expect(onChangeSpy).not.toHaveBeenCalled(); + + // middle track, near left slider + onChangeSpy.mockClear(); + fireEvent.mouseDown(middleTrack, {clientX: 40}); + expect(document.activeElement).not.toBe(sliderLeft); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseUp(thumbLeft, {clientX: 40}); + expect(onChangeSpy).not.toHaveBeenCalled(); + + // middle track, near right slider + onChangeSpy.mockClear(); + fireEvent.mouseDown(middleTrack, {clientX: 60}); + expect(document.activeElement).not.toBe(sliderRight); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseUp(thumbRight, {clientX: 60}); + expect(onChangeSpy).not.toHaveBeenCalled(); + + // right track + onChangeSpy.mockClear(); + fireEvent.mouseDown(rightTrack, {clientX: 90}); + expect(document.activeElement).not.toBe(sliderRight); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseUp(thumbRight, {clientX: 90}); + expect(onChangeSpy).not.toHaveBeenCalled(); + }); + + it.only('cannot click on track to move nearest handle when read only', () => { + let onChangeSpy = jest.fn(); + let {getAllByRole} = render( + + ); + + let [sliderLeft, sliderRight] = getAllByRole('slider'); + let [thumbLeft, thumbRight] = [sliderLeft.parentElement.parentElement, sliderRight.parentElement.parentElement]; + + // @ts-ignore + let [leftTrack, middleTrack, rightTrack] = [...thumbLeft.parentElement.children].filter(c => c !== thumbLeft && c !== thumbRight); + + // left track + fireEvent.mouseDown(leftTrack, {clientX: 20}); + // TODO what should happen here? + // expect(document.activeElement).not.toBe(sliderLeft); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseUp(thumbLeft, {clientX: 20}); + expect(onChangeSpy).not.toHaveBeenCalled(); + + // middle track, near left slider + onChangeSpy.mockClear(); + fireEvent.mouseDown(middleTrack, {clientX: 40}); + // TODO what should happen here? + // expect(document.activeElement).not.toBe(sliderLeft); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseUp(thumbLeft, {clientX: 40}); + expect(onChangeSpy).not.toHaveBeenCalled(); + + // middle track, near right slider + onChangeSpy.mockClear(); + fireEvent.mouseDown(middleTrack, {clientX: 60}); + // TODO what should happen here? + // expect(document.activeElement).not.toBe(sliderRight); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseUp(thumbRight, {clientX: 60}); + expect(onChangeSpy).not.toHaveBeenCalled(); + + // right track + onChangeSpy.mockClear(); + fireEvent.mouseDown(rightTrack, {clientX: 90}); + // TODO what should happen here? + // expect(document.activeElement).not.toBe(sliderRight); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseUp(thumbRight, {clientX: 90}); + expect(onChangeSpy).not.toHaveBeenCalled(); + }); }); }); diff --git a/packages/@react-spectrum/slider/test/Slider.test.tsx b/packages/@react-spectrum/slider/test/Slider.test.tsx index 82656bf22ba..bc50a4e6827 100644 --- a/packages/@react-spectrum/slider/test/Slider.test.tsx +++ b/packages/@react-spectrum/slider/test/Slider.test.tsx @@ -194,13 +194,15 @@ describe('Slider', function () { // Can't test arrow/page up/down arrows because they are handled by the browser and JSDOM doesn't feel like it. it.each` - Name | props | commands - ${'(left/right arrows, ltr)'} | ${{locale: 'de-DE'}} | ${[{left: press.ArrowRight, result: +1}, {left: press.ArrowLeft, result: -1}]} - ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${[{left: press.ArrowRight, result: -1}, {left: press.ArrowLeft, result: +1}]} - ${'(home/end, ltr)'} | ${{locale: 'de-DE'}} | ${[{left: press.End, result: '100'}, {left: press.Home, result: '0'}]} - ${'(home/end, rtl)'} | ${{locale: 'ar-AE'}} | ${[{left: press.End, result: '100'}, {left: press.Home, result: '0'}]} - ${'(left/right arrows, disabled)'} | ${{locale: 'de-DE', isDisabled: true}}| ${[{left: press.ArrowRight, result: 0}, {left: press.ArrowLeft, result: 0}]} - ${'(home/end, disabled)'} | ${{locale: 'de-DE', isDisabled: true}}| ${[{left: press.End, result: 0}, {left: press.Home, result: 0}]} + Name | props | commands + ${'(left/right arrows, ltr)'} | ${{locale: 'de-DE'}} | ${[{left: press.ArrowRight, result: +1}, {left: press.ArrowLeft, result: -1}]} + ${'(left/right arrows, rtl)'} | ${{locale: 'ar-AE'}} | ${[{left: press.ArrowRight, result: -1}, {left: press.ArrowLeft, result: +1}]} + ${'(home/end, ltr)'} | ${{locale: 'de-DE'}} | ${[{left: press.End, result: '100'}, {left: press.Home, result: '0'}]} + ${'(home/end, rtl)'} | ${{locale: 'ar-AE'}} | ${[{left: press.End, result: '100'}, {left: press.Home, result: '0'}]} + ${'(left/right arrows, isDisabled)'} | ${{locale: 'de-DE', isDisabled: true}}| ${[{left: press.ArrowRight, result: 0}, {left: press.ArrowLeft, result: 0}]} + ${'(home/end, isDisabled)'} | ${{locale: 'de-DE', isDisabled: true}}| ${[{left: press.End, result: 0}, {left: press.Home, result: 0}]} + ${'(left/right arrows, isReadOnly)'} | ${{locale: 'de-DE', isReadOnly: true}}| ${[{left: press.ArrowRight, result: 0}, {left: press.ArrowLeft, result: 0}]} + ${'(home/end, isReadOnly)'} | ${{locale: 'de-DE', isReadOnly: true}}| ${[{left: press.End, result: 0}, {left: press.Home, result: 0}]} `('$Name moves the slider in the correct direction', function ({props, commands}) { let tree = render( @@ -293,9 +295,31 @@ describe('Slider', function () { expect(onChangeSpy).not.toHaveBeenCalled(); expect(document.activeElement).not.toBe(slider); fireEvent.mouseMove(thumb, {clientX: 10}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseUp(thumb, {clientX: 10}); + expect(onChangeSpy).not.toHaveBeenCalled(); + }); + + it('cannot click and drag handle when read only', () => { + let onChangeSpy = jest.fn(); + let {getByRole} = render( + + ); + + let slider = getByRole('slider'); + let thumb = slider.parentElement; + fireEvent.mouseDown(thumb, {clientX: 50}); + expect(onChangeSpy).not.toHaveBeenCalled(); + // TODO what should happen here? + // expect(document.activeElement).toBe(slider); + fireEvent.mouseMove(thumb, {clientX: 10}); + expect(onChangeSpy).not.toHaveBeenCalled(); fireEvent.mouseUp(thumb, {clientX: 10}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(onChangeSpy).not.toHaveBeenCalled(); }); it('can click on track to move handle', () => { @@ -348,17 +372,50 @@ describe('Slider', function () { // left track fireEvent.mouseDown(leftTrack, {clientX: 20}); expect(document.activeElement).not.toBe(slider); - expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(onChangeSpy).not.toHaveBeenCalled(); fireEvent.mouseUp(thumb, {clientX: 20}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(onChangeSpy).not.toHaveBeenCalled(); // right track onChangeSpy.mockClear(); fireEvent.mouseDown(rightTrack, {clientX: 70}); expect(document.activeElement).not.toBe(slider); - expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(onChangeSpy).not.toHaveBeenCalled(); fireEvent.mouseUp(thumb, {clientX: 70}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(onChangeSpy).not.toHaveBeenCalled(); + }); + + it('cannot click on track to move handle when read only', () => { + let onChangeSpy = jest.fn(); + let {getByRole} = render( + + ); + + let slider = getByRole('slider'); + let thumb = slider.parentElement.parentElement; + // @ts-ignore + let [leftTrack, rightTrack] = [...thumb.parentElement.children].filter(c => c !== thumb); + + // left track + fireEvent.mouseDown(leftTrack, {clientX: 20}); + // TODO what should happen here? + // expect(document.activeElement).not.toBe(slider); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseUp(thumb, {clientX: 20}); + expect(onChangeSpy).not.toHaveBeenCalled(); + + // right track + onChangeSpy.mockClear(); + fireEvent.mouseDown(rightTrack, {clientX: 70}); + // TODO what should happen here? + // expect(document.activeElement).not.toBe(slider); + expect(onChangeSpy).not.toHaveBeenCalled(); + fireEvent.mouseUp(thumb, {clientX: 70}); + expect(onChangeSpy).not.toHaveBeenCalled(); }); }); }); From 7cad811b5937a2bb95c8aec0cf4c523b9ae6c05e Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Thu, 24 Sep 2020 18:51:35 +0200 Subject: [PATCH 33/40] Remove isReadOnly from Spectrum Slider --- .../@react-spectrum/slider/src/SliderBase.tsx | 1 - .../slider/stories/RangeSlider.stories.tsx | 6 +- .../slider/stories/Slider.stories.tsx | 4 - .../slider/test/RangeSlider.test.tsx | 94 ------------------- .../slider/test/Slider.test.tsx | 57 ----------- packages/@react-types/slider/src/index.d.ts | 2 +- 6 files changed, 2 insertions(+), 162 deletions(-) diff --git a/packages/@react-spectrum/slider/src/SliderBase.tsx b/packages/@react-spectrum/slider/src/SliderBase.tsx index f396d154ede..f32fa0a642a 100644 --- a/packages/@react-spectrum/slider/src/SliderBase.tsx +++ b/packages/@react-spectrum/slider/src/SliderBase.tsx @@ -87,7 +87,6 @@ function SliderBase(props: SliderBaseProps) { // eslint-disable-next-line react-hooks/rules-of-hooks let v = useSliderThumb({ index: i, - isReadOnly: props.isReadOnly, isDisabled: props.isDisabled, trackRef, inputRef: inputRefs[i], diff --git a/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx b/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx index 1071cb4b3e4..151260e0686 100644 --- a/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx +++ b/packages/@react-spectrum/slider/stories/RangeSlider.stories.tsx @@ -29,13 +29,9 @@ storiesOf('RangeSlider', module) 'isDisabled', () => render({label: 'Label', defaultValue: {start: 30, end: 70}, isDisabled: true}) ) - .add( - 'isReadOnly', - () => render({label: 'Label', defaultValue: {start: 30, end: 70}, isReadOnly: true}) - ) .add( 'custom width', - () => render({label: 'Label', maxValue: 1000, width: '200px'}) + () => render({label: 'Label', width: '200px'}) ) .add( 'label overflow', diff --git a/packages/@react-spectrum/slider/stories/Slider.stories.tsx b/packages/@react-spectrum/slider/stories/Slider.stories.tsx index e102d664419..c24a011cfc8 100644 --- a/packages/@react-spectrum/slider/stories/Slider.stories.tsx +++ b/packages/@react-spectrum/slider/stories/Slider.stories.tsx @@ -29,10 +29,6 @@ storiesOf('Slider', module) 'isDisabled', () => render({label: 'Label', defaultValue: 50, isDisabled: true}) ) - .add( - 'isReadOnly', - () => render({label: 'Label', defaultValue: 50, isReadOnly: true}) - ) .add( 'custom width', () => render({label: 'Label', width: '200px'}) diff --git a/packages/@react-spectrum/slider/test/RangeSlider.test.tsx b/packages/@react-spectrum/slider/test/RangeSlider.test.tsx index 04735535fca..b2c068dc6bf 100644 --- a/packages/@react-spectrum/slider/test/RangeSlider.test.tsx +++ b/packages/@react-spectrum/slider/test/RangeSlider.test.tsx @@ -220,8 +220,6 @@ describe('Slider', function () { ${'(home/end, rtl)'} | ${{locale: 'ar-AE'}} | ${[{left: press.End, result: '50'}, {left: press.Home, result: '0'}, {left: press.ArrowLeft, result: '1'}, {right: press.Home, result: '1'}, {right: press.End, result: '100'}]} ${'(left/right arrows, isDisabled)'} | ${{locale: 'de-DE', isDisabled: true}}| ${[{left: press.ArrowRight, result: 0}, {left: press.ArrowLeft, result: 0}, {right: press.ArrowRight, result: 0}, {right: press.ArrowLeft, result: 0}]} ${'(home/end, isDisabled)'} | ${{locale: 'de-DE', isDisabled: true}}| ${[{left: press.End, result: 0}, {left: press.Home, result: 0}, {right: press.End, result: 0}, {right: press.Home, result: 0}]} - ${'(left/right arrows, isReadOnly)'} | ${{locale: 'de-DE', isReadOnly: true}}| ${[{left: press.ArrowRight, result: 0}, {left: press.ArrowLeft, result: 0}, {right: press.ArrowRight, result: 0}, {right: press.ArrowLeft, result: 0}]} - ${'(home/end, isReadOnly)'} | ${{locale: 'de-DE', isReadOnly: true}}| ${[{left: press.End, result: 0}, {left: press.Home, result: 0}, {right: press.End, result: 0}, {right: press.Home, result: 0}]} `('$Name moves the slider in the correct direction', function ({props, commands}) { let tree = render( @@ -355,46 +353,6 @@ describe('Slider', function () { expect(onChangeSpy).not.toHaveBeenCalled(); }); - it('cannot click and drag handle when read only', () => { - let onChangeSpy = jest.fn(); - let {getAllByRole} = render( - - ); - - let [sliderLeft, sliderRight] = getAllByRole('slider'); - let [thumbLeft, thumbRight] = [sliderLeft.parentElement.parentElement, sliderRight.parentElement.parentElement]; - - fireEvent.mouseDown(thumbLeft, {clientX: 20}); - expect(onChangeSpy).not.toHaveBeenCalled(); - expect(document.activeElement).toBe(sliderLeft); - fireEvent.mouseMove(thumbLeft, {clientX: 10}); - expect(onChangeSpy).not.toHaveBeenCalled(); - fireEvent.mouseMove(thumbLeft, {clientX: -10}); - expect(onChangeSpy).not.toHaveBeenCalled(); - fireEvent.mouseMove(thumbLeft, {clientX: 120}); - expect(onChangeSpy).not.toHaveBeenCalled(); - fireEvent.mouseUp(thumbLeft, {clientX: 120}); - expect(onChangeSpy).not.toHaveBeenCalled(); - - onChangeSpy.mockClear(); - - fireEvent.mouseDown(thumbRight, {clientX: 50}); - expect(onChangeSpy).not.toHaveBeenCalled(); - expect(document.activeElement).toBe(sliderRight); - fireEvent.mouseMove(thumbRight, {clientX: 60}); - expect(onChangeSpy).not.toHaveBeenCalled(); - fireEvent.mouseMove(thumbRight, {clientX: -10}); - expect(onChangeSpy).not.toHaveBeenCalled(); - fireEvent.mouseMove(thumbRight, {clientX: 120}); - expect(onChangeSpy).not.toHaveBeenCalled(); - fireEvent.mouseUp(thumbRight, {clientX: 120}); - expect(onChangeSpy).not.toHaveBeenCalled(); - }); - it('can click on track to move nearest handle', () => { let onChangeSpy = jest.fn(); let {getAllByRole} = render( @@ -493,57 +451,5 @@ describe('Slider', function () { fireEvent.mouseUp(thumbRight, {clientX: 90}); expect(onChangeSpy).not.toHaveBeenCalled(); }); - - it.only('cannot click on track to move nearest handle when read only', () => { - let onChangeSpy = jest.fn(); - let {getAllByRole} = render( - - ); - - let [sliderLeft, sliderRight] = getAllByRole('slider'); - let [thumbLeft, thumbRight] = [sliderLeft.parentElement.parentElement, sliderRight.parentElement.parentElement]; - - // @ts-ignore - let [leftTrack, middleTrack, rightTrack] = [...thumbLeft.parentElement.children].filter(c => c !== thumbLeft && c !== thumbRight); - - // left track - fireEvent.mouseDown(leftTrack, {clientX: 20}); - // TODO what should happen here? - // expect(document.activeElement).not.toBe(sliderLeft); - expect(onChangeSpy).not.toHaveBeenCalled(); - fireEvent.mouseUp(thumbLeft, {clientX: 20}); - expect(onChangeSpy).not.toHaveBeenCalled(); - - // middle track, near left slider - onChangeSpy.mockClear(); - fireEvent.mouseDown(middleTrack, {clientX: 40}); - // TODO what should happen here? - // expect(document.activeElement).not.toBe(sliderLeft); - expect(onChangeSpy).not.toHaveBeenCalled(); - fireEvent.mouseUp(thumbLeft, {clientX: 40}); - expect(onChangeSpy).not.toHaveBeenCalled(); - - // middle track, near right slider - onChangeSpy.mockClear(); - fireEvent.mouseDown(middleTrack, {clientX: 60}); - // TODO what should happen here? - // expect(document.activeElement).not.toBe(sliderRight); - expect(onChangeSpy).not.toHaveBeenCalled(); - fireEvent.mouseUp(thumbRight, {clientX: 60}); - expect(onChangeSpy).not.toHaveBeenCalled(); - - // right track - onChangeSpy.mockClear(); - fireEvent.mouseDown(rightTrack, {clientX: 90}); - // TODO what should happen here? - // expect(document.activeElement).not.toBe(sliderRight); - expect(onChangeSpy).not.toHaveBeenCalled(); - fireEvent.mouseUp(thumbRight, {clientX: 90}); - expect(onChangeSpy).not.toHaveBeenCalled(); - }); }); }); diff --git a/packages/@react-spectrum/slider/test/Slider.test.tsx b/packages/@react-spectrum/slider/test/Slider.test.tsx index bc50a4e6827..130ba7f9780 100644 --- a/packages/@react-spectrum/slider/test/Slider.test.tsx +++ b/packages/@react-spectrum/slider/test/Slider.test.tsx @@ -201,8 +201,6 @@ describe('Slider', function () { ${'(home/end, rtl)'} | ${{locale: 'ar-AE'}} | ${[{left: press.End, result: '100'}, {left: press.Home, result: '0'}]} ${'(left/right arrows, isDisabled)'} | ${{locale: 'de-DE', isDisabled: true}}| ${[{left: press.ArrowRight, result: 0}, {left: press.ArrowLeft, result: 0}]} ${'(home/end, isDisabled)'} | ${{locale: 'de-DE', isDisabled: true}}| ${[{left: press.End, result: 0}, {left: press.Home, result: 0}]} - ${'(left/right arrows, isReadOnly)'} | ${{locale: 'de-DE', isReadOnly: true}}| ${[{left: press.ArrowRight, result: 0}, {left: press.ArrowLeft, result: 0}]} - ${'(home/end, isReadOnly)'} | ${{locale: 'de-DE', isReadOnly: true}}| ${[{left: press.End, result: 0}, {left: press.Home, result: 0}]} `('$Name moves the slider in the correct direction', function ({props, commands}) { let tree = render( @@ -300,28 +298,6 @@ describe('Slider', function () { expect(onChangeSpy).not.toHaveBeenCalled(); }); - it('cannot click and drag handle when read only', () => { - let onChangeSpy = jest.fn(); - let {getByRole} = render( - - ); - - let slider = getByRole('slider'); - let thumb = slider.parentElement; - fireEvent.mouseDown(thumb, {clientX: 50}); - expect(onChangeSpy).not.toHaveBeenCalled(); - // TODO what should happen here? - // expect(document.activeElement).toBe(slider); - fireEvent.mouseMove(thumb, {clientX: 10}); - expect(onChangeSpy).not.toHaveBeenCalled(); - fireEvent.mouseUp(thumb, {clientX: 10}); - expect(onChangeSpy).not.toHaveBeenCalled(); - }); - it('can click on track to move handle', () => { let onChangeSpy = jest.fn(); let {getByRole} = render( @@ -384,38 +360,5 @@ describe('Slider', function () { fireEvent.mouseUp(thumb, {clientX: 70}); expect(onChangeSpy).not.toHaveBeenCalled(); }); - - it('cannot click on track to move handle when read only', () => { - let onChangeSpy = jest.fn(); - let {getByRole} = render( - - ); - - let slider = getByRole('slider'); - let thumb = slider.parentElement.parentElement; - // @ts-ignore - let [leftTrack, rightTrack] = [...thumb.parentElement.children].filter(c => c !== thumb); - - // left track - fireEvent.mouseDown(leftTrack, {clientX: 20}); - // TODO what should happen here? - // expect(document.activeElement).not.toBe(slider); - expect(onChangeSpy).not.toHaveBeenCalled(); - fireEvent.mouseUp(thumb, {clientX: 20}); - expect(onChangeSpy).not.toHaveBeenCalled(); - - // right track - onChangeSpy.mockClear(); - fireEvent.mouseDown(rightTrack, {clientX: 70}); - // TODO what should happen here? - // expect(document.activeElement).not.toBe(slider); - expect(onChangeSpy).not.toHaveBeenCalled(); - fireEvent.mouseUp(thumb, {clientX: 70}); - expect(onChangeSpy).not.toHaveBeenCalled(); - }); }); }); diff --git a/packages/@react-types/slider/src/index.d.ts b/packages/@react-types/slider/src/index.d.ts index 840f45f4f6a..d293214e40e 100644 --- a/packages/@react-types/slider/src/index.d.ts +++ b/packages/@react-types/slider/src/index.d.ts @@ -19,7 +19,7 @@ export interface SliderThumbProps extends AriaLabelingProps, FocusableDOMProps, direction?: Direction } -export interface SpectrumBarSliderBase extends BaseSliderProps, ValueBase, StyleProps { +export interface SpectrumBarSliderBase extends Omit, ValueBase, StyleProps { orientation?: Orientation, labelPosition?: LabelPosition, /** Whether the value's label is displayed. True by default if there's a `label`, false by default if not. */ From af0c6247d0fa201be514e94cef8f1321ea4c03f4 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Thu, 24 Sep 2020 21:49:57 +0200 Subject: [PATCH 34/40] Fix constant width value label again --- packages/@react-spectrum/slider/src/SliderBase.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-spectrum/slider/src/SliderBase.tsx b/packages/@react-spectrum/slider/src/SliderBase.tsx index f32fa0a642a..d52f6bb5015 100644 --- a/packages/@react-spectrum/slider/src/SliderBase.tsx +++ b/packages/@react-spectrum/slider/src/SliderBase.tsx @@ -150,7 +150,7 @@ function SliderBase(props: SliderBaseProps) { role="textbox" aria-readonly="true" aria-labelledby={labelProps.id} - style={{width: maxLabelLength && `${maxLabelLength}ch`}}> + style={maxLabelLength && {width: `${maxLabelLength}ch`, minWidth: `${maxLabelLength}ch`}}> {displayValue}
); From 33d4452d3a1e3d35135ed48c45fe38b096018b84 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Mon, 28 Sep 2020 10:33:51 +0200 Subject: [PATCH 35/40] Fixup RTL track clicking --- packages/@react-aria/slider/src/useSlider.ts | 6 +++--- packages/@react-aria/splitview/package.json | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/@react-aria/slider/src/useSlider.ts b/packages/@react-aria/slider/src/useSlider.ts index 355af2e2840..4920e6c9bea 100644 --- a/packages/@react-aria/slider/src/useSlider.ts +++ b/packages/@react-aria/slider/src/useSlider.ts @@ -114,11 +114,11 @@ export function useSlider( const trackPosition = trackRef.current.getBoundingClientRect().left; const clickPosition = e.clientX; const offset = clickPosition - trackPosition; - const percent = offset / trackRef.current.offsetWidth; - let value = state.getPercentValue(percent); + let percent = offset / trackRef.current.offsetWidth; if (direction === 'rtl') { - value = 100 - value; + percent = 1 - percent; } + const value = state.getPercentValue(percent); // Only compute the diff for thumbs that are editable, as only they can be dragged const minDiff = Math.min(...state.values.map((v, index) => state.isThumbEditable(index) ? Math.abs(v - value) : Number.POSITIVE_INFINITY)); diff --git a/packages/@react-aria/splitview/package.json b/packages/@react-aria/splitview/package.json index 3aaf71e05ef..c7a089d2a02 100644 --- a/packages/@react-aria/splitview/package.json +++ b/packages/@react-aria/splitview/package.json @@ -19,9 +19,10 @@ }, "dependencies": { "@babel/runtime": "^7.6.2", + "@react-aria/interactions": "3.2.0", "@react-aria/utils": "^3.1.0", - "@react-types/shared": "^3.1.0", - "@react-stately/splitview": "^3.0.0-alpha.1" + "@react-stately/splitview": "^3.0.0-alpha.1", + "@react-types/shared": "^3.1.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1" From 0cb7d9a6f8ac075496514399754ca55dd8a7843a Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Thu, 1 Oct 2020 22:46:49 +0200 Subject: [PATCH 36/40] Update version --- packages/@react-aria/splitview/package.json | 2 +- packages/@react-spectrum/slider/package.json | 12 ++++++------ packages/@react-spectrum/slider/src/RangeSlider.tsx | 2 +- packages/@react-spectrum/slider/src/Slider.tsx | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/@react-aria/splitview/package.json b/packages/@react-aria/splitview/package.json index c7a089d2a02..6cabe7fdb72 100644 --- a/packages/@react-aria/splitview/package.json +++ b/packages/@react-aria/splitview/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@babel/runtime": "^7.6.2", - "@react-aria/interactions": "3.2.0", + "@react-aria/interactions": "^3.2.0", "@react-aria/utils": "^3.1.0", "@react-stately/splitview": "^3.0.0-alpha.1", "@react-types/shared": "^3.1.0" diff --git a/packages/@react-spectrum/slider/package.json b/packages/@react-spectrum/slider/package.json index 3f2a871347c..cf1bdfca0d0 100644 --- a/packages/@react-spectrum/slider/package.json +++ b/packages/@react-spectrum/slider/package.json @@ -32,17 +32,17 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@adobe/react-spectrum": "3.3.0", "@babel/runtime": "^7.6.2", - "@react-aria/focus": "3.2.1", - "@react-aria/i18n": "3.1.1", - "@react-aria/interactions": "3.2.0", + "@react-aria/focus": "^3.2.1", + "@react-aria/i18n": "^3.1.1", + "@react-aria/interactions": "^3.2.1", "@react-aria/slider": "^3.0.0-alpha.1", "@react-aria/utils": "^3.0.0-alpha.1", + "@react-aria/visually-hidden": "3.2.1", "@react-spectrum/utils": "^3.0.0-alpha.1", "@react-stately/slider": "^3.0.0-alpha.1", - "@react-types/shared": "3.2.0", - "@react-types/slider": "3.0.0-alpha.0" + "@react-types/shared": "^3.2.0", + "@react-types/slider": "^3.0.0-alpha.0" }, "devDependencies": { "@adobe/spectrum-css-temp": "^3.0.0-alpha.1" diff --git a/packages/@react-spectrum/slider/src/RangeSlider.tsx b/packages/@react-spectrum/slider/src/RangeSlider.tsx index 46f0ae732d3..39c38150d22 100644 --- a/packages/@react-spectrum/slider/src/RangeSlider.tsx +++ b/packages/@react-spectrum/slider/src/RangeSlider.tsx @@ -20,7 +20,7 @@ import {SpectrumRangeSliderProps} from '@react-types/slider'; import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; import {useHover} from '@react-aria/interactions'; import {useLocale} from '@react-aria/i18n'; -import {VisuallyHidden} from '@adobe/react-spectrum'; +import {VisuallyHidden} from '@react-aria/visually-hidden'; function RangeSlider(props: SpectrumRangeSliderProps) { let {onChange, value, defaultValue, ...otherProps} = props; diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx index cfb0a527259..d5c1816ac27 100644 --- a/packages/@react-spectrum/slider/src/Slider.tsx +++ b/packages/@react-spectrum/slider/src/Slider.tsx @@ -19,7 +19,7 @@ import {SpectrumSliderProps} from '@react-types/slider'; import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; import {useHover} from '@react-aria/interactions'; import {useLocale} from '@react-aria/i18n'; -import {VisuallyHidden} from '@adobe/react-spectrum'; +import {VisuallyHidden} from '@react-aria/visually-hidden'; function Slider(props: SpectrumSliderProps) { let {onChange, value, defaultValue, isFilled, fillOffset, trackGradient, ...otherProps} = props; From 681776145693c37b1d59832a9f0a13a30b06616e Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Fri, 2 Oct 2020 10:27:22 +0200 Subject: [PATCH 37/40] Cleanup --- .../slider/src/RangeSlider.tsx | 2 +- .../@react-spectrum/slider/src/Slider.tsx | 2 +- .../@react-spectrum/slider/src/SliderBase.tsx | 20 ++++++++----------- .../slider/test/RangeSlider.test.tsx | 2 +- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/@react-spectrum/slider/src/RangeSlider.tsx b/packages/@react-spectrum/slider/src/RangeSlider.tsx index 39c38150d22..043f01d0c80 100644 --- a/packages/@react-spectrum/slider/src/RangeSlider.tsx +++ b/packages/@react-spectrum/slider/src/RangeSlider.tsx @@ -60,7 +60,7 @@ function RangeSlider(props: SpectrumRangeSliderProps) { style={{[cssDirection]: `${state.getThumbPercent(i) * 100}%`}} {...mergeProps(thumbProps[i], hovers[i].hoverProps)} role="presentation"> - +
)); diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx index d5c1816ac27..8fdbca5847b 100644 --- a/packages/@react-spectrum/slider/src/Slider.tsx +++ b/packages/@react-spectrum/slider/src/Slider.tsx @@ -82,7 +82,7 @@ function Slider(props: SpectrumSliderProps) { }} {...mergeProps(thumbProps, hoverProps)} role="presentation"> - +
); diff --git a/packages/@react-spectrum/slider/src/SliderBase.tsx b/packages/@react-spectrum/slider/src/SliderBase.tsx index d52f6bb5015..8de986ee4bb 100644 --- a/packages/@react-spectrum/slider/src/SliderBase.tsx +++ b/packages/@react-spectrum/slider/src/SliderBase.tsx @@ -10,11 +10,10 @@ * governing permissions and limitations under the License. */ -import {BaseSliderProps, SpectrumSliderTicksBase} from '@react-types/slider'; import {classNames, useStyleProps} from '@react-spectrum/utils'; -import {LabelPosition, Orientation, StyleProps, ValueBase} from '@react-types/shared'; import React, {CSSProperties, HTMLAttributes, MutableRefObject, ReactNode, useRef} from 'react'; import {SliderState, useSliderState} from '@react-stately/slider'; +import {SpectrumBarSliderBase, SpectrumSliderTicksBase} from '@react-types/slider'; import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; import {useLocale, useNumberFormatter} from '@react-aria/i18n'; import {useProviderProps} from '@react-spectrum/provider'; @@ -28,17 +27,11 @@ export interface SliderBaseChildArguments { state: SliderState } -export interface SliderBaseProps extends BaseSliderProps, ValueBase, SpectrumSliderTicksBase, StyleProps { +export interface SliderBaseProps extends SpectrumBarSliderBase, SpectrumSliderTicksBase { children: (SliderBaseChildArguments) => ReactNode, - formatOptions?: Intl.NumberFormatOptions, classes?: string[] | Object, style?: CSSProperties, - count: 1 | 2, - // SpectrumBarSliderBase: - orientation?: Orientation, - labelPosition?: LabelPosition, - valueLabel?: ReactNode, - showValueLabel?: boolean + count: 1 | 2 } function SliderBase(props: SliderBaseProps) { @@ -61,8 +54,11 @@ function SliderBase(props: SliderBaseProps) { if (alwaysDisplaySign) { if (formatOptions != null) { if (!('signDisplay' in formatOptions)) { - // @ts-ignore - formatOptions.signDisplay = 'exceptZero'; + formatOptions = { + ...formatOptions, + // @ts-ignore + signDisplay: 'exceptZero' + }; } } else { // @ts-ignore diff --git a/packages/@react-spectrum/slider/test/RangeSlider.test.tsx b/packages/@react-spectrum/slider/test/RangeSlider.test.tsx index b2c068dc6bf..62ba8416726 100644 --- a/packages/@react-spectrum/slider/test/RangeSlider.test.tsx +++ b/packages/@react-spectrum/slider/test/RangeSlider.test.tsx @@ -19,7 +19,7 @@ import {theme} from '@react-spectrum/theme-default'; import userEvent from '@testing-library/user-event'; -describe('Slider', function () { +describe('RangeSlider', function () { it('supports aria-label', function () { let {getByRole} = render(); From ca4fcd74b06fc87daf255a6215d594c701235234 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Fri, 2 Oct 2020 18:50:34 +0200 Subject: [PATCH 38/40] Slider: forwardRef --- packages/@react-spectrum/slider/src/RangeSlider.tsx | 10 ++++++---- packages/@react-spectrum/slider/src/Slider.tsx | 8 +++++--- packages/@react-spectrum/slider/src/SliderBase.tsx | 12 ++++++++---- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/@react-spectrum/slider/src/RangeSlider.tsx b/packages/@react-spectrum/slider/src/RangeSlider.tsx index 043f01d0c80..a7b59778287 100644 --- a/packages/@react-spectrum/slider/src/RangeSlider.tsx +++ b/packages/@react-spectrum/slider/src/RangeSlider.tsx @@ -12,6 +12,7 @@ import {classNames} from '@react-spectrum/utils'; import {DEFAULT_MAX_VALUE, DEFAULT_MIN_VALUE} from '@react-stately/slider'; +import {FocusableRef} from '@react-types/shared'; import {FocusRing} from '@react-aria/focus'; import {mergeProps} from '@react-aria/utils'; import React from 'react'; @@ -22,7 +23,7 @@ import {useHover} from '@react-aria/interactions'; import {useLocale} from '@react-aria/i18n'; import {VisuallyHidden} from '@react-aria/visually-hidden'; -function RangeSlider(props: SpectrumRangeSliderProps) { +function RangeSlider(props: SpectrumRangeSliderProps, ref: FocusableRef) { let {onChange, value, defaultValue, ...otherProps} = props; let baseProps: Omit = { @@ -41,7 +42,7 @@ function RangeSlider(props: SpectrumRangeSliderProps) { let hovers = [useHover({}), useHover({})]; return ( - + {({state, thumbProps, inputRefs, inputProps, ticks}: SliderBaseChildArguments) => { let cssDirection = direction === 'rtl' ? 'right' : 'left'; @@ -81,5 +82,6 @@ function RangeSlider(props: SpectrumRangeSliderProps) { ); } -// TODO forwardref? -export {RangeSlider}; + +const _RangeSlider = React.forwardRef(RangeSlider); +export {_RangeSlider as RangeSlider}; diff --git a/packages/@react-spectrum/slider/src/Slider.tsx b/packages/@react-spectrum/slider/src/Slider.tsx index 8fdbca5847b..408b1dd5e5a 100644 --- a/packages/@react-spectrum/slider/src/Slider.tsx +++ b/packages/@react-spectrum/slider/src/Slider.tsx @@ -12,6 +12,7 @@ import {clamp, mergeProps} from '@react-aria/utils'; import {classNames} from '@react-spectrum/utils'; +import {FocusableRef} from '@react-types/shared'; import {FocusRing} from '@react-aria/focus'; import React from 'react'; import {SliderBase, SliderBaseChildArguments, SliderBaseProps} from './SliderBase'; @@ -21,7 +22,7 @@ import {useHover} from '@react-aria/interactions'; import {useLocale} from '@react-aria/i18n'; import {VisuallyHidden} from '@react-aria/visually-hidden'; -function Slider(props: SpectrumSliderProps) { +function Slider(props: SpectrumSliderProps, ref: FocusableRef) { let {onChange, value, defaultValue, isFilled, fillOffset, trackGradient, ...otherProps} = props; let baseProps: Omit = { @@ -41,6 +42,7 @@ function Slider(props: SpectrumSliderProps) { return ( , Spectr count: 1 | 2 } -function SliderBase(props: SliderBaseProps) { +function SliderBase(props: SliderBaseProps, ref: FocusableRef) { props = useProviderProps(props); let { tickCount, showTickLabels, tickLabels, isDisabled, count, @@ -93,6 +94,8 @@ function SliderBase(props: SliderBaseProps) { thumbProps[i] = v.thumbProps; } + let domRef = useFocusableRef(ref, inputRefs[0]); + let ticks = null; if (tickCount > 0) { let tickList = []; @@ -152,6 +155,7 @@ function SliderBase(props: SliderBaseProps) { return (
Date: Fri, 2 Oct 2020 18:57:20 +0200 Subject: [PATCH 39/40] stately/slider: remove isReadOnly --- packages/@react-aria/slider/src/useSlider.ts | 4 +--- packages/@react-aria/slider/src/useSliderThumb.ts | 15 ++++++--------- .../slider/stories/StoryMultiSlider.tsx | 1 - .../slider/stories/StoryRangeSlider.tsx | 8 +++----- .../@react-aria/slider/stories/StorySlider.tsx | 1 - .../@react-stately/slider/src/useSliderState.ts | 6 +++--- packages/@react-types/slider/src/index.d.ts | 4 +--- 7 files changed, 14 insertions(+), 25 deletions(-) diff --git a/packages/@react-aria/slider/src/useSlider.ts b/packages/@react-aria/slider/src/useSlider.ts index 4920e6c9bea..5d63d2f3040 100644 --- a/packages/@react-aria/slider/src/useSlider.ts +++ b/packages/@react-aria/slider/src/useSlider.ts @@ -45,8 +45,6 @@ export function useSlider( ): SliderAria { const {labelProps, fieldProps} = useLabel(props); - const isSliderEditable = !(props.isDisabled || props.isReadOnly); - // Attach id of the label to the state so it can be accessed by useSliderThumb. sliderIds.set(state, labelProps.id ?? fieldProps.id); @@ -109,7 +107,7 @@ export function useSlider( trackProps: mergeProps({ onMouseDown: (e: React.MouseEvent) => { // We only trigger track-dragging if the user clicks on the track itself. - if (trackRef.current && isSliderEditable) { + if (trackRef.current && !props.isDisabled) { // Find the closest thumb const trackPosition = trackRef.current.getBoundingClientRect().left; const clickPosition = e.clientX; diff --git a/packages/@react-aria/slider/src/useSliderThumb.ts b/packages/@react-aria/slider/src/useSliderThumb.ts index 48872d2bedb..bde1adb9e63 100644 --- a/packages/@react-aria/slider/src/useSliderThumb.ts +++ b/packages/@react-aria/slider/src/useSliderThumb.ts @@ -30,13 +30,12 @@ interface SliderThumbOptions extends SliderThumbProps { */ export function useSliderThumb( opts: SliderThumbOptions, - state: SliderState, + state: SliderState ): SliderThumbAria { const { index, isRequired, isDisabled, - isReadOnly, validationState, trackRef, inputRef, @@ -50,7 +49,6 @@ export function useSliderThumb( }); const value = state.values[index]; - const isEditable = !(isDisabled || isReadOnly); const focusInput = useCallback(() => { if (inputRef.current) { @@ -81,7 +79,7 @@ export function useSliderThumb( }); // Immediately register editability with the state - state.setThumbEditable(index, isEditable); + state.setThumbEditable(index, !isDisabled); const {focusableProps} = useFocusable( mergeProps(opts, { @@ -98,12 +96,11 @@ export function useSliderThumb( return { inputProps: mergeProps(focusableProps, fieldProps, { type: 'range', - tabIndex: isEditable ? 0 : undefined, + tabIndex: !isDisabled ? 0 : undefined, min: state.getThumbMinValue(index), max: state.getThumbMaxValue(index), step: state.step, value: value, - readOnly: isReadOnly, disabled: isDisabled, 'aria-orientation': 'horizontal', 'aria-valuetext': state.getThumbValueLabel(index), @@ -114,13 +111,13 @@ export function useSliderThumb( state.setThumbValue(index, parseFloat(e.target.value)); } }), - thumbProps: mergeProps(isEditable ? { + thumbProps: !isDisabled ? mergeProps({ onMouseDown: draggableProps.onMouseDown, onMouseEnter: draggableProps.onMouseEnter, onMouseOut: draggableProps.onMouseOut - } : {}, isDisabled ? {} : { + }, { onMouseDown: focusInput - }), + }) : {}, labelProps }; } diff --git a/packages/@react-aria/slider/stories/StoryMultiSlider.tsx b/packages/@react-aria/slider/stories/StoryMultiSlider.tsx index 2b7fbcbf818..86d0e8f1e2d 100644 --- a/packages/@react-aria/slider/stories/StoryMultiSlider.tsx +++ b/packages/@react-aria/slider/stories/StoryMultiSlider.tsx @@ -75,7 +75,6 @@ export function StoryThumb(props: StoryThumbProps) { const {inputProps, thumbProps, labelProps} = useSliderThumb({ index, ...props, - isReadOnly: sliderProps.isReadOnly || props.isReadOnly, isDisabled: sliderProps.isDisabled || props.isDisabled, trackRef: context.trackRef, inputRef diff --git a/packages/@react-aria/slider/stories/StoryRangeSlider.tsx b/packages/@react-aria/slider/stories/StoryRangeSlider.tsx index 60cb0031d59..17a4fed7457 100644 --- a/packages/@react-aria/slider/stories/StoryRangeSlider.tsx +++ b/packages/@react-aria/slider/stories/StoryRangeSlider.tsx @@ -31,7 +31,6 @@ export function StoryRangeSlider(props: StoryRangeSliderProps) { const {thumbProps: minThumbProps, inputProps: minInputProps} = useSliderThumb({ index: 0, 'aria-label': minLabel ?? 'Minimum', - isReadOnly: props.isReadOnly, isDisabled: props.isDisabled, trackRef, inputRef: minInputRef @@ -40,7 +39,6 @@ export function StoryRangeSlider(props: StoryRangeSliderProps) { const {thumbProps: maxThumbProps, inputProps: maxInputProps} = useSliderThumb({ index: 1, 'aria-label': maxLabel ?? 'Maximum', - isReadOnly: props.isReadOnly, isDisabled: props.isDisabled, trackRef, inputRef: maxInputRef @@ -58,7 +56,7 @@ export function StoryRangeSlider(props: StoryRangeSliderProps) {
{ - // We make rail and filledRail children of track. User can click on the track, the + // We make rail and filledRail children of track. User can click on the track, the // rail, or the filledRail to drag by track }
@@ -77,7 +75,7 @@ export function StoryRangeSlider(props: StoryRangeSliderProps) { 'left': `${state.getThumbPercent(0) * 100}%` }}> { - // We put thumbProps on thumbHandle, so that you cannot drag by the tip + // We put thumbProps on thumbHandle, so that you cannot drag by the tip }
{props.showTip &&
{state.getThumbValueLabel(0)}
} @@ -92,7 +90,7 @@ export function StoryRangeSlider(props: StoryRangeSliderProps) { 'left': `${state.getThumbPercent(1) * 100}%` }}> { - // For fun, we put the thumbProps on the thumb container instead of just the handle. + // For fun, we put the thumbProps on the thumb container instead of just the handle. // This means you can drag the max thumb by the tip. }
diff --git a/packages/@react-aria/slider/stories/StorySlider.tsx b/packages/@react-aria/slider/stories/StorySlider.tsx index 6a542212e0e..c3d07e4517f 100644 --- a/packages/@react-aria/slider/stories/StorySlider.tsx +++ b/packages/@react-aria/slider/stories/StorySlider.tsx @@ -35,7 +35,6 @@ export function StorySlider(props: StorySliderProps) { const {thumbProps, inputProps} = useSliderThumb({ index: 0, - isReadOnly: props.isReadOnly, isDisabled: props.isDisabled, trackRef, inputRef diff --git a/packages/@react-stately/slider/src/useSliderState.ts b/packages/@react-stately/slider/src/useSliderState.ts index 66a9405e3f8..4459c877f56 100644 --- a/packages/@react-stately/slider/src/useSliderState.ts +++ b/packages/@react-stately/slider/src/useSliderState.ts @@ -64,7 +64,7 @@ export const DEFAULT_MAX_VALUE = 100; export const DEFAULT_STEP_VALUE = 1; export function useSliderState(props: Omit): SliderState { - let {isReadOnly, isDisabled, minValue = DEFAULT_MIN_VALUE, maxValue = DEFAULT_MAX_VALUE, formatOptions, step = DEFAULT_STEP_VALUE} = props; + let {isDisabled, minValue = DEFAULT_MIN_VALUE, maxValue = DEFAULT_MAX_VALUE, formatOptions, step = DEFAULT_STEP_VALUE} = props; const [values, setValues] = useControlledState( props.value as any, @@ -97,7 +97,7 @@ export function useSliderState(props: Omit): SliderSta } function updateValue(index: number, value: number) { - if (isReadOnly || isDisabled || !isThumbEditable(index)) { + if (isDisabled || !isThumbEditable(index)) { return; } const thisMin = getThumbMinValue(index); @@ -109,7 +109,7 @@ export function useSliderState(props: Omit): SliderSta } function updateDragging(index: number, dragging: boolean) { - if (isReadOnly || isDisabled || !isThumbEditable(index)) { + if (isDisabled || !isThumbEditable(index)) { return; } diff --git a/packages/@react-types/slider/src/index.d.ts b/packages/@react-types/slider/src/index.d.ts index d293214e40e..d17d926886b 100644 --- a/packages/@react-types/slider/src/index.d.ts +++ b/packages/@react-types/slider/src/index.d.ts @@ -2,7 +2,6 @@ import {AriaLabelingProps, AriaValidationProps, Direction, FocusableDOMProps, Fo import {ReactNode} from 'react'; export interface BaseSliderProps extends RangeInputBase, LabelableProps, AriaLabelingProps { - isReadOnly?: boolean, isDisabled?: boolean, formatOptions?: Intl.NumberFormatOptions } @@ -13,13 +12,12 @@ export interface SliderProps extends BaseSliderProps, ValueBase { } export interface SliderThumbProps extends AriaLabelingProps, FocusableDOMProps, FocusableProps, Validation, AriaValidationProps, LabelableProps { - isReadOnly?: boolean, isDisabled?: boolean, index: number, direction?: Direction } -export interface SpectrumBarSliderBase extends Omit, ValueBase, StyleProps { +export interface SpectrumBarSliderBase extends BaseSliderProps, ValueBase, StyleProps { orientation?: Orientation, labelPosition?: LabelPosition, /** Whether the value's label is displayed. True by default if there's a `label`, false by default if not. */ From c0e4d59cad4cdf2912a662e0fcc3d6fd1410b414 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Fri, 2 Oct 2020 19:12:35 +0200 Subject: [PATCH 40/40] Use useLocale in aria directly --- packages/@react-aria/slider/src/useSlider.ts | 3 ++- packages/@react-aria/slider/src/useSliderThumb.ts | 6 ++++-- packages/@react-spectrum/slider/src/SliderBase.tsx | 8 +++----- packages/@react-stately/slider/src/useSliderState.ts | 2 +- packages/@react-types/slider/src/index.d.ts | 8 +++----- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/@react-aria/slider/src/useSlider.ts b/packages/@react-aria/slider/src/useSlider.ts index 5d63d2f3040..7644eaa95a5 100644 --- a/packages/@react-aria/slider/src/useSlider.ts +++ b/packages/@react-aria/slider/src/useSlider.ts @@ -16,6 +16,7 @@ import {sliderIds} from './utils'; import {SliderProps} from '@react-types/slider'; import {SliderState} from '@react-stately/slider'; import {useLabel} from '@react-aria/label'; +import {useLocale} from '@react-aria/i18n'; interface SliderAria { /** Props for the label element. */ @@ -48,7 +49,7 @@ export function useSlider( // Attach id of the label to the state so it can be accessed by useSliderThumb. sliderIds.set(state, labelProps.id ?? fieldProps.id); - let {direction = 'ltr'} = props; + let {direction} = useLocale(); // When the user clicks or drags the track, we want the motion to set and drag the // closest thumb. Hence we also need to install useDrag1D() on the track element. diff --git a/packages/@react-aria/slider/src/useSliderThumb.ts b/packages/@react-aria/slider/src/useSliderThumb.ts index bde1adb9e63..90f1dfe2d64 100644 --- a/packages/@react-aria/slider/src/useSliderThumb.ts +++ b/packages/@react-aria/slider/src/useSliderThumb.ts @@ -5,6 +5,7 @@ import {SliderState} from '@react-stately/slider'; import {SliderThumbProps} from '@react-types/slider'; import {useFocusable} from '@react-aria/focus'; import {useLabel} from '@react-aria/label'; +import {useLocale} from '@react-aria/i18n'; interface SliderThumbAria { /** Props for the range input. */ @@ -38,10 +39,11 @@ export function useSliderThumb( isDisabled, validationState, trackRef, - inputRef, - direction = 'ltr' + inputRef } = opts; + let {direction} = useLocale(); + let labelId = sliderIds.get(state); const {labelProps, fieldProps} = useLabel({ ...opts, diff --git a/packages/@react-spectrum/slider/src/SliderBase.tsx b/packages/@react-spectrum/slider/src/SliderBase.tsx index a86cd3b1e19..6cf9b3a126d 100644 --- a/packages/@react-spectrum/slider/src/SliderBase.tsx +++ b/packages/@react-spectrum/slider/src/SliderBase.tsx @@ -16,7 +16,7 @@ import React, {CSSProperties, HTMLAttributes, MutableRefObject, ReactNode, useRe import {SliderState, useSliderState} from '@react-stately/slider'; import {SpectrumBarSliderBase, SpectrumSliderTicksBase} from '@react-types/slider'; import styles from '@adobe/spectrum-css-temp/components/slider/vars.css'; -import {useLocale, useNumberFormatter} from '@react-aria/i18n'; +import {useNumberFormatter} from '@react-aria/i18n'; import {useProviderProps} from '@react-spectrum/provider'; import {useSlider, useSliderThumb} from '@react-aria/slider'; @@ -46,7 +46,6 @@ function SliderBase(props: SliderBaseProps, ref: FocusableRef) { } = props; let {styleProps} = useStyleProps(otherProps); - let {direction} = useLocale(); // Assumes that DEFAULT_MIN_VALUE and DEFAULT_MAX_VALUE are both positive, this value needs to be passed to useSliderState, so // getThumbMinValue/getThumbMaxValue cannot be used here. @@ -73,7 +72,7 @@ function SliderBase(props: SliderBaseProps, ref: FocusableRef) { containerProps, trackProps, labelProps - } = useSlider({...props, direction}, state, trackRef); + } = useSlider(props, state, trackRef); let inputRefs = []; let thumbProps = []; @@ -86,8 +85,7 @@ function SliderBase(props: SliderBaseProps, ref: FocusableRef) { index: i, isDisabled: props.isDisabled, trackRef, - inputRef: inputRefs[i], - direction + inputRef: inputRefs[i] }, state); inputProps[i] = v.inputProps; diff --git a/packages/@react-stately/slider/src/useSliderState.ts b/packages/@react-stately/slider/src/useSliderState.ts index 4459c877f56..deef23e5510 100644 --- a/packages/@react-stately/slider/src/useSliderState.ts +++ b/packages/@react-stately/slider/src/useSliderState.ts @@ -63,7 +63,7 @@ export const DEFAULT_MIN_VALUE = 0; export const DEFAULT_MAX_VALUE = 100; export const DEFAULT_STEP_VALUE = 1; -export function useSliderState(props: Omit): SliderState { +export function useSliderState(props: SliderProps): SliderState { let {isDisabled, minValue = DEFAULT_MIN_VALUE, maxValue = DEFAULT_MAX_VALUE, formatOptions, step = DEFAULT_STEP_VALUE} = props; const [values, setValues] = useControlledState( diff --git a/packages/@react-types/slider/src/index.d.ts b/packages/@react-types/slider/src/index.d.ts index d17d926886b..ff724c5c6da 100644 --- a/packages/@react-types/slider/src/index.d.ts +++ b/packages/@react-types/slider/src/index.d.ts @@ -1,4 +1,4 @@ -import {AriaLabelingProps, AriaValidationProps, Direction, FocusableDOMProps, FocusableProps, LabelableProps, LabelPosition, Orientation, RangeInputBase, RangeValue, StyleProps, Validation, ValueBase} from '@react-types/shared'; +import {AriaLabelingProps, AriaValidationProps, FocusableDOMProps, FocusableProps, LabelableProps, LabelPosition, Orientation, RangeInputBase, RangeValue, StyleProps, Validation, ValueBase} from '@react-types/shared'; import {ReactNode} from 'react'; export interface BaseSliderProps extends RangeInputBase, LabelableProps, AriaLabelingProps { @@ -7,14 +7,12 @@ export interface BaseSliderProps extends RangeInputBase, LabelableProps, } export interface SliderProps extends BaseSliderProps, ValueBase { - onChangeEnd?: (value: number[]) => void, - direction?: Direction + onChangeEnd?: (value: number[]) => void } export interface SliderThumbProps extends AriaLabelingProps, FocusableDOMProps, FocusableProps, Validation, AriaValidationProps, LabelableProps { isDisabled?: boolean, - index: number, - direction?: Direction + index: number } export interface SpectrumBarSliderBase extends BaseSliderProps, ValueBase, StyleProps {