From 1de2a0d0941c2ee9a49b91f07ab0287583839e77 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Wed, 20 Oct 2021 13:55:09 -0700 Subject: [PATCH 01/23] Initial ColorArea implementation --- .../components/colorarea/index.css | 69 +++ .../components/colorarea/skin.css | 36 ++ .../components/colorarea/vars.css | 14 + packages/@react-aria/color/intl/ar-AE.json | 3 + packages/@react-aria/color/intl/bg-BG.json | 3 + packages/@react-aria/color/intl/cs-CZ.json | 3 + packages/@react-aria/color/intl/da-DK.json | 3 + packages/@react-aria/color/intl/de-DE.json | 3 + packages/@react-aria/color/intl/el-GR.json | 3 + packages/@react-aria/color/intl/en-US.json | 3 + packages/@react-aria/color/intl/es-ES.json | 3 + packages/@react-aria/color/intl/et-EE.json | 3 + packages/@react-aria/color/intl/fi-FI.json | 3 + packages/@react-aria/color/intl/fr-FR.json | 3 + packages/@react-aria/color/intl/he-IL.json | 3 + packages/@react-aria/color/intl/hr-HR.json | 3 + packages/@react-aria/color/intl/hu-HU.json | 3 + packages/@react-aria/color/intl/it-IT.json | 3 + packages/@react-aria/color/intl/ja-JP.json | 3 + packages/@react-aria/color/intl/ko-KR.json | 3 + packages/@react-aria/color/intl/lt-LT.json | 3 + packages/@react-aria/color/intl/lv-LV.json | 3 + packages/@react-aria/color/intl/nb-NO.json | 3 + packages/@react-aria/color/intl/nl-NL.json | 3 + packages/@react-aria/color/intl/pl-PL.json | 3 + packages/@react-aria/color/intl/pt-BR.json | 3 + packages/@react-aria/color/intl/pt-PT.json | 3 + packages/@react-aria/color/intl/ro-RO.json | 3 + packages/@react-aria/color/intl/ru-RU.json | 3 + packages/@react-aria/color/intl/sk-SK.json | 3 + packages/@react-aria/color/intl/sl-SI.json | 3 + packages/@react-aria/color/intl/sr-SP.json | 3 + packages/@react-aria/color/intl/sv-SE.json | 3 + packages/@react-aria/color/intl/tr-TR.json | 3 + packages/@react-aria/color/intl/uk-UA.json | 3 + packages/@react-aria/color/intl/zh-CN.json | 3 + packages/@react-aria/color/intl/zh-TW.json | 3 + packages/@react-aria/color/package.json | 6 +- packages/@react-aria/color/src/index.ts | 1 + .../@react-aria/color/src/useColorArea.ts | 424 ++++++++++++++++++ .../@react-spectrum/color/src/ColorArea.tsx | 199 ++++++++ .../@react-spectrum/color/src/ColorThumb.tsx | 1 + packages/@react-spectrum/color/src/index.ts | 1 + .../color/stories/ColorArea.stories.tsx | 75 ++++ packages/@react-stately/color/src/Color.ts | 8 + packages/@react-stately/color/src/index.ts | 1 + .../color/src/useColorAreaState.ts | 219 +++++++++ .../color/src/useColorSliderState.ts | 10 +- packages/@react-types/color/src/index.d.ts | 29 ++ 49 files changed, 1184 insertions(+), 11 deletions(-) create mode 100644 packages/@adobe/spectrum-css-temp/components/colorarea/index.css create mode 100644 packages/@adobe/spectrum-css-temp/components/colorarea/skin.css create mode 100644 packages/@adobe/spectrum-css-temp/components/colorarea/vars.css create mode 100644 packages/@react-aria/color/intl/ar-AE.json create mode 100644 packages/@react-aria/color/intl/bg-BG.json create mode 100644 packages/@react-aria/color/intl/cs-CZ.json create mode 100644 packages/@react-aria/color/intl/da-DK.json create mode 100644 packages/@react-aria/color/intl/de-DE.json create mode 100644 packages/@react-aria/color/intl/el-GR.json create mode 100644 packages/@react-aria/color/intl/en-US.json create mode 100644 packages/@react-aria/color/intl/es-ES.json create mode 100644 packages/@react-aria/color/intl/et-EE.json create mode 100644 packages/@react-aria/color/intl/fi-FI.json create mode 100644 packages/@react-aria/color/intl/fr-FR.json create mode 100644 packages/@react-aria/color/intl/he-IL.json create mode 100644 packages/@react-aria/color/intl/hr-HR.json create mode 100644 packages/@react-aria/color/intl/hu-HU.json create mode 100644 packages/@react-aria/color/intl/it-IT.json create mode 100644 packages/@react-aria/color/intl/ja-JP.json create mode 100644 packages/@react-aria/color/intl/ko-KR.json create mode 100644 packages/@react-aria/color/intl/lt-LT.json create mode 100644 packages/@react-aria/color/intl/lv-LV.json create mode 100644 packages/@react-aria/color/intl/nb-NO.json create mode 100644 packages/@react-aria/color/intl/nl-NL.json create mode 100644 packages/@react-aria/color/intl/pl-PL.json create mode 100644 packages/@react-aria/color/intl/pt-BR.json create mode 100644 packages/@react-aria/color/intl/pt-PT.json create mode 100644 packages/@react-aria/color/intl/ro-RO.json create mode 100644 packages/@react-aria/color/intl/ru-RU.json create mode 100644 packages/@react-aria/color/intl/sk-SK.json create mode 100644 packages/@react-aria/color/intl/sl-SI.json create mode 100644 packages/@react-aria/color/intl/sr-SP.json create mode 100644 packages/@react-aria/color/intl/sv-SE.json create mode 100644 packages/@react-aria/color/intl/tr-TR.json create mode 100644 packages/@react-aria/color/intl/uk-UA.json create mode 100644 packages/@react-aria/color/intl/zh-CN.json create mode 100644 packages/@react-aria/color/intl/zh-TW.json create mode 100644 packages/@react-aria/color/src/useColorArea.ts create mode 100644 packages/@react-spectrum/color/src/ColorArea.tsx create mode 100644 packages/@react-spectrum/color/stories/ColorArea.stories.tsx create mode 100644 packages/@react-stately/color/src/useColorAreaState.ts diff --git a/packages/@adobe/spectrum-css-temp/components/colorarea/index.css b/packages/@adobe/spectrum-css-temp/components/colorarea/index.css new file mode 100644 index 00000000000..db0c9c08907 --- /dev/null +++ b/packages/@adobe/spectrum-css-temp/components/colorarea/index.css @@ -0,0 +1,69 @@ +.spectrum-ColorArea { + position: relative; + display: inline-block; + width: var(--spectrum-colorarea-default-width); + height: var(--spectrum-colorarea-default-height); + + border-radius: var(--spectrum-colorarea-border-radius); + + cursor: default; + + user-select: none; + + &.is-focused, + &.focus-ring { + z-index: 2; + + .spectrum-ColorArea-handle { + /* Bigger handle when focused */ + width: calc(var(--spectrum-colorhandle-size) * 2); + height: calc(var(--spectrum-colorhandle-size) * 2); + + margin-left: calc(-1 * var(--spectrum-colorhandle-size)); + margin-top: calc(-1 * var(--spectrum-colorhandle-size)); + } + } + + &:focus-within { + z-index: 2; + } + + &.is-disabled { + pointer-events: none; + } + + /* the floating inset box shadow must be a separate element since won't take it */ + &:before { + content: ''; + z-index: 1; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + border-radius: var(--spectrum-colorarea-border-radius); + } +} + +.spectrum-ColorArea-handle { + left: 0; + top: 0; +} + +.spectrum-ColorArea-gradient { + width: 100%; + height: 100%; + border-radius: var(--spectrum-colorarea-border-radius); +} + +.spectrum-ColorArea-slider { + opacity: 0.0001; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 0; + margin: 0; + pointer-events: none; +} diff --git a/packages/@adobe/spectrum-css-temp/components/colorarea/skin.css b/packages/@adobe/spectrum-css-temp/components/colorarea/skin.css new file mode 100644 index 00000000000..e558a2e2542 --- /dev/null +++ b/packages/@adobe/spectrum-css-temp/components/colorarea/skin.css @@ -0,0 +1,36 @@ +.spectrum-ColorArea { + &:before { + box-shadow: inset 0 0 0 var(--spectrum-colorarea-border-size) var(--spectrum-colorarea-border-color); + } + } + .spectrum-ColorArea-gradient { + forced-color-adjust: none; + } + .spectrum-ColorHandle-color { + forced-color-adjust: none; + } + + .spectrum-ColorArea { + &.is-disabled { + background: var(--spectrum-colorarea-fill-color-disabled); + + &:before { + box-shadow: inset 0 0 0 var(--spectrum-colorarea-border-size) var(--spectrum-colorarea-border-color-disabled); + } + + .spectrum-ColorArea-gradient { + display: none; + } + } + } + + @media (forced-colors: active) { + .spectrum-ColorArea { + --spectrum-colorarea-fill-color-disabled : GrayText; + } + .spectrum-ColorArea { + &.is-disabled { + forced-color-adjust: none; + } + } + } \ No newline at end of file diff --git a/packages/@adobe/spectrum-css-temp/components/colorarea/vars.css b/packages/@adobe/spectrum-css-temp/components/colorarea/vars.css new file mode 100644 index 00000000000..f0681ac2aaf --- /dev/null +++ b/packages/@adobe/spectrum-css-temp/components/colorarea/vars.css @@ -0,0 +1,14 @@ +/* + * Copyright 2021 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 './index.css'; +@import './skin.css'; diff --git a/packages/@react-aria/color/intl/ar-AE.json b/packages/@react-aria/color/intl/ar-AE.json new file mode 100644 index 00000000000..2c4a69d4c60 --- /dev/null +++ b/packages/@react-aria/color/intl/ar-AE.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "شريط تمرير ثنائي الأبعاد" +} diff --git a/packages/@react-aria/color/intl/bg-BG.json b/packages/@react-aria/color/intl/bg-BG.json new file mode 100644 index 00000000000..124b7776de8 --- /dev/null +++ b/packages/@react-aria/color/intl/bg-BG.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "2D плъзгач" +} diff --git a/packages/@react-aria/color/intl/cs-CZ.json b/packages/@react-aria/color/intl/cs-CZ.json new file mode 100644 index 00000000000..15dfd16c849 --- /dev/null +++ b/packages/@react-aria/color/intl/cs-CZ.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "2D posuvník" +} diff --git a/packages/@react-aria/color/intl/da-DK.json b/packages/@react-aria/color/intl/da-DK.json new file mode 100644 index 00000000000..c17a0058b2c --- /dev/null +++ b/packages/@react-aria/color/intl/da-DK.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "2D-skyder" +} diff --git a/packages/@react-aria/color/intl/de-DE.json b/packages/@react-aria/color/intl/de-DE.json new file mode 100644 index 00000000000..89ef078dc20 --- /dev/null +++ b/packages/@react-aria/color/intl/de-DE.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "2D-Schieberegler" +} diff --git a/packages/@react-aria/color/intl/el-GR.json b/packages/@react-aria/color/intl/el-GR.json new file mode 100644 index 00000000000..85a99c744c3 --- /dev/null +++ b/packages/@react-aria/color/intl/el-GR.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "Ρυθμιστικό 2D" +} diff --git a/packages/@react-aria/color/intl/en-US.json b/packages/@react-aria/color/intl/en-US.json new file mode 100644 index 00000000000..ba3b22fee50 --- /dev/null +++ b/packages/@react-aria/color/intl/en-US.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "2D slider" +} \ No newline at end of file diff --git a/packages/@react-aria/color/intl/es-ES.json b/packages/@react-aria/color/intl/es-ES.json new file mode 100644 index 00000000000..11bc234cdd1 --- /dev/null +++ b/packages/@react-aria/color/intl/es-ES.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "Control deslizante en 2D" +} diff --git a/packages/@react-aria/color/intl/et-EE.json b/packages/@react-aria/color/intl/et-EE.json new file mode 100644 index 00000000000..8cb808cd272 --- /dev/null +++ b/packages/@react-aria/color/intl/et-EE.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "2D-liugur" +} diff --git a/packages/@react-aria/color/intl/fi-FI.json b/packages/@react-aria/color/intl/fi-FI.json new file mode 100644 index 00000000000..11e27b2cc1c --- /dev/null +++ b/packages/@react-aria/color/intl/fi-FI.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "2D-liukusäädin" +} diff --git a/packages/@react-aria/color/intl/fr-FR.json b/packages/@react-aria/color/intl/fr-FR.json new file mode 100644 index 00000000000..7fce4f30892 --- /dev/null +++ b/packages/@react-aria/color/intl/fr-FR.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "Curseur 2D" +} diff --git a/packages/@react-aria/color/intl/he-IL.json b/packages/@react-aria/color/intl/he-IL.json new file mode 100644 index 00000000000..51d213388c7 --- /dev/null +++ b/packages/@react-aria/color/intl/he-IL.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "מחוון דו-ממדי" +} diff --git a/packages/@react-aria/color/intl/hr-HR.json b/packages/@react-aria/color/intl/hr-HR.json new file mode 100644 index 00000000000..690c8b20cda --- /dev/null +++ b/packages/@react-aria/color/intl/hr-HR.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "2D kliznik" +} diff --git a/packages/@react-aria/color/intl/hu-HU.json b/packages/@react-aria/color/intl/hu-HU.json new file mode 100644 index 00000000000..9d1747751f1 --- /dev/null +++ b/packages/@react-aria/color/intl/hu-HU.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "2D csúszka" +} diff --git a/packages/@react-aria/color/intl/it-IT.json b/packages/@react-aria/color/intl/it-IT.json new file mode 100644 index 00000000000..d6abe384322 --- /dev/null +++ b/packages/@react-aria/color/intl/it-IT.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "Dispositivo di scorrimento 2D" +} diff --git a/packages/@react-aria/color/intl/ja-JP.json b/packages/@react-aria/color/intl/ja-JP.json new file mode 100644 index 00000000000..e5f4bfc0c47 --- /dev/null +++ b/packages/@react-aria/color/intl/ja-JP.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "2D スライダー" +} diff --git a/packages/@react-aria/color/intl/ko-KR.json b/packages/@react-aria/color/intl/ko-KR.json new file mode 100644 index 00000000000..775390ef610 --- /dev/null +++ b/packages/@react-aria/color/intl/ko-KR.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "2D 슬라이더" +} diff --git a/packages/@react-aria/color/intl/lt-LT.json b/packages/@react-aria/color/intl/lt-LT.json new file mode 100644 index 00000000000..0cc8009a9d5 --- /dev/null +++ b/packages/@react-aria/color/intl/lt-LT.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "2D slankiklis" +} diff --git a/packages/@react-aria/color/intl/lv-LV.json b/packages/@react-aria/color/intl/lv-LV.json new file mode 100644 index 00000000000..981f5c8cfe6 --- /dev/null +++ b/packages/@react-aria/color/intl/lv-LV.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "Plaknes slīdnis" +} diff --git a/packages/@react-aria/color/intl/nb-NO.json b/packages/@react-aria/color/intl/nb-NO.json new file mode 100644 index 00000000000..88624e00e74 --- /dev/null +++ b/packages/@react-aria/color/intl/nb-NO.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "2D-glidebryter" +} diff --git a/packages/@react-aria/color/intl/nl-NL.json b/packages/@react-aria/color/intl/nl-NL.json new file mode 100644 index 00000000000..3c553ec0684 --- /dev/null +++ b/packages/@react-aria/color/intl/nl-NL.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "2D-schuifregelaar" +} diff --git a/packages/@react-aria/color/intl/pl-PL.json b/packages/@react-aria/color/intl/pl-PL.json new file mode 100644 index 00000000000..17561805b95 --- /dev/null +++ b/packages/@react-aria/color/intl/pl-PL.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "Suwak 2D" +} diff --git a/packages/@react-aria/color/intl/pt-BR.json b/packages/@react-aria/color/intl/pt-BR.json new file mode 100644 index 00000000000..f3f99a977c5 --- /dev/null +++ b/packages/@react-aria/color/intl/pt-BR.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "Controle deslizante 2D" +} diff --git a/packages/@react-aria/color/intl/pt-PT.json b/packages/@react-aria/color/intl/pt-PT.json new file mode 100644 index 00000000000..d4158c3fe81 --- /dev/null +++ b/packages/@react-aria/color/intl/pt-PT.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "Controlo de deslize 2D" +} diff --git a/packages/@react-aria/color/intl/ro-RO.json b/packages/@react-aria/color/intl/ro-RO.json new file mode 100644 index 00000000000..7acde457ab1 --- /dev/null +++ b/packages/@react-aria/color/intl/ro-RO.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "Cursor 2D" +} diff --git a/packages/@react-aria/color/intl/ru-RU.json b/packages/@react-aria/color/intl/ru-RU.json new file mode 100644 index 00000000000..26007c38cc7 --- /dev/null +++ b/packages/@react-aria/color/intl/ru-RU.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "Двумерный ползунок" +} diff --git a/packages/@react-aria/color/intl/sk-SK.json b/packages/@react-aria/color/intl/sk-SK.json new file mode 100644 index 00000000000..f36345a42f3 --- /dev/null +++ b/packages/@react-aria/color/intl/sk-SK.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "2D jazdec" +} diff --git a/packages/@react-aria/color/intl/sl-SI.json b/packages/@react-aria/color/intl/sl-SI.json new file mode 100644 index 00000000000..9e524397af3 --- /dev/null +++ b/packages/@react-aria/color/intl/sl-SI.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "2D-drsnik" +} diff --git a/packages/@react-aria/color/intl/sr-SP.json b/packages/@react-aria/color/intl/sr-SP.json new file mode 100644 index 00000000000..cc2bd776eb7 --- /dev/null +++ b/packages/@react-aria/color/intl/sr-SP.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "2D клизач" +} diff --git a/packages/@react-aria/color/intl/sv-SE.json b/packages/@react-aria/color/intl/sv-SE.json new file mode 100644 index 00000000000..cbc522867fb --- /dev/null +++ b/packages/@react-aria/color/intl/sv-SE.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "2D-reglage" +} diff --git a/packages/@react-aria/color/intl/tr-TR.json b/packages/@react-aria/color/intl/tr-TR.json new file mode 100644 index 00000000000..b6ecba53326 --- /dev/null +++ b/packages/@react-aria/color/intl/tr-TR.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "2B slayt gösterisi" +} diff --git a/packages/@react-aria/color/intl/uk-UA.json b/packages/@react-aria/color/intl/uk-UA.json new file mode 100644 index 00000000000..c175dc51e4e --- /dev/null +++ b/packages/@react-aria/color/intl/uk-UA.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "Повзунок 2D" +} diff --git a/packages/@react-aria/color/intl/zh-CN.json b/packages/@react-aria/color/intl/zh-CN.json new file mode 100644 index 00000000000..579d2f6729f --- /dev/null +++ b/packages/@react-aria/color/intl/zh-CN.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "2D 滑块" +} diff --git a/packages/@react-aria/color/intl/zh-TW.json b/packages/@react-aria/color/intl/zh-TW.json new file mode 100644 index 00000000000..d09b5f80c2e --- /dev/null +++ b/packages/@react-aria/color/intl/zh-TW.json @@ -0,0 +1,3 @@ +{ + "twoDimensionalSlider": "2D 滑桿" +} diff --git a/packages/@react-aria/color/package.json b/packages/@react-aria/color/package.json index 8f4ac5a2cb7..e3af1d84054 100644 --- a/packages/@react-aria/color/package.json +++ b/packages/@react-aria/color/package.json @@ -18,12 +18,14 @@ }, "dependencies": { "@babel/runtime": "^7.6.2", - "@react-aria/spinbutton": "^3.0.1", - "@react-aria/textfield": "^3.4.0", + "@internationalized/message": "^3.0.2", "@react-aria/i18n": "^3.3.2", "@react-aria/interactions": "^3.6.0", "@react-aria/slider": "^3.0.3", + "@react-aria/spinbutton": "^3.0.1", + "@react-aria/textfield": "^3.4.0", "@react-aria/utils": "^3.9.0", + "@react-aria/visually-hidden": "^3.2.3", "@react-stately/color": "3.0.0-beta.4", "@react-types/color": "3.0.0-beta.3", "@react-types/shared": "^3.9.0", diff --git a/packages/@react-aria/color/src/index.ts b/packages/@react-aria/color/src/index.ts index dd98ffece47..64b356d1a53 100644 --- a/packages/@react-aria/color/src/index.ts +++ b/packages/@react-aria/color/src/index.ts @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ +export * from './useColorArea'; export * from './useColorSlider'; export * from './useColorWheel'; export * from './useColorField'; diff --git a/packages/@react-aria/color/src/useColorArea.ts b/packages/@react-aria/color/src/useColorArea.ts new file mode 100644 index 00000000000..04f1e5e55fc --- /dev/null +++ b/packages/@react-aria/color/src/useColorArea.ts @@ -0,0 +1,424 @@ +/* + * 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 {AriaColorAreaProps} from '@react-types/color'; +import {clamp, focusWithoutScrolling, mergeProps, useGlobalListeners, useLabels} from '@react-aria/utils'; +import {ColorAreaState} from '@react-stately/color'; +// @ts-ignore +import intlMessages from '../intl/*.json'; +import {MessageDictionary} from '@internationalized/message'; +import React, {ChangeEvent, HTMLAttributes, InputHTMLAttributes, RefObject, useCallback, useRef} from 'react'; +import {useKeyboard, useMove} from '@react-aria/interactions'; +import {useLocale} from '@react-aria/i18n'; +import {useVisuallyHidden} from '@react-aria/visually-hidden'; + +const messages = new MessageDictionary(intlMessages); + +interface ColorAreaAria { + /** Props for the color area container element. */ + colorAreaProps: HTMLAttributes, + /** Props for the color area gradient foreground element. */ + gradientProps: HTMLAttributes, + /** Props for the thumb element. */ + thumbProps: HTMLAttributes, + /** Props for the visually hidden horizontal range input element. */ + xInputProps: InputHTMLAttributes, + /** Props for the visually hidden vertical range input element. */ + yInputProps: InputHTMLAttributes +} + +const PERCENT_STEP_SIZE = 10; +const HUE_STEP_SIZE = 15; +const RGB_STEP_SIZE = 16; +const CHANNEL_STEP_SIZE = { + hue: HUE_STEP_SIZE, + saturation: PERCENT_STEP_SIZE, + brightness: PERCENT_STEP_SIZE, + lightness: PERCENT_STEP_SIZE, + red: RGB_STEP_SIZE, + green: RGB_STEP_SIZE, + blue: RGB_STEP_SIZE +}; + + +function maxMinOrZero(value1: number, value2: number): number { + if (value1 === 0) { + return 0; + } + return value1 < 0 ? Math.min(value1, -1 * value2) : Math.max(value1, value2); +} + +/** + * Provides the behavior and accessibility implementation for a color wheel component. + * Color wheels allow users to adjust the hue of an HSL or HSB color value on a circular track. + */ +export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, inputXRef: RefObject, inputYRef: RefObject, containerRef: RefObject): ColorAreaAria { + let { + isDisabled, + xChannel, + yChannel, + xChannelStep, + yChannelStep + } = props; + + let {addGlobalListener, removeGlobalListener} = useGlobalListeners(); + + let {direction, locale} = useLocale(); + + let focusedInputRef = useRef(null); + + let focusInput = useCallback((inputRef:RefObject = inputXRef) => { + if (inputRef.current) { + focusWithoutScrolling(inputRef.current); + } + }, [inputXRef]); + + let stateRef = useRef(null); + stateRef.current = state; + let channels = stateRef.current.getChannels(); + if (!xChannel || !yChannel) { + xChannel = channels.xChannel; + yChannel = channels.yChannel; + } + let zChannel = channels.zChannel; + + if (!xChannelStep) { + xChannelStep = stateRef.current.value.getChannelRange(xChannel).step; + } + + if (!yChannelStep) { + yChannelStep = stateRef.current.value.getChannelRange(yChannel).step; + } + + let currentPosition = useRef<{x: number, y: number}>(null); + + let {keyboardProps} = useKeyboard({ + onKeyDown(e) { + if (!e.shiftKey && /^Arrow(?:Right|Left|Up|Down)$/.test(e.key)) { + return; + } + let stepSize = Math.max(xChannelStep, CHANNEL_STEP_SIZE[xChannel]); + let range = stateRef.current.value.getChannelRange(xChannel); + switch (e.key) { + case 'PageUp': + case 'ArrowUp': + range = stateRef.current.value.getChannelRange(yChannel); + stepSize = Math.max(yChannelStep, CHANNEL_STEP_SIZE[yChannel]); + stateRef.current.setYValue( + clamp( + (Math.floor(stateRef.current.yValue / stepSize) + 1) * stepSize, + range.minValue, + range.maxValue + ) + ); + focusedInputRef.current = inputYRef.current; + break; + case 'PageDown': + case 'ArrowDown': + range = stateRef.current.value.getChannelRange(yChannel); + stepSize = Math.max(yChannelStep, CHANNEL_STEP_SIZE[yChannel]); + stateRef.current.setYValue( + clamp( + (Math.ceil(stateRef.current.yValue / stepSize) - 1) * stepSize, + range.minValue, + range.maxValue + ) + ); + focusedInputRef.current = inputYRef.current; + break; + case 'Home': + case 'ArrowLeft': + stateRef.current.setXValue( + clamp( + (Math[direction === 'rtl' ? 'floor' : 'ceil'](stateRef.current.xValue / stepSize) + (direction === 'rtl' ? 1 : -1)) * stepSize, + range.minValue, + range.maxValue + ) + ); + focusedInputRef.current = inputXRef.current; + break; + case 'End': + case 'ArrowRight': + stateRef.current.setXValue( + clamp( + (Math[direction === 'rtl' ? 'floor' : 'ceil'](stateRef.current.xValue / stepSize) + (direction === 'rtl' ? -1 : 1)) * stepSize, + range.minValue, + range.maxValue + ) + ); + focusedInputRef.current = inputXRef.current; + break; + } + if (focusedInputRef.current) { + e.preventDefault(); + focusInput(focusedInputRef.current ? focusedInputRef : inputXRef); + focusedInputRef.current = undefined; + } + } + }); + + let moveHandler = { + onMoveStart() { + currentPosition.current = null; + state.setDragging(true); + }, + onMove({deltaX, deltaY, pointerType}) { + if (currentPosition.current == null) { + currentPosition.current = stateRef.current.getThumbPosition(); + } + let {width, height} = containerRef.current.getBoundingClientRect(); + if (pointerType === 'keyboard') { + deltaX = maxMinOrZero(deltaX, xChannelStep); + deltaY = maxMinOrZero(deltaY, yChannelStep); + if (deltaX !== 0) { + stateRef.current[`${deltaX > 0 ? 'increment' : 'decrement'}X`](Math.abs(deltaX)); + } + if (deltaY !== 0) { + stateRef.current[`${deltaY < 0 ? 'increment' : 'decrement'}Y`](Math.abs(deltaY)); + } + // set the focused input based on which axis has the greater delta + focusedInputRef.current = (deltaX !== 0 || deltaY !== 0) && Math.abs(deltaY) > Math.abs(deltaX) ? inputYRef.current : inputXRef.current; + } + currentPosition.current.x += (direction === 'rtl' ? -1 : 1) * deltaX / width ; + currentPosition.current.y += deltaY / height; + if (pointerType !== 'keyboard') { + stateRef.current.setColorFromPoint(currentPosition.current.x, currentPosition.current.y); + } + }, + onMoveEnd() { + isOnColorArea.current = undefined; + state.setDragging(false); + focusInput(focusedInputRef.current ? focusedInputRef : inputXRef); + focusedInputRef.current = undefined; + } + }; + let {moveProps: movePropsThumb} = useMove(moveHandler); + + let currentPointer = useRef(undefined); + let isOnColorArea = useRef(false); + let {moveProps: movePropsContainer} = useMove({ + onMoveStart() { + if (isOnColorArea.current) { + moveHandler.onMoveStart(); + } + }, + onMove(e) { + if (isOnColorArea.current) { + moveHandler.onMove(e); + } + }, + onMoveEnd() { + if (isOnColorArea.current) { + moveHandler.onMoveEnd(); + } + } + }); + + let onThumbDown = (id: number | null) => { + if (!state.isDragging) { + currentPointer.current = id; + focusInput(); + state.setDragging(true); + if (typeof PointerEvent !== 'undefined') { + addGlobalListener(window, 'pointerup', onThumbUp, false); + } else { + addGlobalListener(window, 'mouseup', onThumbUp, false); + addGlobalListener(window, 'touchend', onThumbUp, false); + } + } + }; + + let onThumbUp = (e) => { + let id = e.pointerId ?? e.changedTouches?.[0].identifier; + if (id === currentPointer.current) { + focusInput(); + state.setDragging(false); + currentPointer.current = undefined; + isOnColorArea.current = false; + + if (typeof PointerEvent !== 'undefined') { + removeGlobalListener(window, 'pointerup', onThumbUp, false); + } else { + removeGlobalListener(window, 'mouseup', onThumbUp, false); + removeGlobalListener(window, 'touchend', onThumbUp, false); + } + } + }; + + let onColorAreaDown = (colorArea: Element, id: number | null, clientX: number, clientY: number) => { + let rect = colorArea.getBoundingClientRect(); + let {width, height} = rect; + let x = (clientX - rect.x) / width; + let y = (clientY - rect.y) / height; + if (direction === 'rtl') { + x = 1 - x; + } + if (x >= 0 && x <= 1 && y >= 0 && y <= 1 && !state.isDragging && currentPointer.current === undefined) { + isOnColorArea.current = true; + currentPointer.current = id; + state.setColorFromPoint(x, y); + + focusInput(); + state.setDragging(true); + + if (typeof PointerEvent !== 'undefined') { + addGlobalListener(window, 'pointerup', onColorAreaUp, false); + } else { + addGlobalListener(window, 'mouseup', onColorAreaUp, false); + addGlobalListener(window, 'touchend', onColorAreaUp, false); + } + } + }; + + let onColorAreaUp = (e) => { + let id = e.pointerId ?? e.changedTouches?.[0].identifier; + if (isOnColorArea.current && id === currentPointer.current) { + isOnColorArea.current = false; + currentPointer.current = undefined; + state.setDragging(false); + focusInput(); + + if (typeof PointerEvent !== 'undefined') { + removeGlobalListener(window, 'pointerup', onColorAreaUp, false); + } else { + removeGlobalListener(window, 'mouseup', onColorAreaUp, false); + removeGlobalListener(window, 'touchend', onColorAreaUp, false); + } + } + }; + + let colorAreaInteractions = isDisabled ? {} : mergeProps({ + ...(typeof PointerEvent !== 'undefined' ? { + onPointerDown: (e: React.PointerEvent) => { + if (e.pointerType === 'mouse' && (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey)) { + return; + } + onColorAreaDown(e.currentTarget, e.pointerId, e.clientX, e.clientY); + }} : { + onMouseDown: (e: React.MouseEvent) => { + if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey) { + return; + } + onColorAreaDown(e.currentTarget, undefined, e.clientX, e.clientY); + }, + onTouchStart: (e: React.TouchEvent) => { + onColorAreaDown(e.currentTarget, e.changedTouches[0].identifier, e.changedTouches[0].clientX, e.changedTouches[0].clientY); + } + }) + }, movePropsContainer); + + let thumbInteractions = isDisabled ? {} : mergeProps({ + ...(typeof PointerEvent !== 'undefined' ? { + onPointerDown: (e: React.PointerEvent) => { + if (e.pointerType === 'mouse' && (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey)) { + return; + } + onThumbDown(e.pointerId); + }} : { + onMouseDown: (e: React.MouseEvent) => { + if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey) { + return; + } + onThumbDown(undefined); + }, + onTouchStart: (e: React.TouchEvent) => { + onThumbDown(e.changedTouches[0].identifier); + } + }) + }, movePropsThumb, keyboardProps); + + let {x, y} = stateRef.current.getThumbPosition(); + + if (direction === 'rtl') { + x = 1 - x; + } + + let inputLabellingProps = useLabels({ + ...props, + 'aria-label': `${state.value.getChannelName(xChannel, locale)} / ${state.value.getChannelName(yChannel, locale)}` + }); + + let colorAriaLabellingProps = useLabels(props); + + let getValueTitle = () => [ + `${state.value.getChannelName('red', locale)}: ${state.value.formatChannelValue('red', locale)}`, + `${state.value.getChannelName('green', locale)}: ${state.value.formatChannelValue('green', locale)}`, + `${state.value.getChannelName('blue', locale)}: ${state.value.formatChannelValue('blue', locale)}` + ].join(', '); + + let ariaRoleDescription = messages.getStringForLocale('twoDimensionalSlider', locale); + + let {visuallyHiddenProps} = useVisuallyHidden({style: { + opacity: '0.0001', + width: '100%', + height: '100%', + pointerEvents: 'none' + }}); + + return { + xChannel, + yChannel, + zChannel, + x, + y, + colorAreaProps: { + ...colorAriaLabellingProps, + ...colorAreaInteractions, + role: 'group' + }, + gradientProps: {}, + thumbProps: { + ...thumbInteractions + }, + xInputProps: { + ...inputLabellingProps, + ...visuallyHiddenProps, + type: 'range', + min: state.value.getChannelRange(xChannel).minValue, + max: state.value.getChannelRange(xChannel).maxValue, + step: xChannelStep, + 'aria-roledescription': ariaRoleDescription, + 'aria-valuetext': [ + `${state.value.getChannelName(xChannel, locale)}: ${state.value.formatChannelValue(xChannel, locale)}`, + `${state.value.getChannelName(yChannel, locale)}: ${state.value.formatChannelValue(yChannel, locale)}` + ].join(', '), + title: getValueTitle(), + disabled: isDisabled, + value: state.value.getChannelValue(xChannel), + tabIndex: 0, + onChange: (e: ChangeEvent) => { + state.setXValue(parseFloat(e.target.value)); + } + }, + yInputProps: { + ...inputLabellingProps, + ...visuallyHiddenProps, + type: 'range', + min: state.value.getChannelRange(yChannel).minValue, + max: state.value.getChannelRange(yChannel).maxValue, + step: yChannelStep, + 'aria-roledescription': ariaRoleDescription, + 'aria-valuetext': [ + `${state.value.getChannelName(yChannel, locale)}: ${state.value.formatChannelValue(yChannel, locale)}`, + `${state.value.getChannelName(xChannel, locale)}: ${state.value.formatChannelValue(xChannel, locale)}` + ].join(', '), + 'aria-orientation': 'vertical', + title: getValueTitle(), + disabled: isDisabled, + value: state.value.getChannelValue(yChannel), + tabIndex: -1, + onChange: (e: ChangeEvent) => { + state.setYValue(parseFloat(e.target.value)); + } + } + }; +} diff --git a/packages/@react-spectrum/color/src/ColorArea.tsx b/packages/@react-spectrum/color/src/ColorArea.tsx new file mode 100644 index 00000000000..e9eb5a4d37c --- /dev/null +++ b/packages/@react-spectrum/color/src/ColorArea.tsx @@ -0,0 +1,199 @@ +/* + * Copyright 2021 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, dimensionValue, useFocusableRef, useStyleProps} from '@react-spectrum/utils'; +import {ColorThumb} from './ColorThumb'; +import {FocusableRef} from '@react-types/shared'; +import {mergeProps} from '@react-aria/utils'; +import React, {CSSProperties, ReactElement, useRef} from 'react'; +import {SpectrumColorAreaProps} from '@react-types/color'; +import styles from '@adobe/spectrum-css-temp/components/colorarea/vars.css'; +import {useColorArea} from '@react-aria/color'; +import {useColorAreaState} from '@react-stately/color'; +import {useFocusRing} from '@react-aria/focus'; +import {useLocale} from '@react-aria/i18n'; +import {useProviderProps} from '@react-spectrum/provider'; + +function ColorArea(props: SpectrumColorAreaProps, ref: FocusableRef) { + props = useProviderProps(props); + + let {isDisabled} = props; + let size = props.size && dimensionValue(props.size); + let {styleProps} = useStyleProps(props); + + let xInputRef = useRef(null); + let yInputRef = useRef(null); + let containerRef = useFocusableRef(ref, xInputRef); + + let state = useColorAreaState(props); + + let { + xChannel, + yChannel, + zChannel, + x, + y, + colorAreaProps, + gradientProps, + xInputProps, + yInputProps, + thumbProps + } = useColorArea(props, state, xInputRef, yInputRef, containerRef); + let {direction} = useLocale(); + let {colorAreaStyleProps, gradientStyleProps, thumbStyleProps} = useGradients({direction, state, xChannel, yChannel, zChannel, x, y}); + + let {focusProps, isFocusVisible} = useFocusRing(); + + return ( +
+
+ +
+ + +
+
+
+ ); +} + +let _ColorArea = React.forwardRef(ColorArea) as (props: SpectrumColorAreaProps & {ref?: FocusableRef}) => ReactElement; +export {_ColorArea as ColorArea}; + +interface Gradients { + colorAreaStyleProps: { + style: CSSProperties + }, + gradientStyleProps: { + style: CSSProperties + }, + thumbStyleProps: { + style: CSSProperties + } +} + +// this function looks scary, but it's actually pretty quick, just generates some strings +function useGradients({direction, state, zChannel, xChannel, yChannel, x = 0, y = 0}): Gradients { + + let orientation = ['top', direction === 'rtl' ? 'left' : 'right']; + let dir: boolean | number = 0; + let background = {colorAreaStyles: {}, gradientStyles: {}}; + let zValue = state.value.getChannelValue(zChannel); + let maskImage; + switch (zChannel) { + case 'red': { + dir = xChannel === 'green'; + maskImage = `linear-gradient(to ${orientation[Number(!dir)]}, transparent, #000)`; + background.colorAreaStyles = { + /* the background represents the green channel as a linear gradient from min to max, + with the blue channel minimized, adjusted by the red channel value. */ + backgroundImage: `linear-gradient(to ${orientation[Number(dir)]},rgb(${zValue},0,0),rgb(${zValue},255,0))` + }; + background.gradientStyles = { + /* the foreground represents the green channel as a linear gradient from min to max, + with the blue channel maximized, adjusted by the red channel value. */ + backgroundImage: `linear-gradient(to ${orientation[Number(dir)]},rgb(${zValue},0,255),rgb(${zValue},255,255))`, + /* the foreground gradient is masked by a perpendicular linear gradient from black to white */ + 'WebkitMaskImage': maskImage, + maskImage + }; + break; + } + case 'green': { + dir = xChannel === 'red'; + maskImage = `linear-gradient(to ${orientation[Number(!dir)]}, transparent, #000)`; + background.colorAreaStyles = { + /* the background represents the red channel as a linear gradient from min to max, + with the blue channel minimized, adjusted by the green channel value. */ + backgroundImage: `linear-gradient(to ${orientation[Number(dir)]},rgb(0,${zValue},0),rgb(255,${zValue},0))` + }; + background.gradientStyles = { + /* the foreground represents the red channel as a linear gradient from min to max, + with the blue channel maximized, adjusted by the green channel value. */ + backgroundImage: `linear-gradient(to ${orientation[Number(dir)]},rgb(0,${zValue},255),rgb(255,${zValue},255))`, + /* the foreground gradient is masked by a perpendicular linear gradient from black to white */ + 'WebkitMaskImage': maskImage, + maskImage + }; + break; + } + case 'blue': { + dir = xChannel === 'red'; + maskImage = `linear-gradient(to ${orientation[Number(!dir)]}, transparent, #000)`; + background.colorAreaStyles = { + /* the background represents the red channel as a linear gradient from min to max, + with the green channel minimized, adjusted by the blue channel value. */ + backgroundImage: `linear-gradient(to ${orientation[Number(dir)]},rgb(0,0,${zValue}),rgb(255,0,${zValue}))` + }; + background.gradientStyles = { + /* the foreground represents the red channel as a linear gradient from min to max, + with the green channel maximized, adjusted by the blue channel value. */ + backgroundImage: `linear-gradient(to ${orientation[Number(dir)]},rgb(0,255,${zValue}),rgb(255,255,${zValue}))`, + /* the foreground gradient is masked by a perpendicular linear gradient from black to white */ + 'WebkitMaskImage': maskImage, + maskImage + }; + break; + } + } + + return { + colorAreaStyleProps: { + style: { + position: 'relative', + touchAction: 'none', + ...background.colorAreaStyles + } + }, + gradientStyleProps: { + style: { + touchAction: 'none', + ...background.gradientStyles + } + }, + thumbStyleProps: { + style: { + position: 'absolute', + left: `${x * 100}%`, + top: `${y * 100}%`, + transform: 'translate(0%, 0%)', + touchAction: 'none' + } + } + }; +} diff --git a/packages/@react-spectrum/color/src/ColorThumb.tsx b/packages/@react-spectrum/color/src/ColorThumb.tsx index 14581d265d4..09cf6eb010f 100644 --- a/packages/@react-spectrum/color/src/ColorThumb.tsx +++ b/packages/@react-spectrum/color/src/ColorThumb.tsx @@ -33,6 +33,7 @@ function ColorThumb(props: ColorThumbProps) { let patternId = useId(); let valueCSS = value.toString('css'); + console.log('otherProps', otherProps) return (
diff --git a/packages/@react-spectrum/color/src/index.ts b/packages/@react-spectrum/color/src/index.ts index 54569da6a5d..d87f973282f 100644 --- a/packages/@react-spectrum/color/src/index.ts +++ b/packages/@react-spectrum/color/src/index.ts @@ -12,6 +12,7 @@ /// +export * from './ColorArea'; export * from './ColorWheel'; export * from './ColorSlider'; export * from './ColorField'; diff --git a/packages/@react-spectrum/color/stories/ColorArea.stories.tsx b/packages/@react-spectrum/color/stories/ColorArea.stories.tsx new file mode 100644 index 00000000000..7ed0af23899 --- /dev/null +++ b/packages/@react-spectrum/color/stories/ColorArea.stories.tsx @@ -0,0 +1,75 @@ +/* + * 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 {ColorArea, ColorSlider} from '../'; +import {ColorChannel, SpectrumColorAreaProps} from '@react-types/color'; +import {Flex} from '@adobe/react-spectrum'; +import {parseColor} from '@react-stately/color'; +import React, {useState} from 'react'; +import {storiesOf} from '@storybook/react'; +import {Text} from '@react-spectrum/text'; + +let RGB: Set = new Set(['red', 'green', 'blue']); +let difference = (a, b): Set => new Set([...a].filter(x => !b.has(x))); + +function ColorAreaExample(props: SpectrumColorAreaProps) { + let {xChannel, yChannel} = props; + let channels = new Set([xChannel, yChannel]); + let zChannel: ColorChannel = difference(RGB, channels).keys().next().value as ColorChannel; + let [color, setColor] = useState(parseColor('#ff00ff')); + return (
+ + + { + action('change')(e); + setColor(e); + }} + xChannel={xChannel} + yChannel={yChannel} /> + + + +
+ {color.toString('hex')} + + +
); +} + +storiesOf('ColorArea', module) + .add( + 'RGB xChannel="blue", yChannel="green"', + () => + ) + .add( + 'RGB xChannel="green", yChannel="blue"', + () => + ) + .add( + 'RGB xChannel="blue", yChannel="red"', + () => + ) + .add( + 'RGB xChannel="red", yChannel="blue"', + () => + ) + .add( + 'RGB xChannel="red", yChannel="green"', + () => + ) + .add( + 'RGB xChannel="green", yChannel="red"', + () => + ); diff --git a/packages/@react-stately/color/src/Color.ts b/packages/@react-stately/color/src/Color.ts index 3d6dcb1c95e..a08256f98fc 100644 --- a/packages/@react-stately/color/src/Color.ts +++ b/packages/@react-stately/color/src/Color.ts @@ -29,6 +29,14 @@ export function parseColor(value: string): IColor { throw new Error('Invalid color value: ' + value); } +export function normalizeColor(v: string | Color) { + if (typeof v === 'string') { + return parseColor(v); + } else { + return v; + } +} + abstract class Color implements IColor { abstract toFormat(format: ColorFormat): IColor; abstract toString(format: ColorFormat | 'css'): string; diff --git a/packages/@react-stately/color/src/index.ts b/packages/@react-stately/color/src/index.ts index a8336f72515..0721490e23f 100644 --- a/packages/@react-stately/color/src/index.ts +++ b/packages/@react-stately/color/src/index.ts @@ -11,6 +11,7 @@ */ export * from './Color'; +export * from './useColorAreaState'; export * from './useColorSliderState'; export * from './useColorWheelState'; export * from './useColorFieldState'; diff --git a/packages/@react-stately/color/src/useColorAreaState.ts b/packages/@react-stately/color/src/useColorAreaState.ts new file mode 100644 index 00000000000..fe32ee1252b --- /dev/null +++ b/packages/@react-stately/color/src/useColorAreaState.ts @@ -0,0 +1,219 @@ +/* + * 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 {clamp, snapValueToStep} from '@react-aria/utils'; +import {Color, ColorAreaProps, ColorChannel} from '@react-types/color'; +import {normalizeColor, parseColor} from './Color'; +import {useControlledState} from '@react-stately/utils'; +import {useRef, useState} from 'react'; + +export interface ColorAreaState { + /** The current color value displayed by the color area. */ + readonly value: Color, + /** Sets the current color value. If a string is passed, it will be parsed to a Color. */ + setValue(value: string | Color): void, + + /** The current value of the horizontal axis channel displayed by the color area. */ + xValue: number, + /** Sets the value for the horizontal axis channel displayed by the color area, and triggers `onChange`. */ + setXValue(value: number): void, + + /** The current value of the vertical axis channel displayed by the color area. */ + yValue: number, + /** Sets the value for the vertical axis channel displayed by the color area, and triggers `onChange`. */ + setYValue(value: number): void, + + /** Sets the x and y channels of the current color value based on a percentage of the width and height of the color area, and triggers `onChange`. */ + setColorFromPoint(x: number, y: number): void, + /** Returns the coordinates of the thumb relative to the upper left corner of the color area as a percentage. */ + getThumbPosition(): {x: number, y: number}, + + /** Increments the value of the horizontal axis channel by the given amount (defaults to 1). */ + incrementX(minStepSize?: number): void, + /** Decrements the value of the horizontal axis channel by the given amount (defaults to 1). */ + decrementX(minStepSize?: number): void, + + /** Increments the value of the vertical axis channel by the given amount (defaults to 1). */ + incrementY(minStepSize?: number): void, + /** Decrements the value of the vertical axis channel by the given amount (defaults to 1). */ + decrementY(minStepSize?: number): void, + + /** Whether the color area is currently being dragged. */ + readonly isDragging: boolean, + /** Sets whether the color area is being dragged. */ + setDragging(value: boolean): void, + + /** Returns the xChannel, yChannel and zChannel names based on the color value. */ + getChannels(): {xChannel: ColorChannel, yChannel: ColorChannel, zChannel: ColorChannel}, + + /** Returns the color that should be displayed in the color area thumb instead of `value`. */ + getDisplayColor(): Color +} + +const DEFAULT_COLOR = parseColor('hsb(0, 100%, 100%)'); + +/** + * Provides state management for a color area component. + * Color area allows users to adjust two channels of an HSL, HSB or RGB color value against a two-dimensional gradient background. + */ +export function useColorAreaState(props: ColorAreaProps): ColorAreaState { + let {value, defaultValue, xChannel, yChannel, onChange, onChangeEnd, xChannelStep = 1, yChannelStep = 1} = props; + + if (!value && !defaultValue) { + defaultValue = DEFAULT_COLOR; + } + + let [color, setColor] = useControlledState(value && normalizeColor(value), defaultValue && normalizeColor(defaultValue), onChange); + let valueRef = useRef(color); + valueRef.current = color; + + let getChannels = () => { + // determine the color space from the color value + let zChannel: ColorChannel; + let xyChannels: Array; + + if (!xChannel) { + switch (yChannel) { + case 'red': + case 'green': + xChannel = 'blue'; + break; + case 'blue': + xChannel = 'red'; + break; + default: + xChannel = 'blue'; + yChannel = 'green'; + } + } else if (!yChannel) { + switch (xChannel) { + case 'red': + yChannel = 'green'; + break; + case 'blue': + yChannel = 'red'; + break; + default: + xChannel = 'blue'; + yChannel = 'green'; + } + } + xyChannels = [xChannel, yChannel]; + if (xyChannels.includes('red')) { + zChannel = xyChannels.includes('green') ? 'blue' : 'green'; + } else if (xyChannels.includes('green')) { + zChannel = xyChannels.includes('blue') ? 'red' : 'blue'; + } else if (xyChannels.includes('blue')) { + zChannel = xyChannels.includes('green') ? 'red' : 'green'; + } + + return {xChannel, yChannel, zChannel}; + }; + + let channels = getChannels(); + if (!xChannel || !yChannel) { + xChannel = channels.xChannel; + yChannel = channels.yChannel; + } + + let [isDragging, setDragging] = useState(false); + let isDraggingRef = useRef(false).current; + + let xValue = color.getChannelValue(xChannel); + let yValue = color.getChannelValue(yChannel); + let setXValue = (v: number) => setColor(color.withChannelValue(xChannel, v)); + let setYValue = (v: number) => setColor(color.withChannelValue(yChannel, v)); + + return { + value: color, + setValue(value) { + let c = normalizeColor(value); + valueRef.current = c; + setColor(c); + }, + xValue, + setXValue, + yValue, + setYValue, + setColorFromPoint(x: number, y: number) { + let {minValue: minValueX, maxValue: maxValueX} = color.getChannelRange(xChannel); + let {minValue: minValueY, maxValue: maxValueY} = color.getChannelRange(yChannel); + let newXValue = minValueX + clamp(x, 0, 1) * (maxValueX - minValueX); + let newYValue = minValueY + (1 - clamp(y, 0, 1)) * (maxValueY - minValueY); + let newColor:Color; + if (newXValue !== xValue) { + // Round new value to multiple of step, clamp value between min and max + newXValue = snapValueToStep(newXValue, minValueX, maxValueX, xChannelStep); + newColor = color.withChannelValue(xChannel, newXValue); + } + if (newYValue !== yValue) { + // Round new value to multiple of step, clamp value between min and max + newYValue = snapValueToStep(newYValue, minValueY, maxValueY, yChannelStep); + newColor = (newColor || color).withChannelValue(yChannel, newYValue); + } + if (newColor) { + setColor(newColor); + } + }, + getThumbPosition() { + let {minValue, maxValue} = color.getChannelRange(xChannel); + let {minValue: minValueY, maxValue: maxValueY} = color.getChannelRange(yChannel); + let x = (xValue - minValue) / (maxValue - minValue); + let y = 1 - (yValue - minValueY) / (maxValueY - minValueY); + return {x, y}; + }, + incrementX(minStepSize: number = 0) { + let s = Math.max(minStepSize, xChannelStep); + let {maxValue} = color.getChannelRange(xChannel); + if (xValue < maxValue) { + setXValue(Math.min(xValue + s, maxValue)); + } + }, + incrementY(minStepSize: number = 0) { + let s = Math.max(minStepSize, yChannelStep); + let {maxValue} = color.getChannelRange(yChannel); + if (yValue < maxValue) { + setYValue(Math.min(yValue + s, maxValue)); + } + }, + decrementX(minStepSize: number = 0) { + let s = Math.max(minStepSize, xChannelStep); + let {minValue} = color.getChannelRange(xChannel); + if (xValue > minValue) { + setXValue(Math.max(xValue - s, minValue)); + } + }, + decrementY(minStepSize: number = 0) { + let s = Math.max(minStepSize, yChannelStep); + let {minValue} = color.getChannelRange(yChannel); + if (yValue > minValue) { + setYValue(Math.max(yValue - s, minValue)); + } + }, + setDragging(isDragging) { + let wasDragging = isDraggingRef; + isDraggingRef = isDragging; + + if (onChangeEnd && !isDragging && wasDragging) { + onChangeEnd(valueRef.current); + } + + console.log('setDragging', isDragging) + setDragging(isDragging); + }, + isDragging, + getChannels, + getDisplayColor() { + return color.withChannelValue('alpha', 1); + } + }; +} diff --git a/packages/@react-stately/color/src/useColorSliderState.ts b/packages/@react-stately/color/src/useColorSliderState.ts index 3f1f427fcc7..d6ce9e8ccd0 100644 --- a/packages/@react-stately/color/src/useColorSliderState.ts +++ b/packages/@react-stately/color/src/useColorSliderState.ts @@ -11,7 +11,7 @@ */ import {Color, ColorSliderProps} from '@react-types/color'; -import {parseColor} from './Color'; +import {normalizeColor, parseColor} from './Color'; import {SliderState, useSliderState} from '@react-stately/slider'; import {useControlledState} from '@react-stately/utils'; @@ -30,14 +30,6 @@ interface ColorSliderStateOptions extends ColorSliderProps { locale: string } -function normalizeColor(v: string | Color) { - if (typeof v === 'string') { - return parseColor(v); - } else { - return v; - } -} - /** * Provides state management for a color slider component. * Color sliders allow users to adjust an individual channel of a color value. diff --git a/packages/@react-types/color/src/index.d.ts b/packages/@react-types/color/src/index.d.ts index 44f013081d4..6a74a1b00e7 100644 --- a/packages/@react-types/color/src/index.d.ts +++ b/packages/@react-types/color/src/index.d.ts @@ -134,3 +134,32 @@ export interface SpectrumColorSliderProps extends AriaColorSliderProps, StylePro /** Whether the value label is displayed. True by default if there is a label, false by default if not. */ showValueLabel?: boolean } + +export interface ColorAreaProps extends ValueBase { + /** Color channel for the horizontal axis. */ + xChannel?: ColorChannel, + /** Color channel for the vertical axis. */ + yChannel?: ColorChannel, + /** Whether the ColorArea is disabled. */ + isDisabled?: boolean, + /** Handler that is called when the value changes, as the user drags. */ + onChange?: (value: Color) => void, + /** Handler that is called when the user stops dragging. */ + onChangeEnd?: (value: Color) => void, + /** + * The step value for the xChannel. + * @default 1 + */ + xChannelStep?: number, + /** + * The step value for the yChannel. + * @default 1 + */ + yChannelStep?: number +} + +export interface AriaColorAreaProps extends ColorAreaProps, DOMProps, AriaLabelingProps {} + +export interface SpectrumColorAreaProps extends AriaColorAreaProps, Omit { + size?: DimensionValue +} From c36c999ccf053324bfee120679f6dc651c1b60a5 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Wed, 20 Oct 2021 17:02:03 -0700 Subject: [PATCH 02/23] refactor aria back to stately --- .../@react-aria/color/src/useColorArea.ts | 26 +- .../@react-spectrum/color/src/ColorArea.tsx | 4 +- .../@react-spectrum/color/src/ColorThumb.tsx | 1 - .../color/stories/ColorArea.stories.tsx | 62 ++-- .../color/test/ColorArea.test.tsx | 349 ++++++++++++++++++ .../color/src/useColorAreaState.ts | 84 ++--- 6 files changed, 431 insertions(+), 95 deletions(-) create mode 100644 packages/@react-spectrum/color/test/ColorArea.test.tsx diff --git a/packages/@react-aria/color/src/useColorArea.ts b/packages/@react-aria/color/src/useColorArea.ts index 04f1e5e55fc..adeeed73e5a 100644 --- a/packages/@react-aria/color/src/useColorArea.ts +++ b/packages/@react-aria/color/src/useColorArea.ts @@ -63,11 +63,7 @@ function maxMinOrZero(value1: number, value2: number): number { */ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, inputXRef: RefObject, inputYRef: RefObject, containerRef: RefObject): ColorAreaAria { let { - isDisabled, - xChannel, - yChannel, - xChannelStep, - yChannelStep + isDisabled } = props; let {addGlobalListener, removeGlobalListener} = useGlobalListeners(); @@ -84,20 +80,9 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i let stateRef = useRef(null); stateRef.current = state; - let channels = stateRef.current.getChannels(); - if (!xChannel || !yChannel) { - xChannel = channels.xChannel; - yChannel = channels.yChannel; - } - let zChannel = channels.zChannel; - - if (!xChannelStep) { - xChannelStep = stateRef.current.value.getChannelRange(xChannel).step; - } - - if (!yChannelStep) { - yChannelStep = stateRef.current.value.getChannelRange(yChannel).step; - } + let {xChannel, yChannel, zChannel} = stateRef.current.channels; + let xChannelStep = stateRef.current.xChannelStep; + let yChannelStep = stateRef.current.xChannelStep; let currentPosition = useRef<{x: number, y: number}>(null); @@ -365,9 +350,6 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i }}); return { - xChannel, - yChannel, - zChannel, x, y, colorAreaProps: { diff --git a/packages/@react-spectrum/color/src/ColorArea.tsx b/packages/@react-spectrum/color/src/ColorArea.tsx index e9eb5a4d37c..4d70d95177d 100644 --- a/packages/@react-spectrum/color/src/ColorArea.tsx +++ b/packages/@react-spectrum/color/src/ColorArea.tsx @@ -36,10 +36,8 @@ function ColorArea(props: SpectrumColorAreaProps, ref: FocusableRef diff --git a/packages/@react-spectrum/color/stories/ColorArea.stories.tsx b/packages/@react-spectrum/color/stories/ColorArea.stories.tsx index 7ed0af23899..39fe5ad63b7 100644 --- a/packages/@react-spectrum/color/stories/ColorArea.stories.tsx +++ b/packages/@react-spectrum/color/stories/ColorArea.stories.tsx @@ -14,11 +14,23 @@ import {action} from '@storybook/addon-actions'; import {ColorArea, ColorSlider} from '../'; import {ColorChannel, SpectrumColorAreaProps} from '@react-types/color'; import {Flex} from '@adobe/react-spectrum'; +import {Meta, Story} from '@storybook/react'; import {parseColor} from '@react-stately/color'; import React, {useState} from 'react'; -import {storiesOf} from '@storybook/react'; import {Text} from '@react-spectrum/text'; + +const meta: Meta = { + title: 'ColorArea', + component: ColorArea +}; + +export default meta; + +const Template: Story = (args) => ( + +); + let RGB: Set = new Set(['red', 'green', 'blue']); let difference = (a, b): Set => new Set([...a].filter(x => !b.has(x))); @@ -48,28 +60,26 @@ function ColorAreaExample(props: SpectrumColorAreaProps) {
); } -storiesOf('ColorArea', module) - .add( - 'RGB xChannel="blue", yChannel="green"', - () => - ) - .add( - 'RGB xChannel="green", yChannel="blue"', - () => - ) - .add( - 'RGB xChannel="blue", yChannel="red"', - () => - ) - .add( - 'RGB xChannel="red", yChannel="blue"', - () => - ) - .add( - 'RGB xChannel="red", yChannel="green"', - () => - ) - .add( - 'RGB xChannel="green", yChannel="red"', - () => - ); +export let XBlueYGreen = Template.bind({}); +XBlueYGreen.title = 'RGB xChannel="blue", yChannel="green"'; +XBlueYGreen.args = {xChannel: 'blue', yChannel: 'green'}; + +export let XGreenYBlue = Template.bind({}); +XGreenYBlue.title = 'RGB xChannel="green", yChannel="blue"'; +XGreenYBlue.args = {xChannel: 'green', yChannel: 'blue'}; + +export let XBlueYRed = Template.bind({}); +XBlueYRed.title = 'RGB xChannel="blue", yChannel="red"'; +XBlueYRed.args = {xChannel: 'blue', yChannel: 'red'}; + +export let XRedYBlue = Template.bind({}); +XRedYBlue.title = 'GB xChannel="red", yChannel="blue"'; +XRedYBlue.args = {xChannel: 'red', yChannel: 'blue'}; + +export let XRedYGreen = Template.bind({}); +XRedYGreen.title = 'RGB xChannel="red", yChannel="green"'; +XRedYGreen.args = {xChannel: 'red', yChannel: 'green'}; + +export let XGreenYRed = Template.bind({}); +XGreenYRed.title = 'RGB xChannel="green", yChannel="red"'; +XGreenYRed.args = {xChannel: 'green', yChannel: 'red'}; diff --git a/packages/@react-spectrum/color/test/ColorArea.test.tsx b/packages/@react-spectrum/color/test/ColorArea.test.tsx new file mode 100644 index 00000000000..f6bff61b53e --- /dev/null +++ b/packages/@react-spectrum/color/test/ColorArea.test.tsx @@ -0,0 +1,349 @@ +/* + * 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 {ColorArea} from '../'; +import {installMouseEvent, installPointerEvent} from '@react-spectrum/test-utils'; +import {parseColor} from '@react-stately/color'; +import React from 'react'; +import userEvent from '@testing-library/user-event'; +import {XBlueYGreen as DefaultColorArea} from '../stories/ColorArea.stories'; + +const SIZE = 160; +const CENTER = SIZE / 2; +const THUMB_RADIUS = 68; + +const getBoundingClientRect = () => ({ + width: SIZE, height: SIZE, + x: 0, y: 0, + top: 0, left: 0, + bottom: SIZE, right: SIZE, + toJSON() { return this; } +}); + +describe('ColorArea', () => { + let onChangeSpy = jest.fn(); + let onChangeEndSpy = jest.fn(); + + beforeAll(() => { + jest.spyOn(window.HTMLElement.prototype, 'offsetWidth', 'get').mockImplementation(() => SIZE); + jest.useFakeTimers('modern'); + }); + afterAll(() => { + // @ts-ignore + window.HTMLElement.prototype.offsetWidth.mockReset(); + jest.useRealTimers(); + }); + + afterEach(() => { + // for restoreTextSelection + jest.runAllTimers(); + onChangeSpy.mockClear(); + onChangeEndSpy.mockClear(); + }); + + describe('attributes', () => { + it.only('sets input props', () => { + let {getAllByRole} = render(); + let sliders = getAllByRole('slider'); + + expect(sliders.length).toBe(3); + + // + // expect(slider).toHaveAttribute('type', 'range'); + // expect(slider).toHaveAttribute('aria-label', 'Hue'); + // expect(slider).toHaveAttribute('min', '0'); + // expect(slider).toHaveAttribute('max', '360'); + // expect(slider).toHaveAttribute('step', '1'); + // expect(slider).toHaveAttribute('aria-valuetext', '0°'); + }); + + it('the slider is focusable', () => { + let {getAllByRole, getByRole} = render(
+ + + +
); + let slider = getByRole('slider'); + let [buttonA, buttonB] = getAllByRole('button'); + + userEvent.tab(); + expect(document.activeElement).toBe(buttonA); + userEvent.tab(); + expect(document.activeElement).toBe(slider); + userEvent.tab(); + expect(document.activeElement).toBe(buttonB); + userEvent.tab({shift: true}); + expect(document.activeElement).toBe(slider); + }); + + it('disabled', () => { + let {getAllByRole, getByRole} = render(
+ + + +
); + let slider = getByRole('slider'); + let [buttonA, buttonB] = getAllByRole('button'); + expect(slider).toHaveAttribute('disabled'); + + userEvent.tab(); + expect(document.activeElement).toBe(buttonA); + userEvent.tab(); + expect(document.activeElement).toBe(buttonB); + userEvent.tab({shift: true}); + expect(document.activeElement).toBe(buttonA); + }); + + describe('labelling', () => { + it('should support a custom aria-label', () => { + let {getByRole} = render(); + let slider = getByRole('slider'); + + expect(slider).toHaveAttribute('aria-label', 'Color hue'); + expect(slider).not.toHaveAttribute('aria-labelledby'); + }); + + it('should support a custom aria-labelledby', () => { + let {getByRole} = render(); + let slider = getByRole('slider'); + + expect(slider).not.toHaveAttribute('aria-label'); + expect(slider).toHaveAttribute('aria-labelledby', 'label-id'); + }); + }); + }); + + describe('behaviors', () => { + describe('keyboard events', () => { + it('works', () => { + let defaultColor = parseColor('hsl(0, 100%, 50%)'); + let {getByRole} = render(); + let slider = getByRole('slider'); + act(() => { + slider.focus(); + }); + + fireEvent.keyDown(slider, {key: 'Right'}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 1).toString('hsla')); + expect(onChangeEndSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 1).toString('hsla')); + fireEvent.keyDown(slider, {key: 'Left'}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 0).toString('hsla')); + expect(onChangeEndSpy).toHaveBeenCalledTimes(2); + expect(onChangeEndSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 0).toString('hsla')); + }); + + it('doesn\'t work when disabled', () => { + let defaultColor = parseColor('hsl(0, 100%, 50%)'); + let {getByRole} = render(); + let slider = getByRole('slider'); + act(() => { + slider.focus(); + }); + + fireEvent.keyDown(slider, {key: 'Right'}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + fireEvent.keyDown(slider, {key: 'Left'}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + }); + + it('wraps around', () => { + let defaultColor = parseColor('hsl(0, 100%, 50%)'); + let {getByRole} = render(); + let slider = getByRole('slider'); + act(() => { + slider.focus(); + }); + + fireEvent.keyDown(slider, {key: 'Left'}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 359).toString('hsla')); + }); + + it('respects step', () => { + let defaultColor = parseColor('hsl(0, 100%, 50%)'); + let {getByRole} = render(); + let slider = getByRole('slider'); + act(() => { + slider.focus(); + }); + + fireEvent.keyDown(slider, {key: 'Right'}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 45).toString('hsla')); + fireEvent.keyDown(slider, {key: 'Left'}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 0).toString('hsla')); + }); + }); + + describe.each` + type | prepare | actions + ${'Mouse Events'} | ${installMouseEvent} | ${[ + (el, {pageX, pageY}) => fireEvent.mouseDown(el, {button: 0, pageX, pageY, clientX: pageX, clientY: pageY}), + (el, {pageX, pageY}) => fireEvent.mouseMove(el, {button: 0, pageX, pageY, clientX: pageX, clientY: pageY}), + (el, {pageX, pageY}) => fireEvent.mouseUp(el, {button: 0, pageX, pageY, clientX: pageX, clientY: pageY}) + ]} + ${'Pointer Events'} | ${installPointerEvent}| ${[ + (el, {pageX, pageY}) => fireEvent.pointerDown(el, {button: 0, pointerId: 1, pageX, pageY, clientX: pageX, clientY: pageY}), + (el, {pageX, pageY}) => fireEvent.pointerMove(el, {button: 0, pointerId: 1, pageX, pageY, clientX: pageX, clientY: pageY}), + (el, {pageX, pageY}) => fireEvent.pointerUp(el, {button: 0, pointerId: 1, pageX, pageY, clientX: pageX, clientY: pageY}) + ]} + ${'Touch Events'} | ${() => {}} | ${[ + (el, {pageX, pageY}) => fireEvent.touchStart(el, {changedTouches: [{identifier: 1, pageX, pageY, clientX: pageX, clientY: pageY}]}), + (el, {pageX, pageY}) => fireEvent.touchMove(el, {changedTouches: [{identifier: 1, pageX, pageY, clientX: pageX, clientY: pageY}]}), + (el, {pageX, pageY}) => fireEvent.touchEnd(el, {changedTouches: [{identifier: 1, pageX, pageY, clientX: pageX, clientY: pageY}]}) + ]} + `('$type', ({actions: [start, move, end], prepare}) => { + prepare(); + + it('dragging the thumb works', () => { + let defaultColor = parseColor('hsl(0, 100%, 50%)'); + let {container: _container, getByRole} = render(); + let slider = getByRole('slider'); + let thumb = slider.parentElement; + let container = _container.firstChild.firstChild as HTMLElement; + container.getBoundingClientRect = getBoundingClientRect; + + expect(document.activeElement).not.toBe(slider); + start(thumb, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(onChangeEndSpy).toHaveBeenCalledTimes(0); + expect(document.activeElement).toBe(slider); + + move(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 90).toString('hsla')); + expect(onChangeEndSpy).toHaveBeenCalledTimes(0); + expect(document.activeElement).toBe(slider); + + end(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 90).toString('hsla')); + expect(document.activeElement).toBe(slider); + }); + + it('dragging the thumb doesn\'t works when disabled', () => { + let defaultColor = parseColor('hsl(0, 100%, 50%)'); + let {container: _container, getByRole} = render(); + let slider = getByRole('slider'); + let container = _container.firstChild.firstChild as HTMLElement; + container.getBoundingClientRect = getBoundingClientRect; + let thumb = slider.parentElement; + + expect(document.activeElement).not.toBe(slider); + start(thumb, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(document.activeElement).not.toBe(slider); + + move(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(document.activeElement).not.toBe(slider); + + end(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(document.activeElement).not.toBe(slider); + }); + + it('dragging the thumb respects the step', () => { + let defaultColor = parseColor('hsl(0, 100%, 50%)'); + let {container: _container, getByRole} = render(); + let slider = getByRole('slider'); + let container = _container.firstChild.firstChild as HTMLElement; + let thumb = slider.parentElement; + container.getBoundingClientRect = getBoundingClientRect; + + start(thumb, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + move(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 120).toString('hsla')); + end(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + }); + + it('clicking and dragging on the track works', () => { + let defaultColor = parseColor('hsl(0, 100%, 50%)'); + let {container: _container, getByRole} = render(); + let slider = getByRole('slider'); + let thumb = slider.parentElement; + let container = _container.firstChild.firstChild as HTMLElement; + container.getBoundingClientRect = getBoundingClientRect; + + expect(document.activeElement).not.toBe(slider); + start(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 90).toString('hsla')); + expect(document.activeElement).toBe(slider); + + move(thumb, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 180).toString('hsla')); + expect(document.activeElement).toBe(slider); + + end(thumb, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(document.activeElement).toBe(slider); + }); + + it('clicking and dragging on the track doesn\'t work when disabled', () => { + let defaultColor = parseColor('hsl(0, 100%, 50%)'); + let {container: _container, getByRole} = render(); + let slider = getByRole('slider'); + let container = _container.firstChild.firstChild as HTMLElement; + container.getBoundingClientRect = getBoundingClientRect; + + expect(document.activeElement).not.toBe(slider); + start(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(document.activeElement).not.toBe(slider); + + move(container, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(document.activeElement).not.toBe(slider); + + end(container, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(document.activeElement).not.toBe(slider); + }); + + it('clicking and dragging on the track respects the step', () => { + let defaultColor = parseColor('hsl(0, 100%, 50%)'); + let {container: _container, getByRole} = render(); + let slider = getByRole('slider'); + let thumb = slider.parentElement; + let container = _container.firstChild.firstChild as HTMLElement; + container.getBoundingClientRect = getBoundingClientRect; + + start(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 120).toString('hsla')); + move(thumb, {pageX: CENTER, pageY: CENTER - THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 240).toString('hsla')); + end(thumb, {pageX: CENTER, pageY: CENTER - THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + }); + }); + }); +}); diff --git a/packages/@react-stately/color/src/useColorAreaState.ts b/packages/@react-stately/color/src/useColorAreaState.ts index fe32ee1252b..7e3af186c3c 100644 --- a/packages/@react-stately/color/src/useColorAreaState.ts +++ b/packages/@react-stately/color/src/useColorAreaState.ts @@ -60,7 +60,8 @@ export interface ColorAreaState { } const DEFAULT_COLOR = parseColor('hsb(0, 100%, 100%)'); - +const RGBSet: Set = new Set(['red', 'green', 'blue']); +let difference = (a: Set, b: Set): Set => new Set([...a].filter(x => !b.has(x))); /** * Provides state management for a color area component. * Color area allows users to adjust two channels of an HSL, HSB or RGB color value against a two-dimensional gradient background. @@ -76,55 +77,50 @@ export function useColorAreaState(props: ColorAreaProps): ColorAreaState { let valueRef = useRef(color); valueRef.current = color; - let getChannels = () => { - // determine the color space from the color value - let zChannel: ColorChannel; - let xyChannels: Array; - - if (!xChannel) { - switch (yChannel) { - case 'red': - case 'green': - xChannel = 'blue'; - break; - case 'blue': - xChannel = 'red'; - break; - default: - xChannel = 'blue'; - yChannel = 'green'; - } - } else if (!yChannel) { - switch (xChannel) { - case 'red': - yChannel = 'green'; - break; - case 'blue': - yChannel = 'red'; - break; - default: - xChannel = 'blue'; - yChannel = 'green'; - } + if (!xChannel) { + switch (yChannel) { + case 'red': + case 'green': + xChannel = 'blue'; + break; + case 'blue': + xChannel = 'red'; + break; + default: + xChannel = 'blue'; + yChannel = 'green'; } - xyChannels = [xChannel, yChannel]; - if (xyChannels.includes('red')) { - zChannel = xyChannels.includes('green') ? 'blue' : 'green'; - } else if (xyChannels.includes('green')) { - zChannel = xyChannels.includes('blue') ? 'red' : 'blue'; - } else if (xyChannels.includes('blue')) { - zChannel = xyChannels.includes('green') ? 'red' : 'green'; + } else if (!yChannel) { + switch (xChannel) { + case 'red': + yChannel = 'green'; + break; + case 'blue': + yChannel = 'red'; + break; + default: + xChannel = 'blue'; + yChannel = 'green'; } + } + let xyChannels: Set = new Set([xChannel, yChannel]); + let zChannel = difference(RGBSet, xyChannels).values().next().value as ColorChannel; + console.log('zChannel', zChannel) - return {xChannel, yChannel, zChannel}; - }; - - let channels = getChannels(); + let channels = {xChannel, yChannel, zChannel}; if (!xChannel || !yChannel) { xChannel = channels.xChannel; yChannel = channels.yChannel; } + if (!xChannelStep) { + xChannelStep = color.getChannelRange(xChannel).step; + } + + if (!yChannelStep) { + yChannelStep = color.getChannelRange(yChannel).step; + } + let [isDragging, setDragging] = useState(false); let isDraggingRef = useRef(false).current; @@ -134,6 +130,9 @@ export function useColorAreaState(props: ColorAreaProps): ColorAreaState { let setYValue = (v: number) => setColor(color.withChannelValue(yChannel, v)); return { + channels, + xChannelStep, + yChannelStep, value: color, setValue(value) { let c = normalizeColor(value); @@ -211,7 +210,6 @@ export function useColorAreaState(props: ColorAreaProps): ColorAreaState { setDragging(isDragging); }, isDragging, - getChannels, getDisplayColor() { return color.withChannelValue('alpha', 1); } From dd5c47e85e1be3f728050fdca4c9655d9c647875 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Wed, 20 Oct 2021 17:49:43 -0700 Subject: [PATCH 03/23] more refactoring --- .../@react-aria/color/src/useColorArea.ts | 9 +-- .../@react-spectrum/color/src/ColorArea.tsx | 14 ++-- packages/@react-stately/color/src/Color.ts | 2 +- .../color/src/useColorAreaState.ts | 74 +++++++++---------- 4 files changed, 47 insertions(+), 52 deletions(-) diff --git a/packages/@react-aria/color/src/useColorArea.ts b/packages/@react-aria/color/src/useColorArea.ts index adeeed73e5a..8cb23b5cb8b 100644 --- a/packages/@react-aria/color/src/useColorArea.ts +++ b/packages/@react-aria/color/src/useColorArea.ts @@ -80,7 +80,7 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i let stateRef = useRef(null); stateRef.current = state; - let {xChannel, yChannel, zChannel} = stateRef.current.channels; + let {xChannel, yChannel} = stateRef.current.channels; let xChannelStep = stateRef.current.xChannelStep; let yChannelStep = stateRef.current.xChannelStep; @@ -321,11 +321,6 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i }) }, movePropsThumb, keyboardProps); - let {x, y} = stateRef.current.getThumbPosition(); - - if (direction === 'rtl') { - x = 1 - x; - } let inputLabellingProps = useLabels({ ...props, @@ -350,8 +345,6 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i }}); return { - x, - y, colorAreaProps: { ...colorAriaLabellingProps, ...colorAreaInteractions, diff --git a/packages/@react-spectrum/color/src/ColorArea.tsx b/packages/@react-spectrum/color/src/ColorArea.tsx index 4d70d95177d..807e3987b14 100644 --- a/packages/@react-spectrum/color/src/ColorArea.tsx +++ b/packages/@react-spectrum/color/src/ColorArea.tsx @@ -38,8 +38,6 @@ function ColorArea(props: SpectrumColorAreaProps, ref: FocusableRef = new Set(['red', 'green', 'blue']); -let difference = (a: Set, b: Set): Set => new Set([...a].filter(x => !b.has(x))); +let difference = (a: Set, b: Set): Set => new Set([...a].filter(x => !b.has(x))); /** * Provides state management for a color area component. * Color area allows users to adjust two channels of an HSL, HSB or RGB color value against a two-dimensional gradient background. @@ -77,41 +79,38 @@ export function useColorAreaState(props: ColorAreaProps): ColorAreaState { let valueRef = useRef(color); valueRef.current = color; - if (!xChannel) { - switch (yChannel) { - case 'red': - case 'green': - xChannel = 'blue'; - break; - case 'blue': - xChannel = 'red'; - break; - default: - xChannel = 'blue'; - yChannel = 'green'; - } - } else if (!yChannel) { - switch (xChannel) { - case 'red': - yChannel = 'green'; - break; - case 'blue': - yChannel = 'red'; - break; - default: - xChannel = 'blue'; - yChannel = 'green'; + let channels = useMemo(() => { + if (!xChannel) { + switch (yChannel) { + case 'red': + case 'green': + xChannel = 'blue'; + break; + case 'blue': + xChannel = 'red'; + break; + default: + xChannel = 'blue'; + yChannel = 'green'; + } + } else if (!yChannel) { + switch (xChannel) { + case 'red': + yChannel = 'green'; + break; + case 'blue': + yChannel = 'red'; + break; + default: + xChannel = 'blue'; + yChannel = 'green'; + } } - } - let xyChannels: Set = new Set([xChannel, yChannel]); - let zChannel = difference(RGBSet, xyChannels).values().next().value as ColorChannel; - console.log('zChannel', zChannel) - - let channels = {xChannel, yChannel, zChannel}; - if (!xChannel || !yChannel) { - xChannel = channels.xChannel; - yChannel = channels.yChannel; - } + let xyChannels: Set = new Set([xChannel, yChannel]); + let zChannel = difference(RGBSet, xyChannels).values().next().value; + + return {xChannel, yChannel, zChannel}; + }, [xChannel, yChannel]); if (!xChannelStep) { xChannelStep = color.getChannelRange(xChannel).step; @@ -206,7 +205,6 @@ export function useColorAreaState(props: ColorAreaProps): ColorAreaState { onChangeEnd(valueRef.current); } - console.log('setDragging', isDragging) setDragging(isDragging); }, isDragging, From fc3de3aa74e547815814912925416ef9ec00c1f4 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Wed, 20 Oct 2021 18:34:19 -0700 Subject: [PATCH 04/23] refactor step logic and defaults --- .../@react-aria/color/src/useColorArea.ts | 53 +- .../@react-spectrum/color/src/ColorArea.tsx | 6 +- .../color/stories/ColorArea.stories.tsx | 9 +- .../color/test/ColorArea.test.tsx | 591 +++++++++--------- packages/@react-stately/color/package.json | 5 +- .../color/src/useColorAreaState.ts | 79 ++- packages/@react-types/color/src/index.d.ts | 10 +- 7 files changed, 363 insertions(+), 390 deletions(-) diff --git a/packages/@react-aria/color/src/useColorArea.ts b/packages/@react-aria/color/src/useColorArea.ts index 8cb23b5cb8b..b9a01efe13b 100644 --- a/packages/@react-aria/color/src/useColorArea.ts +++ b/packages/@react-aria/color/src/useColorArea.ts @@ -11,8 +11,8 @@ */ import {AriaColorAreaProps} from '@react-types/color'; -import {clamp, focusWithoutScrolling, mergeProps, useGlobalListeners, useLabels} from '@react-aria/utils'; import {ColorAreaState} from '@react-stately/color'; +import {focusWithoutScrolling, mergeProps, useGlobalListeners, useLabels} from '@react-aria/utils'; // @ts-ignore import intlMessages from '../intl/*.json'; import {MessageDictionary} from '@internationalized/message'; @@ -36,19 +36,6 @@ interface ColorAreaAria { yInputProps: InputHTMLAttributes } -const PERCENT_STEP_SIZE = 10; -const HUE_STEP_SIZE = 15; -const RGB_STEP_SIZE = 16; -const CHANNEL_STEP_SIZE = { - hue: HUE_STEP_SIZE, - saturation: PERCENT_STEP_SIZE, - brightness: PERCENT_STEP_SIZE, - lightness: PERCENT_STEP_SIZE, - red: RGB_STEP_SIZE, - green: RGB_STEP_SIZE, - blue: RGB_STEP_SIZE -}; - function maxMinOrZero(value1: number, value2: number): number { if (value1 === 0) { @@ -91,55 +78,25 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i if (!e.shiftKey && /^Arrow(?:Right|Left|Up|Down)$/.test(e.key)) { return; } - let stepSize = Math.max(xChannelStep, CHANNEL_STEP_SIZE[xChannel]); - let range = stateRef.current.value.getChannelRange(xChannel); switch (e.key) { case 'PageUp': case 'ArrowUp': - range = stateRef.current.value.getChannelRange(yChannel); - stepSize = Math.max(yChannelStep, CHANNEL_STEP_SIZE[yChannel]); - stateRef.current.setYValue( - clamp( - (Math.floor(stateRef.current.yValue / stepSize) + 1) * stepSize, - range.minValue, - range.maxValue - ) - ); + stateRef.current.incrementY(); focusedInputRef.current = inputYRef.current; break; case 'PageDown': case 'ArrowDown': - range = stateRef.current.value.getChannelRange(yChannel); - stepSize = Math.max(yChannelStep, CHANNEL_STEP_SIZE[yChannel]); - stateRef.current.setYValue( - clamp( - (Math.ceil(stateRef.current.yValue / stepSize) - 1) * stepSize, - range.minValue, - range.maxValue - ) - ); + stateRef.current.decrementY(); focusedInputRef.current = inputYRef.current; break; case 'Home': case 'ArrowLeft': - stateRef.current.setXValue( - clamp( - (Math[direction === 'rtl' ? 'floor' : 'ceil'](stateRef.current.xValue / stepSize) + (direction === 'rtl' ? 1 : -1)) * stepSize, - range.minValue, - range.maxValue - ) - ); + direction === 'rtl' ? stateRef.current.incrementY() : stateRef.current.decrementY(); focusedInputRef.current = inputXRef.current; break; case 'End': case 'ArrowRight': - stateRef.current.setXValue( - clamp( - (Math[direction === 'rtl' ? 'floor' : 'ceil'](stateRef.current.xValue / stepSize) + (direction === 'rtl' ? -1 : 1)) * stepSize, - range.minValue, - range.maxValue - ) - ); + direction === 'rtl' ? stateRef.current.decrementY() : stateRef.current.incrementY(); focusedInputRef.current = inputXRef.current; break; } diff --git a/packages/@react-spectrum/color/src/ColorArea.tsx b/packages/@react-spectrum/color/src/ColorArea.tsx index 807e3987b14..bc71fa2510e 100644 --- a/packages/@react-spectrum/color/src/ColorArea.tsx +++ b/packages/@react-spectrum/color/src/ColorArea.tsx @@ -36,7 +36,7 @@ function ColorArea(props: SpectrumColorAreaProps, ref: FocusableRef { action('change')(e); setColor(e); - }} - xChannel={xChannel} - yChannel={yChannel} /> + }} /> @@ -83,3 +82,7 @@ XRedYGreen.args = {xChannel: 'red', yChannel: 'green'}; export let XGreenYRed = Template.bind({}); XGreenYRed.title = 'RGB xChannel="green", yChannel="red"'; XGreenYRed.args = {xChannel: 'green', yChannel: 'red'}; + +export let XBlueYGreenStep16 = Template.bind({}); +XBlueYGreenStep16.title = 'RGB xChannel="blue", yChannel="green" step="16"'; +XBlueYGreenStep16.args = {...XBlueYGreen.args, xChannelStep: 16, yChannelStep: 16}; diff --git a/packages/@react-spectrum/color/test/ColorArea.test.tsx b/packages/@react-spectrum/color/test/ColorArea.test.tsx index f6bff61b53e..476ec6181f7 100644 --- a/packages/@react-spectrum/color/test/ColorArea.test.tsx +++ b/packages/@react-spectrum/color/test/ColorArea.test.tsx @@ -10,25 +10,25 @@ * governing permissions and limitations under the License. */ -import {act, fireEvent, render} from '@testing-library/react'; -import {ColorArea} from '../'; -import {installMouseEvent, installPointerEvent} from '@react-spectrum/test-utils'; -import {parseColor} from '@react-stately/color'; -import React from 'react'; -import userEvent from '@testing-library/user-event'; +// import {ColorArea} from '../'; import {XBlueYGreen as DefaultColorArea} from '../stories/ColorArea.stories'; +// import {installMouseEvent, installPointerEvent} from '@react-spectrum/test-utils'; +// import {parseColor} from '@react-stately/color'; +import React from 'react'; +import {render} from '@testing-library/react'; +// import userEvent from '@testing-library/user-event'; const SIZE = 160; -const CENTER = SIZE / 2; -const THUMB_RADIUS = 68; - -const getBoundingClientRect = () => ({ - width: SIZE, height: SIZE, - x: 0, y: 0, - top: 0, left: 0, - bottom: SIZE, right: SIZE, - toJSON() { return this; } -}); +// const CENTER = SIZE / 2; +// const THUMB_RADIUS = 68; +// +// const getBoundingClientRect = () => ({ +// width: SIZE, height: SIZE, +// x: 0, y: 0, +// top: 0, left: 0, +// bottom: SIZE, right: SIZE, +// toJSON() { return this; } +// }); describe('ColorArea', () => { let onChangeSpy = jest.fn(); @@ -36,6 +36,7 @@ describe('ColorArea', () => { beforeAll(() => { jest.spyOn(window.HTMLElement.prototype, 'offsetWidth', 'get').mockImplementation(() => SIZE); + // @ts-ignore jest.useFakeTimers('modern'); }); afterAll(() => { @@ -66,284 +67,284 @@ describe('ColorArea', () => { // expect(slider).toHaveAttribute('step', '1'); // expect(slider).toHaveAttribute('aria-valuetext', '0°'); }); - - it('the slider is focusable', () => { - let {getAllByRole, getByRole} = render(
- - - -
); - let slider = getByRole('slider'); - let [buttonA, buttonB] = getAllByRole('button'); - - userEvent.tab(); - expect(document.activeElement).toBe(buttonA); - userEvent.tab(); - expect(document.activeElement).toBe(slider); - userEvent.tab(); - expect(document.activeElement).toBe(buttonB); - userEvent.tab({shift: true}); - expect(document.activeElement).toBe(slider); - }); - - it('disabled', () => { - let {getAllByRole, getByRole} = render(
- - - -
); - let slider = getByRole('slider'); - let [buttonA, buttonB] = getAllByRole('button'); - expect(slider).toHaveAttribute('disabled'); - - userEvent.tab(); - expect(document.activeElement).toBe(buttonA); - userEvent.tab(); - expect(document.activeElement).toBe(buttonB); - userEvent.tab({shift: true}); - expect(document.activeElement).toBe(buttonA); - }); - - describe('labelling', () => { - it('should support a custom aria-label', () => { - let {getByRole} = render(); - let slider = getByRole('slider'); - - expect(slider).toHaveAttribute('aria-label', 'Color hue'); - expect(slider).not.toHaveAttribute('aria-labelledby'); - }); - - it('should support a custom aria-labelledby', () => { - let {getByRole} = render(); - let slider = getByRole('slider'); - - expect(slider).not.toHaveAttribute('aria-label'); - expect(slider).toHaveAttribute('aria-labelledby', 'label-id'); - }); - }); - }); - - describe('behaviors', () => { - describe('keyboard events', () => { - it('works', () => { - let defaultColor = parseColor('hsl(0, 100%, 50%)'); - let {getByRole} = render(); - let slider = getByRole('slider'); - act(() => { - slider.focus(); - }); - - fireEvent.keyDown(slider, {key: 'Right'}); - expect(onChangeSpy).toHaveBeenCalledTimes(1); - expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 1).toString('hsla')); - expect(onChangeEndSpy).toHaveBeenCalledTimes(1); - expect(onChangeEndSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 1).toString('hsla')); - fireEvent.keyDown(slider, {key: 'Left'}); - expect(onChangeSpy).toHaveBeenCalledTimes(2); - expect(onChangeSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 0).toString('hsla')); - expect(onChangeEndSpy).toHaveBeenCalledTimes(2); - expect(onChangeEndSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 0).toString('hsla')); - }); - - it('doesn\'t work when disabled', () => { - let defaultColor = parseColor('hsl(0, 100%, 50%)'); - let {getByRole} = render(); - let slider = getByRole('slider'); - act(() => { - slider.focus(); - }); - - fireEvent.keyDown(slider, {key: 'Right'}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); - fireEvent.keyDown(slider, {key: 'Left'}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); - }); - - it('wraps around', () => { - let defaultColor = parseColor('hsl(0, 100%, 50%)'); - let {getByRole} = render(); - let slider = getByRole('slider'); - act(() => { - slider.focus(); - }); - - fireEvent.keyDown(slider, {key: 'Left'}); - expect(onChangeSpy).toHaveBeenCalledTimes(1); - expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 359).toString('hsla')); - }); - - it('respects step', () => { - let defaultColor = parseColor('hsl(0, 100%, 50%)'); - let {getByRole} = render(); - let slider = getByRole('slider'); - act(() => { - slider.focus(); - }); - - fireEvent.keyDown(slider, {key: 'Right'}); - expect(onChangeSpy).toHaveBeenCalledTimes(1); - expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 45).toString('hsla')); - fireEvent.keyDown(slider, {key: 'Left'}); - expect(onChangeSpy).toHaveBeenCalledTimes(2); - expect(onChangeSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 0).toString('hsla')); - }); - }); - - describe.each` - type | prepare | actions - ${'Mouse Events'} | ${installMouseEvent} | ${[ - (el, {pageX, pageY}) => fireEvent.mouseDown(el, {button: 0, pageX, pageY, clientX: pageX, clientY: pageY}), - (el, {pageX, pageY}) => fireEvent.mouseMove(el, {button: 0, pageX, pageY, clientX: pageX, clientY: pageY}), - (el, {pageX, pageY}) => fireEvent.mouseUp(el, {button: 0, pageX, pageY, clientX: pageX, clientY: pageY}) - ]} - ${'Pointer Events'} | ${installPointerEvent}| ${[ - (el, {pageX, pageY}) => fireEvent.pointerDown(el, {button: 0, pointerId: 1, pageX, pageY, clientX: pageX, clientY: pageY}), - (el, {pageX, pageY}) => fireEvent.pointerMove(el, {button: 0, pointerId: 1, pageX, pageY, clientX: pageX, clientY: pageY}), - (el, {pageX, pageY}) => fireEvent.pointerUp(el, {button: 0, pointerId: 1, pageX, pageY, clientX: pageX, clientY: pageY}) - ]} - ${'Touch Events'} | ${() => {}} | ${[ - (el, {pageX, pageY}) => fireEvent.touchStart(el, {changedTouches: [{identifier: 1, pageX, pageY, clientX: pageX, clientY: pageY}]}), - (el, {pageX, pageY}) => fireEvent.touchMove(el, {changedTouches: [{identifier: 1, pageX, pageY, clientX: pageX, clientY: pageY}]}), - (el, {pageX, pageY}) => fireEvent.touchEnd(el, {changedTouches: [{identifier: 1, pageX, pageY, clientX: pageX, clientY: pageY}]}) - ]} - `('$type', ({actions: [start, move, end], prepare}) => { - prepare(); - - it('dragging the thumb works', () => { - let defaultColor = parseColor('hsl(0, 100%, 50%)'); - let {container: _container, getByRole} = render(); - let slider = getByRole('slider'); - let thumb = slider.parentElement; - let container = _container.firstChild.firstChild as HTMLElement; - container.getBoundingClientRect = getBoundingClientRect; - - expect(document.activeElement).not.toBe(slider); - start(thumb, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); - expect(onChangeEndSpy).toHaveBeenCalledTimes(0); - expect(document.activeElement).toBe(slider); - - move(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(1); - expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 90).toString('hsla')); - expect(onChangeEndSpy).toHaveBeenCalledTimes(0); - expect(document.activeElement).toBe(slider); - - end(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(1); - expect(onChangeEndSpy).toHaveBeenCalledTimes(1); - expect(onChangeEndSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 90).toString('hsla')); - expect(document.activeElement).toBe(slider); - }); - - it('dragging the thumb doesn\'t works when disabled', () => { - let defaultColor = parseColor('hsl(0, 100%, 50%)'); - let {container: _container, getByRole} = render(); - let slider = getByRole('slider'); - let container = _container.firstChild.firstChild as HTMLElement; - container.getBoundingClientRect = getBoundingClientRect; - let thumb = slider.parentElement; - - expect(document.activeElement).not.toBe(slider); - start(thumb, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); - expect(document.activeElement).not.toBe(slider); - - move(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); - expect(document.activeElement).not.toBe(slider); - - end(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); - expect(document.activeElement).not.toBe(slider); - }); - - it('dragging the thumb respects the step', () => { - let defaultColor = parseColor('hsl(0, 100%, 50%)'); - let {container: _container, getByRole} = render(); - let slider = getByRole('slider'); - let container = _container.firstChild.firstChild as HTMLElement; - let thumb = slider.parentElement; - container.getBoundingClientRect = getBoundingClientRect; - - start(thumb, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); - move(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(1); - expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 120).toString('hsla')); - end(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(1); - }); - - it('clicking and dragging on the track works', () => { - let defaultColor = parseColor('hsl(0, 100%, 50%)'); - let {container: _container, getByRole} = render(); - let slider = getByRole('slider'); - let thumb = slider.parentElement; - let container = _container.firstChild.firstChild as HTMLElement; - container.getBoundingClientRect = getBoundingClientRect; - - expect(document.activeElement).not.toBe(slider); - start(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(1); - expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 90).toString('hsla')); - expect(document.activeElement).toBe(slider); - - move(thumb, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); - expect(onChangeSpy).toHaveBeenCalledTimes(2); - expect(onChangeSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 180).toString('hsla')); - expect(document.activeElement).toBe(slider); - - end(thumb, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); - expect(onChangeSpy).toHaveBeenCalledTimes(2); - expect(document.activeElement).toBe(slider); - }); - - it('clicking and dragging on the track doesn\'t work when disabled', () => { - let defaultColor = parseColor('hsl(0, 100%, 50%)'); - let {container: _container, getByRole} = render(); - let slider = getByRole('slider'); - let container = _container.firstChild.firstChild as HTMLElement; - container.getBoundingClientRect = getBoundingClientRect; - - expect(document.activeElement).not.toBe(slider); - start(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); - expect(document.activeElement).not.toBe(slider); - - move(container, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); - expect(document.activeElement).not.toBe(slider); - - end(container, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); - expect(document.activeElement).not.toBe(slider); - }); - - it('clicking and dragging on the track respects the step', () => { - let defaultColor = parseColor('hsl(0, 100%, 50%)'); - let {container: _container, getByRole} = render(); - let slider = getByRole('slider'); - let thumb = slider.parentElement; - let container = _container.firstChild.firstChild as HTMLElement; - container.getBoundingClientRect = getBoundingClientRect; - - start(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(1); - expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 120).toString('hsla')); - move(thumb, {pageX: CENTER, pageY: CENTER - THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(2); - expect(onChangeSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 240).toString('hsla')); - end(thumb, {pageX: CENTER, pageY: CENTER - THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(2); - }); - }); + // + // it('the slider is focusable', () => { + // let {getAllByRole, getByRole} = render(
+ // + // + // + //
); + // let slider = getByRole('slider'); + // let [buttonA, buttonB] = getAllByRole('button'); + // + // userEvent.tab(); + // expect(document.activeElement).toBe(buttonA); + // userEvent.tab(); + // expect(document.activeElement).toBe(slider); + // userEvent.tab(); + // expect(document.activeElement).toBe(buttonB); + // userEvent.tab({shift: true}); + // expect(document.activeElement).toBe(slider); + // }); + // + // it('disabled', () => { + // let {getAllByRole, getByRole} = render(
+ // + // + // + //
); + // let slider = getByRole('slider'); + // let [buttonA, buttonB] = getAllByRole('button'); + // expect(slider).toHaveAttribute('disabled'); + // + // userEvent.tab(); + // expect(document.activeElement).toBe(buttonA); + // userEvent.tab(); + // expect(document.activeElement).toBe(buttonB); + // userEvent.tab({shift: true}); + // expect(document.activeElement).toBe(buttonA); + // }); + // + // describe('labelling', () => { + // it('should support a custom aria-label', () => { + // let {getByRole} = render(); + // let slider = getByRole('slider'); + // + // expect(slider).toHaveAttribute('aria-label', 'Color hue'); + // expect(slider).not.toHaveAttribute('aria-labelledby'); + // }); + // + // it('should support a custom aria-labelledby', () => { + // let {getByRole} = render(); + // let slider = getByRole('slider'); + // + // expect(slider).not.toHaveAttribute('aria-label'); + // expect(slider).toHaveAttribute('aria-labelledby', 'label-id'); + // }); + // }); + // }); + // + // describe('behaviors', () => { + // describe('keyboard events', () => { + // it('works', () => { + // let defaultColor = parseColor('hsl(0, 100%, 50%)'); + // let {getByRole} = render(); + // let slider = getByRole('slider'); + // act(() => { + // slider.focus(); + // }); + // + // fireEvent.keyDown(slider, {key: 'Right'}); + // expect(onChangeSpy).toHaveBeenCalledTimes(1); + // expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 1).toString('hsla')); + // expect(onChangeEndSpy).toHaveBeenCalledTimes(1); + // expect(onChangeEndSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 1).toString('hsla')); + // fireEvent.keyDown(slider, {key: 'Left'}); + // expect(onChangeSpy).toHaveBeenCalledTimes(2); + // expect(onChangeSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 0).toString('hsla')); + // expect(onChangeEndSpy).toHaveBeenCalledTimes(2); + // expect(onChangeEndSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 0).toString('hsla')); + // }); + // + // it('doesn\'t work when disabled', () => { + // let defaultColor = parseColor('hsl(0, 100%, 50%)'); + // let {getByRole} = render(); + // let slider = getByRole('slider'); + // act(() => { + // slider.focus(); + // }); + // + // fireEvent.keyDown(slider, {key: 'Right'}); + // expect(onChangeSpy).toHaveBeenCalledTimes(0); + // fireEvent.keyDown(slider, {key: 'Left'}); + // expect(onChangeSpy).toHaveBeenCalledTimes(0); + // }); + // + // it('wraps around', () => { + // let defaultColor = parseColor('hsl(0, 100%, 50%)'); + // let {getByRole} = render(); + // let slider = getByRole('slider'); + // act(() => { + // slider.focus(); + // }); + // + // fireEvent.keyDown(slider, {key: 'Left'}); + // expect(onChangeSpy).toHaveBeenCalledTimes(1); + // expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 359).toString('hsla')); + // }); + // + // it('respects step', () => { + // let defaultColor = parseColor('hsl(0, 100%, 50%)'); + // let {getByRole} = render(); + // let slider = getByRole('slider'); + // act(() => { + // slider.focus(); + // }); + // + // fireEvent.keyDown(slider, {key: 'Right'}); + // expect(onChangeSpy).toHaveBeenCalledTimes(1); + // expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 45).toString('hsla')); + // fireEvent.keyDown(slider, {key: 'Left'}); + // expect(onChangeSpy).toHaveBeenCalledTimes(2); + // expect(onChangeSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 0).toString('hsla')); + // }); + // }); + // + // describe.each` + // type | prepare | actions + // ${'Mouse Events'} | ${installMouseEvent} | ${[ + // (el, {pageX, pageY}) => fireEvent.mouseDown(el, {button: 0, pageX, pageY, clientX: pageX, clientY: pageY}), + // (el, {pageX, pageY}) => fireEvent.mouseMove(el, {button: 0, pageX, pageY, clientX: pageX, clientY: pageY}), + // (el, {pageX, pageY}) => fireEvent.mouseUp(el, {button: 0, pageX, pageY, clientX: pageX, clientY: pageY}) + // ]} + // ${'Pointer Events'} | ${installPointerEvent}| ${[ + // (el, {pageX, pageY}) => fireEvent.pointerDown(el, {button: 0, pointerId: 1, pageX, pageY, clientX: pageX, clientY: pageY}), + // (el, {pageX, pageY}) => fireEvent.pointerMove(el, {button: 0, pointerId: 1, pageX, pageY, clientX: pageX, clientY: pageY}), + // (el, {pageX, pageY}) => fireEvent.pointerUp(el, {button: 0, pointerId: 1, pageX, pageY, clientX: pageX, clientY: pageY}) + // ]} + // ${'Touch Events'} | ${() => {}} | ${[ + // (el, {pageX, pageY}) => fireEvent.touchStart(el, {changedTouches: [{identifier: 1, pageX, pageY, clientX: pageX, clientY: pageY}]}), + // (el, {pageX, pageY}) => fireEvent.touchMove(el, {changedTouches: [{identifier: 1, pageX, pageY, clientX: pageX, clientY: pageY}]}), + // (el, {pageX, pageY}) => fireEvent.touchEnd(el, {changedTouches: [{identifier: 1, pageX, pageY, clientX: pageX, clientY: pageY}]}) + // ]} + // `('$type', ({actions: [start, move, end], prepare}) => { + // prepare(); + // + // it('dragging the thumb works', () => { + // let defaultColor = parseColor('hsl(0, 100%, 50%)'); + // let {container: _container, getByRole} = render(); + // let slider = getByRole('slider'); + // let thumb = slider.parentElement; + // let container = _container.firstChild.firstChild as HTMLElement; + // container.getBoundingClientRect = getBoundingClientRect; + // + // expect(document.activeElement).not.toBe(slider); + // start(thumb, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); + // expect(onChangeSpy).toHaveBeenCalledTimes(0); + // expect(onChangeEndSpy).toHaveBeenCalledTimes(0); + // expect(document.activeElement).toBe(slider); + // + // move(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + // expect(onChangeSpy).toHaveBeenCalledTimes(1); + // expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 90).toString('hsla')); + // expect(onChangeEndSpy).toHaveBeenCalledTimes(0); + // expect(document.activeElement).toBe(slider); + // + // end(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + // expect(onChangeSpy).toHaveBeenCalledTimes(1); + // expect(onChangeEndSpy).toHaveBeenCalledTimes(1); + // expect(onChangeEndSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 90).toString('hsla')); + // expect(document.activeElement).toBe(slider); + // }); + // + // it('dragging the thumb doesn\'t works when disabled', () => { + // let defaultColor = parseColor('hsl(0, 100%, 50%)'); + // let {container: _container, getByRole} = render(); + // let slider = getByRole('slider'); + // let container = _container.firstChild.firstChild as HTMLElement; + // container.getBoundingClientRect = getBoundingClientRect; + // let thumb = slider.parentElement; + // + // expect(document.activeElement).not.toBe(slider); + // start(thumb, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); + // expect(onChangeSpy).toHaveBeenCalledTimes(0); + // expect(document.activeElement).not.toBe(slider); + // + // move(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + // expect(onChangeSpy).toHaveBeenCalledTimes(0); + // expect(document.activeElement).not.toBe(slider); + // + // end(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + // expect(onChangeSpy).toHaveBeenCalledTimes(0); + // expect(document.activeElement).not.toBe(slider); + // }); + // + // it('dragging the thumb respects the step', () => { + // let defaultColor = parseColor('hsl(0, 100%, 50%)'); + // let {container: _container, getByRole} = render(); + // let slider = getByRole('slider'); + // let container = _container.firstChild.firstChild as HTMLElement; + // let thumb = slider.parentElement; + // container.getBoundingClientRect = getBoundingClientRect; + // + // start(thumb, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); + // expect(onChangeSpy).toHaveBeenCalledTimes(0); + // move(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + // expect(onChangeSpy).toHaveBeenCalledTimes(1); + // expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 120).toString('hsla')); + // end(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + // expect(onChangeSpy).toHaveBeenCalledTimes(1); + // }); + // + // it('clicking and dragging on the track works', () => { + // let defaultColor = parseColor('hsl(0, 100%, 50%)'); + // let {container: _container, getByRole} = render(); + // let slider = getByRole('slider'); + // let thumb = slider.parentElement; + // let container = _container.firstChild.firstChild as HTMLElement; + // container.getBoundingClientRect = getBoundingClientRect; + // + // expect(document.activeElement).not.toBe(slider); + // start(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + // expect(onChangeSpy).toHaveBeenCalledTimes(1); + // expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 90).toString('hsla')); + // expect(document.activeElement).toBe(slider); + // + // move(thumb, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); + // expect(onChangeSpy).toHaveBeenCalledTimes(2); + // expect(onChangeSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 180).toString('hsla')); + // expect(document.activeElement).toBe(slider); + // + // end(thumb, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); + // expect(onChangeSpy).toHaveBeenCalledTimes(2); + // expect(document.activeElement).toBe(slider); + // }); + // + // it('clicking and dragging on the track doesn\'t work when disabled', () => { + // let defaultColor = parseColor('hsl(0, 100%, 50%)'); + // let {container: _container, getByRole} = render(); + // let slider = getByRole('slider'); + // let container = _container.firstChild.firstChild as HTMLElement; + // container.getBoundingClientRect = getBoundingClientRect; + // + // expect(document.activeElement).not.toBe(slider); + // start(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + // expect(onChangeSpy).toHaveBeenCalledTimes(0); + // expect(document.activeElement).not.toBe(slider); + // + // move(container, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); + // expect(onChangeSpy).toHaveBeenCalledTimes(0); + // expect(document.activeElement).not.toBe(slider); + // + // end(container, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); + // expect(onChangeSpy).toHaveBeenCalledTimes(0); + // expect(document.activeElement).not.toBe(slider); + // }); + // + // it('clicking and dragging on the track respects the step', () => { + // let defaultColor = parseColor('hsl(0, 100%, 50%)'); + // let {container: _container, getByRole} = render(); + // let slider = getByRole('slider'); + // let thumb = slider.parentElement; + // let container = _container.firstChild.firstChild as HTMLElement; + // container.getBoundingClientRect = getBoundingClientRect; + // + // start(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + // expect(onChangeSpy).toHaveBeenCalledTimes(1); + // expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 120).toString('hsla')); + // move(thumb, {pageX: CENTER, pageY: CENTER - THUMB_RADIUS}); + // expect(onChangeSpy).toHaveBeenCalledTimes(2); + // expect(onChangeSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 240).toString('hsla')); + // end(thumb, {pageX: CENTER, pageY: CENTER - THUMB_RADIUS}); + // expect(onChangeSpy).toHaveBeenCalledTimes(2); + // }); + // }); }); }); diff --git a/packages/@react-stately/color/package.json b/packages/@react-stately/color/package.json index 4176c49eb6e..947a59f4f3b 100644 --- a/packages/@react-stately/color/package.json +++ b/packages/@react-stately/color/package.json @@ -18,13 +18,14 @@ }, "dependencies": { "@babel/runtime": "^7.6.2", + "@internationalized/message": "^3.0.2", + "@internationalized/number": "^3.0.3", "@react-aria/utils": "^3.9.0", "@react-stately/slider": "^3.0.3", "@react-stately/utils": "^3.2.2", "@react-types/color": "3.0.0-beta.3", "@react-types/numberfield": "^3.1.0", - "@internationalized/message": "^3.0.2", - "@internationalized/number": "^3.0.3" + "@react-types/shared": "^3.9.0" }, "peerDependencies": { "react": "^16.8.0" diff --git a/packages/@react-stately/color/src/useColorAreaState.ts b/packages/@react-stately/color/src/useColorAreaState.ts index 6a7eba5a3ba..11ef62fdfd1 100644 --- a/packages/@react-stately/color/src/useColorAreaState.ts +++ b/packages/@react-stately/color/src/useColorAreaState.ts @@ -38,14 +38,14 @@ export interface ColorAreaState { getThumbPosition(): {x: number, y: number}, /** Increments the value of the horizontal axis channel by the given amount (defaults to 1). */ - incrementX(minStepSize?: number): void, + incrementX(): void, /** Decrements the value of the horizontal axis channel by the given amount (defaults to 1). */ - decrementX(minStepSize?: number): void, + decrementX(): void, /** Increments the value of the vertical axis channel by the given amount (defaults to 1). */ - incrementY(minStepSize?: number): void, + incrementY(): void, /** Decrements the value of the vertical axis channel by the given amount (defaults to 1). */ - decrementY(minStepSize?: number): void, + decrementY(): void, /** Whether the color area is currently being dragged. */ readonly isDragging: boolean, @@ -64,12 +64,13 @@ export interface ColorAreaState { const DEFAULT_COLOR = parseColor('hsb(0, 100%, 100%)'); const RGBSet: Set = new Set(['red', 'green', 'blue']); let difference = (a: Set, b: Set): Set => new Set([...a].filter(x => !b.has(x))); + /** * Provides state management for a color area component. * Color area allows users to adjust two channels of an HSL, HSB or RGB color value against a two-dimensional gradient background. */ export function useColorAreaState(props: ColorAreaProps): ColorAreaState { - let {value, defaultValue, xChannel, yChannel, onChange, onChangeEnd, xChannelStep = 1, yChannelStep = 1} = props; + let {value, defaultValue, xChannel, yChannel, onChange, onChangeEnd, xChannelStep, yChannelStep} = props; if (!value && !defaultValue) { defaultValue = DEFAULT_COLOR; @@ -112,11 +113,11 @@ export function useColorAreaState(props: ColorAreaProps): ColorAreaState { return {xChannel, yChannel, zChannel}; }, [xChannel, yChannel]); - if (!xChannelStep) { + if (isNaN(xChannelStep)) { xChannelStep = color.getChannelRange(xChannel).step; } - if (!yChannelStep) { + if (isNaN(yChannelStep)) { yChannelStep = color.getChannelRange(yChannel).step; } @@ -169,33 +170,49 @@ export function useColorAreaState(props: ColorAreaProps): ColorAreaState { let y = 1 - (yValue - minValueY) / (maxValueY - minValueY); return {x, y}; }, - incrementX(minStepSize: number = 0) { - let s = Math.max(minStepSize, xChannelStep); - let {maxValue} = color.getChannelRange(xChannel); - if (xValue < maxValue) { - setXValue(Math.min(xValue + s, maxValue)); - } + incrementX() { + let range = color.getChannelRange(xChannel); + let stepSize = xChannelStep; + setXValue( + clamp( + (Math.floor(xValue / stepSize) + 1) * stepSize, + range.minValue, + range.maxValue + ) + ); }, - incrementY(minStepSize: number = 0) { - let s = Math.max(minStepSize, yChannelStep); - let {maxValue} = color.getChannelRange(yChannel); - if (yValue < maxValue) { - setYValue(Math.min(yValue + s, maxValue)); - } + incrementY() { + let range = color.getChannelRange(yChannel); + let stepSize = yChannelStep; + setYValue( + clamp( + (Math.floor(yValue / stepSize) + 1) * stepSize, + range.minValue, + range.maxValue + ) + ); }, - decrementX(minStepSize: number = 0) { - let s = Math.max(minStepSize, xChannelStep); - let {minValue} = color.getChannelRange(xChannel); - if (xValue > minValue) { - setXValue(Math.max(xValue - s, minValue)); - } + decrementX() { + let range = color.getChannelRange(xChannel); + let stepSize = xChannelStep; + setXValue( + clamp( + (Math.ceil(xValue / stepSize) - 1) * stepSize, + range.minValue, + range.maxValue + ) + ); }, - decrementY(minStepSize: number = 0) { - let s = Math.max(minStepSize, yChannelStep); - let {minValue} = color.getChannelRange(yChannel); - if (yValue > minValue) { - setYValue(Math.max(yValue - s, minValue)); - } + decrementY() { + let range = color.getChannelRange(yChannel); + let stepSize = yChannelStep; + setYValue( + clamp( + (Math.ceil(yValue / stepSize) - 1) * stepSize, + range.minValue, + range.maxValue + ) + ); }, setDragging(isDragging) { let wasDragging = isDraggingRef; diff --git a/packages/@react-types/color/src/index.d.ts b/packages/@react-types/color/src/index.d.ts index 6a74a1b00e7..c9165d49172 100644 --- a/packages/@react-types/color/src/index.d.ts +++ b/packages/@react-types/color/src/index.d.ts @@ -146,15 +146,9 @@ export interface ColorAreaProps extends ValueBase { onChange?: (value: Color) => void, /** Handler that is called when the user stops dragging. */ onChangeEnd?: (value: Color) => void, - /** - * The step value for the xChannel. - * @default 1 - */ + /** The step value for the xChannel. */ xChannelStep?: number, - /** - * The step value for the yChannel. - * @default 1 - */ + /** The step value for the yChannel. */ yChannelStep?: number } From 4ba225c186f88a28771cfbcd8e4d2931a9d46c29 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Thu, 21 Oct 2021 16:57:32 -0700 Subject: [PATCH 05/23] tests and subsequent fixes from tests --- .../@react-aria/color/src/useColorArea.ts | 15 +- .../color/stories/ColorArea.stories.tsx | 44 +- .../color/test/ColorArea.test.tsx | 709 ++++++++++-------- .../color/src/useColorAreaState.ts | 40 +- 4 files changed, 468 insertions(+), 340 deletions(-) diff --git a/packages/@react-aria/color/src/useColorArea.ts b/packages/@react-aria/color/src/useColorArea.ts index b9a01efe13b..1d039cad8e1 100644 --- a/packages/@react-aria/color/src/useColorArea.ts +++ b/packages/@react-aria/color/src/useColorArea.ts @@ -122,10 +122,10 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i deltaX = maxMinOrZero(deltaX, xChannelStep); deltaY = maxMinOrZero(deltaY, yChannelStep); if (deltaX !== 0) { - stateRef.current[`${deltaX > 0 ? 'increment' : 'decrement'}X`](Math.abs(deltaX)); + stateRef.current[`${deltaX > 0 ? 'increment' : 'decrement'}X`](); } if (deltaY !== 0) { - stateRef.current[`${deltaY < 0 ? 'increment' : 'decrement'}Y`](Math.abs(deltaY)); + stateRef.current[`${deltaY < 0 ? 'increment' : 'decrement'}Y`](); } // set the focused input based on which axis has the greater delta focusedInputRef.current = (deltaX !== 0 || deltaY !== 0) && Math.abs(deltaY) > Math.abs(deltaX) ? inputYRef.current : inputXRef.current; @@ -279,7 +279,12 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i }, movePropsThumb, keyboardProps); - let inputLabellingProps = useLabels({ + let xInputLabellingProps = useLabels({ + ...props, + 'aria-label': `${state.value.getChannelName(xChannel, locale)} / ${state.value.getChannelName(yChannel, locale)}` + }); + + let yInputLabellingProps = useLabels({ ...props, 'aria-label': `${state.value.getChannelName(xChannel, locale)} / ${state.value.getChannelName(yChannel, locale)}` }); @@ -312,7 +317,7 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i ...thumbInteractions }, xInputProps: { - ...inputLabellingProps, + ...xInputLabellingProps, ...visuallyHiddenProps, type: 'range', min: state.value.getChannelRange(xChannel).minValue, @@ -332,7 +337,7 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i } }, yInputProps: { - ...inputLabellingProps, + ...yInputLabellingProps, ...visuallyHiddenProps, type: 'range', min: state.value.getChannelRange(yChannel).minValue, diff --git a/packages/@react-spectrum/color/stories/ColorArea.stories.tsx b/packages/@react-spectrum/color/stories/ColorArea.stories.tsx index da8a3088b69..f184572b13b 100644 --- a/packages/@react-spectrum/color/stories/ColorArea.stories.tsx +++ b/packages/@react-spectrum/color/stories/ColorArea.stories.tsx @@ -35,7 +35,7 @@ let RGB: Set = new Set(['red', 'green', 'blue']); let difference = (a, b): Set => new Set([...a].filter(x => !b.has(x))); function ColorAreaExample(props: SpectrumColorAreaProps) { - let {xChannel, yChannel} = props; + let {xChannel, yChannel, isDisabled} = props; let channels = new Set([xChannel, yChannel]); let zChannel: ColorChannel = difference(RGB, channels).keys().next().value as ColorChannel; let [color, setColor] = useState(parseColor('#ff00ff')); @@ -46,10 +46,12 @@ function ColorAreaExample(props: SpectrumColorAreaProps) { {...props} value={color} onChange={(e) => { - action('change')(e); + if (props.onChange) { + props.onChange(e); + } setColor(e); }} /> - +
@@ -60,29 +62,39 @@ function ColorAreaExample(props: SpectrumColorAreaProps) { } export let XBlueYGreen = Template.bind({}); -XBlueYGreen.title = 'RGB xChannel="blue", yChannel="green"'; -XBlueYGreen.args = {xChannel: 'blue', yChannel: 'green'}; +XBlueYGreen.storyName = 'RGB xChannel="blue", yChannel="green"'; +XBlueYGreen.args = {xChannel: 'blue', yChannel: 'green', onChange: action('onChange')}; export let XGreenYBlue = Template.bind({}); -XGreenYBlue.title = 'RGB xChannel="green", yChannel="blue"'; -XGreenYBlue.args = {xChannel: 'green', yChannel: 'blue'}; +XGreenYBlue.storyName = 'RGB xChannel="green", yChannel="blue"'; +XGreenYBlue.args = {xChannel: 'green', yChannel: 'blue', onChange: action('onChange')}; export let XBlueYRed = Template.bind({}); -XBlueYRed.title = 'RGB xChannel="blue", yChannel="red"'; -XBlueYRed.args = {xChannel: 'blue', yChannel: 'red'}; +XBlueYRed.storyName = 'RGB xChannel="blue", yChannel="red"'; +XBlueYRed.args = {xChannel: 'blue', yChannel: 'red', onChange: action('onChange')}; export let XRedYBlue = Template.bind({}); -XRedYBlue.title = 'GB xChannel="red", yChannel="blue"'; -XRedYBlue.args = {xChannel: 'red', yChannel: 'blue'}; +XRedYBlue.storyName = 'GB xChannel="red", yChannel="blue"'; +XRedYBlue.args = {xChannel: 'red', yChannel: 'blue', onChange: action('onChange')}; export let XRedYGreen = Template.bind({}); -XRedYGreen.title = 'RGB xChannel="red", yChannel="green"'; -XRedYGreen.args = {xChannel: 'red', yChannel: 'green'}; +XRedYGreen.storyName = 'RGB xChannel="red", yChannel="green"'; +XRedYGreen.args = {xChannel: 'red', yChannel: 'green', onChange: action('onChange')}; export let XGreenYRed = Template.bind({}); -XGreenYRed.title = 'RGB xChannel="green", yChannel="red"'; -XGreenYRed.args = {xChannel: 'green', yChannel: 'red'}; +XGreenYRed.storyName = 'RGB xChannel="green", yChannel="red"'; +XGreenYRed.args = {xChannel: 'green', yChannel: 'red', onChange: action('onChange')}; export let XBlueYGreenStep16 = Template.bind({}); -XBlueYGreenStep16.title = 'RGB xChannel="blue", yChannel="green" step="16"'; +XBlueYGreenStep16.storyName = 'RGB xChannel="blue", yChannel="green", step="16"'; XBlueYGreenStep16.args = {...XBlueYGreen.args, xChannelStep: 16, yChannelStep: 16}; + +/* TODO: what does a disabled color area look like? */ +export let XBlueYGreenisDisabled = Template.bind({}); +XBlueYGreenisDisabled.storyName = 'RGB xChannel="blue", yChannel="green", isDisabled'; +XBlueYGreenisDisabled.args = {...XBlueYGreen.args, isDisabled: true}; + +/* TODO: how do we visually label and how to do we aria-label */ +export let XBlueYGreenAriaLabelled = Template.bind({}); +XBlueYGreenAriaLabelled.storyName = 'RGB xChannel="blue", yChannel="green", aria-label="foo"'; +XBlueYGreenAriaLabelled.args = {...XBlueYGreen.args, label: undefined, ariaLabel: 'foo'}; diff --git a/packages/@react-spectrum/color/test/ColorArea.test.tsx b/packages/@react-spectrum/color/test/ColorArea.test.tsx index 476ec6181f7..f8b743d6946 100644 --- a/packages/@react-spectrum/color/test/ColorArea.test.tsx +++ b/packages/@react-spectrum/color/test/ColorArea.test.tsx @@ -12,23 +12,23 @@ // import {ColorArea} from '../'; import {XBlueYGreen as DefaultColorArea} from '../stories/ColorArea.stories'; -// import {installMouseEvent, installPointerEvent} from '@react-spectrum/test-utils'; -// import {parseColor} from '@react-stately/color'; +import {fireEvent, render} from '@testing-library/react'; +import {installMouseEvent, installPointerEvent} from '@react-spectrum/test-utils'; +import {parseColor} from '@react-stately/color'; import React from 'react'; -import {render} from '@testing-library/react'; -// import userEvent from '@testing-library/user-event'; +import userEvent from '@testing-library/user-event'; const SIZE = 160; -// const CENTER = SIZE / 2; -// const THUMB_RADIUS = 68; -// -// const getBoundingClientRect = () => ({ -// width: SIZE, height: SIZE, -// x: 0, y: 0, -// top: 0, left: 0, -// bottom: SIZE, right: SIZE, -// toJSON() { return this; } -// }); +const CENTER = SIZE / 2; +const THUMB_RADIUS = 68; + +const getBoundingClientRect = () => ({ + width: SIZE, height: SIZE, + x: 0, y: 0, + top: 0, left: 0, + bottom: SIZE, right: SIZE, + toJSON() { return this; } +}); describe('ColorArea', () => { let onChangeSpy = jest.fn(); @@ -53,298 +53,403 @@ describe('ColorArea', () => { }); describe('attributes', () => { - it.only('sets input props', () => { - let {getAllByRole} = render(); + it('sets input props', () => { + let {getAllByRole, getByLabelText} = render(); let sliders = getAllByRole('slider'); expect(sliders.length).toBe(3); + let [xSlider, ySlider, zSlider] = sliders; + + expect(xSlider).toHaveAttribute('type', 'range'); + expect(xSlider).toHaveAttribute('aria-label', 'Blue / Green'); + expect(xSlider).toHaveAttribute('min', '0'); + expect(xSlider).toHaveAttribute('max', '255'); + expect(xSlider).toHaveAttribute('step', '1'); + expect(xSlider).toHaveAttribute('aria-valuetext', 'Blue: 255, Green: 0'); + + expect(ySlider).toHaveAttribute('type', 'range'); + expect(ySlider).toHaveAttribute('aria-label', 'Blue / Green'); + expect(ySlider).toHaveAttribute('min', '0'); + expect(ySlider).toHaveAttribute('max', '255'); + expect(ySlider).toHaveAttribute('step', '1'); + expect(ySlider).toHaveAttribute('aria-valuetext', 'Green: 0, Blue: 255'); + + let redSlider = getByLabelText('Red', {selector: 'input'}); + expect(zSlider).toHaveAttribute('type', 'range'); + expect(zSlider).not.toHaveAttribute('aria-label'); + expect(zSlider).toBe(redSlider); + expect(zSlider).toHaveAttribute('min', '0'); + expect(zSlider).toHaveAttribute('max', '255'); + expect(zSlider).toHaveAttribute('step', '1'); + expect(zSlider).toHaveAttribute('aria-valuetext', '255'); + }); + + it('the slider is focusable', () => { + let {getAllByRole} = render(
+ + + +
); + let sliders = getAllByRole('slider'); + let [buttonA, buttonB] = getAllByRole('button'); + + userEvent.tab(); + expect(document.activeElement).toBe(buttonA); + userEvent.tab(); + expect(document.activeElement).toBe(sliders[0]); + userEvent.tab(); + expect(document.activeElement).toBe(sliders[2]); + userEvent.tab(); + expect(document.activeElement).toBe(buttonB); + userEvent.tab({shift: true}); + expect(document.activeElement).toBe(sliders[2]); + }); + + it('disabled', () => { + let {getAllByRole} = render(
+ + + +
); + let sliders = getAllByRole('slider'); + let [buttonA, buttonB] = getAllByRole('button'); + sliders.forEach(slider => { + expect(slider).toHaveAttribute('disabled'); + }); + + userEvent.tab(); + expect(document.activeElement).toBe(buttonA); + userEvent.tab(); + expect(document.activeElement).toBe(buttonB); + userEvent.tab({shift: true}); + expect(document.activeElement).toBe(buttonA); + }); + + // TODO: don't know how to do this yet + describe.skip('labelling', () => { + it('should support a custom aria-label', () => { + let {getAllByRole} = render(); + let slider = getAllByRole('slider'); + + expect(slider).toHaveAttribute('aria-label', 'Color hue'); + expect(slider).not.toHaveAttribute('aria-labelledby'); + }); + + it('should support a custom aria-labelledby', () => { + let {getAllByRole} = render(); + let slider = getAllByRole('slider'); + + expect(slider).not.toHaveAttribute('aria-label'); + expect(slider).toHaveAttribute('aria-labelledby', 'label-id'); + }); + }); + }); + + describe('behaviors', () => { + let pressKey = (element, options) => { + fireEvent.keyDown(element, options); + fireEvent.keyUp(element, options); + }; + describe('keyboard events', () => { + it.each` + Name | props | actions | result + ${'left/right'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'Left'}), backward: (elem) => pressKey(elem, {key: 'Right'})}} | ${parseColor('#ff00fe')} + ${'up/down'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'Up'}), backward: (elem) => pressKey(elem, {key: 'Down'})}} | ${parseColor('#ff01ff')} + `('$Name', ({props, actions: {forward, backward}, result}) => { + let {getAllByRole} = render( + + ); + let sliders = getAllByRole('slider'); + userEvent.tab(); + + forward(sliders[0], {key: 'Left'}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(result.toString('rgba')); + expect(onChangeEndSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy.mock.calls[0][0].toString('rgba')).toBe(result.toString('rgba')); + + backward(sliders[0], {key: 'Right'}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeSpy.mock.calls[1][0].toString('rgba')).toBe(props.defaultValue.toString('rgba')); + expect(onChangeEndSpy).toHaveBeenCalledTimes(2); + expect(onChangeEndSpy.mock.calls[1][0].toString('rgba')).toBe(props.defaultValue.toString('rgba')); + }); + + it('no events when disabled', () => { + let defaultColor = parseColor('#ff00ff'); + let {getAllByRole, getByRole} = render(
+ + +
); + let buttonA = getByRole('button'); + let sliders = getAllByRole('slider'); + userEvent.tab(); + expect(buttonA).toBe(document.activeElement); + + pressKey(sliders[0], {key: 'Left'}); + expect(onChangeSpy).not.toHaveBeenCalled(); + expect(onChangeEndSpy).not.toHaveBeenCalled(); + pressKey(sliders[0], {key: 'Right'}); + expect(onChangeSpy).not.toHaveBeenCalled(); + expect(onChangeEndSpy).not.toHaveBeenCalled(); + }); + + it.each` + Name | props | actions | result + ${'left/right'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'Left'}), backward: (elem) => pressKey(elem, {key: 'Right'})}} | ${parseColor('#ff00f0')} + ${'up/down'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'Up'}), backward: (elem) => pressKey(elem, {key: 'Down'})}} | ${parseColor('#ff0fff')} + `('$Name with step', ({props, actions: {forward, backward}, result}) => { + let {getAllByRole} = render( + + ); + let sliders = getAllByRole('slider'); + userEvent.tab(); + + forward(sliders[0], {key: 'Left'}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(result.toString('rgba')); + expect(onChangeEndSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy.mock.calls[0][0].toString('rgba')).toBe(result.toString('rgba')); + + backward(sliders[0], {key: 'Right'}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeSpy.mock.calls[1][0].toString('rgba')).toBe(props.defaultValue.toString('rgba')); + expect(onChangeEndSpy).toHaveBeenCalledTimes(2); + expect(onChangeEndSpy.mock.calls[1][0].toString('rgba')).toBe(props.defaultValue.toString('rgba')); + }); + }); + + describe.each` + type | prepare | actions + ${'Mouse Events'} | ${installMouseEvent} | ${[ + (el, {pageX, pageY}) => fireEvent.mouseDown(el, {button: 0, pageX, pageY, clientX: pageX, clientY: pageY}), + (el, {pageX, pageY}) => fireEvent.mouseMove(el, {button: 0, pageX, pageY, clientX: pageX, clientY: pageY}), + (el, {pageX, pageY}) => fireEvent.mouseUp(el, {button: 0, pageX, pageY, clientX: pageX, clientY: pageY}) + ]} + ${'Pointer Events'} | ${installPointerEvent}| ${[ + (el, {pageX, pageY}) => fireEvent.pointerDown(el, {button: 0, pointerId: 1, pageX, pageY, clientX: pageX, clientY: pageY}), + (el, {pageX, pageY}) => fireEvent.pointerMove(el, {button: 0, pointerId: 1, pageX, pageY, clientX: pageX, clientY: pageY}), + (el, {pageX, pageY}) => fireEvent.pointerUp(el, {button: 0, pointerId: 1, pageX, pageY, clientX: pageX, clientY: pageY}) + ]} + ${'Touch Events'} | ${() => {}} | ${[ + (el, {pageX, pageY}) => fireEvent.touchStart(el, {changedTouches: [{identifier: 1, pageX, pageY, clientX: pageX, clientY: pageY}]}), + (el, {pageX, pageY}) => fireEvent.touchMove(el, {changedTouches: [{identifier: 1, pageX, pageY, clientX: pageX, clientY: pageY}]}), + (el, {pageX, pageY}) => fireEvent.touchEnd(el, {changedTouches: [{identifier: 1, pageX, pageY, clientX: pageX, clientY: pageY}]}) + ]} + `('$type', ({actions: [start, move, end], prepare}) => { + prepare(); + + it('clicking on the area chooses the color at that point', () => { + let defaultColor = parseColor('#ff00ff'); + let {getAllByRole} = render( + + ); + let sliders = getAllByRole('slider'); + let groups = getAllByRole('group'); + let container = groups[1]; + container.getBoundingClientRect = getBoundingClientRect; + + expect(document.activeElement).not.toBe(sliders[0]); + start(container, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy).toHaveBeenCalledTimes(0); + expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff80EC').toString('rgba')); + expect(document.activeElement).toBe(sliders[0]); + + end(container, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff80EC').toString('rgba')); + expect(document.activeElement).toBe(sliders[0]); + }); + + it('dragging the thumb works', () => { + let defaultColor = parseColor('#ff00ff'); + let {getAllByRole} = render( + + ); + let sliders = getAllByRole('slider'); + let groups = getAllByRole('group'); + let thumb = sliders[0].parentElement; + let container = groups[1]; + container.getBoundingClientRect = getBoundingClientRect; + + expect(document.activeElement).not.toBe(sliders[0]); + start(thumb, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(onChangeEndSpy).toHaveBeenCalledTimes(0); + expect(document.activeElement).toBe(sliders[0]); + + move(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff0093').toString('rgba')); + expect(onChangeEndSpy).toHaveBeenCalledTimes(0); + expect(document.activeElement).toBe(sliders[0]); + + end(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff0093').toString('rgba')); + expect(document.activeElement).toBe(sliders[0]); + }); + + it('dragging the thumb doesn\'t works when disabled', () => { + let defaultColor = parseColor('#ff00ff'); + let {getAllByRole} = render( + + ); + let sliders = getAllByRole('slider'); + let groups = getAllByRole('group'); + let thumb = sliders[0].parentElement; + let container = groups[1]; + container.getBoundingClientRect = getBoundingClientRect; + + expect(document.activeElement).not.toBe(sliders[0]); + start(thumb, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + + move(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + + end(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(onChangeEndSpy).toHaveBeenCalledTimes(0); + }); + + // TODO: Should it? + it('dragging the thumb respects the step', () => { + let defaultColor = parseColor('#ff00ff'); + let {getAllByRole} = render( + + ); + let sliders = getAllByRole('slider'); + let groups = getAllByRole('group'); + let thumb = sliders[0].parentElement; + let container = groups[1]; + container.getBoundingClientRect = getBoundingClientRect; + + start(thumb, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + + move(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff0090').toString('rgba')); + + end(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + }); + + it('clicking and dragging on the track works', () => { + let defaultColor = parseColor('#ff00ff'); + let {getAllByRole} = render( + + ); + let sliders = getAllByRole('slider'); + let groups = getAllByRole('group'); + let container = groups[1]; + container.getBoundingClientRect = getBoundingClientRect; + + expect(document.activeElement).not.toBe(sliders[0]); + start(container, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy).toHaveBeenCalledTimes(0); + expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff80EC').toString('rgba')); + expect(document.activeElement).toBe(sliders[0]); + + move(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeSpy.mock.calls[1][0].toString('rgba')).toBe(parseColor('#ff1480').toString('rgba')); + expect(document.activeElement).toBe(sliders[0]); + + end(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeEndSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff1480').toString('rgba')); + expect(document.activeElement).toBe(sliders[0]); + }); + + it('clicking and dragging on the track doesn\'t work when disabled', () => { + let defaultColor = parseColor('#ff00ff'); + let {getAllByRole} = render( + + ); + let sliders = getAllByRole('slider'); + let groups = getAllByRole('group'); + let container = groups[1]; + container.getBoundingClientRect = getBoundingClientRect; + + expect(document.activeElement).not.toBe(sliders[0]); + start(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(document.activeElement).not.toBe(sliders[0]); + + move(container, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(document.activeElement).not.toBe(sliders[0]); + + end(container, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(document.activeElement).not.toBe(sliders[0]); + }); + + it('clicking and dragging on the track respects the step', () => { + let defaultColor = parseColor('#ff00ff'); + let {getAllByRole} = render( + + ); + let groups = getAllByRole('group'); + let container = groups[1]; + container.getBoundingClientRect = getBoundingClientRect; + + start(container, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + + move(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff80f0').toString('rgba')); - // - // expect(slider).toHaveAttribute('type', 'range'); - // expect(slider).toHaveAttribute('aria-label', 'Hue'); - // expect(slider).toHaveAttribute('min', '0'); - // expect(slider).toHaveAttribute('max', '360'); - // expect(slider).toHaveAttribute('step', '1'); - // expect(slider).toHaveAttribute('aria-valuetext', '0°'); + end(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + }); }); - // - // it('the slider is focusable', () => { - // let {getAllByRole, getByRole} = render(
- // - // - // - //
); - // let slider = getByRole('slider'); - // let [buttonA, buttonB] = getAllByRole('button'); - // - // userEvent.tab(); - // expect(document.activeElement).toBe(buttonA); - // userEvent.tab(); - // expect(document.activeElement).toBe(slider); - // userEvent.tab(); - // expect(document.activeElement).toBe(buttonB); - // userEvent.tab({shift: true}); - // expect(document.activeElement).toBe(slider); - // }); - // - // it('disabled', () => { - // let {getAllByRole, getByRole} = render(
- // - // - // - //
); - // let slider = getByRole('slider'); - // let [buttonA, buttonB] = getAllByRole('button'); - // expect(slider).toHaveAttribute('disabled'); - // - // userEvent.tab(); - // expect(document.activeElement).toBe(buttonA); - // userEvent.tab(); - // expect(document.activeElement).toBe(buttonB); - // userEvent.tab({shift: true}); - // expect(document.activeElement).toBe(buttonA); - // }); - // - // describe('labelling', () => { - // it('should support a custom aria-label', () => { - // let {getByRole} = render(); - // let slider = getByRole('slider'); - // - // expect(slider).toHaveAttribute('aria-label', 'Color hue'); - // expect(slider).not.toHaveAttribute('aria-labelledby'); - // }); - // - // it('should support a custom aria-labelledby', () => { - // let {getByRole} = render(); - // let slider = getByRole('slider'); - // - // expect(slider).not.toHaveAttribute('aria-label'); - // expect(slider).toHaveAttribute('aria-labelledby', 'label-id'); - // }); - // }); - // }); - // - // describe('behaviors', () => { - // describe('keyboard events', () => { - // it('works', () => { - // let defaultColor = parseColor('hsl(0, 100%, 50%)'); - // let {getByRole} = render(); - // let slider = getByRole('slider'); - // act(() => { - // slider.focus(); - // }); - // - // fireEvent.keyDown(slider, {key: 'Right'}); - // expect(onChangeSpy).toHaveBeenCalledTimes(1); - // expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 1).toString('hsla')); - // expect(onChangeEndSpy).toHaveBeenCalledTimes(1); - // expect(onChangeEndSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 1).toString('hsla')); - // fireEvent.keyDown(slider, {key: 'Left'}); - // expect(onChangeSpy).toHaveBeenCalledTimes(2); - // expect(onChangeSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 0).toString('hsla')); - // expect(onChangeEndSpy).toHaveBeenCalledTimes(2); - // expect(onChangeEndSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 0).toString('hsla')); - // }); - // - // it('doesn\'t work when disabled', () => { - // let defaultColor = parseColor('hsl(0, 100%, 50%)'); - // let {getByRole} = render(); - // let slider = getByRole('slider'); - // act(() => { - // slider.focus(); - // }); - // - // fireEvent.keyDown(slider, {key: 'Right'}); - // expect(onChangeSpy).toHaveBeenCalledTimes(0); - // fireEvent.keyDown(slider, {key: 'Left'}); - // expect(onChangeSpy).toHaveBeenCalledTimes(0); - // }); - // - // it('wraps around', () => { - // let defaultColor = parseColor('hsl(0, 100%, 50%)'); - // let {getByRole} = render(); - // let slider = getByRole('slider'); - // act(() => { - // slider.focus(); - // }); - // - // fireEvent.keyDown(slider, {key: 'Left'}); - // expect(onChangeSpy).toHaveBeenCalledTimes(1); - // expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 359).toString('hsla')); - // }); - // - // it('respects step', () => { - // let defaultColor = parseColor('hsl(0, 100%, 50%)'); - // let {getByRole} = render(); - // let slider = getByRole('slider'); - // act(() => { - // slider.focus(); - // }); - // - // fireEvent.keyDown(slider, {key: 'Right'}); - // expect(onChangeSpy).toHaveBeenCalledTimes(1); - // expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 45).toString('hsla')); - // fireEvent.keyDown(slider, {key: 'Left'}); - // expect(onChangeSpy).toHaveBeenCalledTimes(2); - // expect(onChangeSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 0).toString('hsla')); - // }); - // }); - // - // describe.each` - // type | prepare | actions - // ${'Mouse Events'} | ${installMouseEvent} | ${[ - // (el, {pageX, pageY}) => fireEvent.mouseDown(el, {button: 0, pageX, pageY, clientX: pageX, clientY: pageY}), - // (el, {pageX, pageY}) => fireEvent.mouseMove(el, {button: 0, pageX, pageY, clientX: pageX, clientY: pageY}), - // (el, {pageX, pageY}) => fireEvent.mouseUp(el, {button: 0, pageX, pageY, clientX: pageX, clientY: pageY}) - // ]} - // ${'Pointer Events'} | ${installPointerEvent}| ${[ - // (el, {pageX, pageY}) => fireEvent.pointerDown(el, {button: 0, pointerId: 1, pageX, pageY, clientX: pageX, clientY: pageY}), - // (el, {pageX, pageY}) => fireEvent.pointerMove(el, {button: 0, pointerId: 1, pageX, pageY, clientX: pageX, clientY: pageY}), - // (el, {pageX, pageY}) => fireEvent.pointerUp(el, {button: 0, pointerId: 1, pageX, pageY, clientX: pageX, clientY: pageY}) - // ]} - // ${'Touch Events'} | ${() => {}} | ${[ - // (el, {pageX, pageY}) => fireEvent.touchStart(el, {changedTouches: [{identifier: 1, pageX, pageY, clientX: pageX, clientY: pageY}]}), - // (el, {pageX, pageY}) => fireEvent.touchMove(el, {changedTouches: [{identifier: 1, pageX, pageY, clientX: pageX, clientY: pageY}]}), - // (el, {pageX, pageY}) => fireEvent.touchEnd(el, {changedTouches: [{identifier: 1, pageX, pageY, clientX: pageX, clientY: pageY}]}) - // ]} - // `('$type', ({actions: [start, move, end], prepare}) => { - // prepare(); - // - // it('dragging the thumb works', () => { - // let defaultColor = parseColor('hsl(0, 100%, 50%)'); - // let {container: _container, getByRole} = render(); - // let slider = getByRole('slider'); - // let thumb = slider.parentElement; - // let container = _container.firstChild.firstChild as HTMLElement; - // container.getBoundingClientRect = getBoundingClientRect; - // - // expect(document.activeElement).not.toBe(slider); - // start(thumb, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); - // expect(onChangeSpy).toHaveBeenCalledTimes(0); - // expect(onChangeEndSpy).toHaveBeenCalledTimes(0); - // expect(document.activeElement).toBe(slider); - // - // move(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - // expect(onChangeSpy).toHaveBeenCalledTimes(1); - // expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 90).toString('hsla')); - // expect(onChangeEndSpy).toHaveBeenCalledTimes(0); - // expect(document.activeElement).toBe(slider); - // - // end(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - // expect(onChangeSpy).toHaveBeenCalledTimes(1); - // expect(onChangeEndSpy).toHaveBeenCalledTimes(1); - // expect(onChangeEndSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 90).toString('hsla')); - // expect(document.activeElement).toBe(slider); - // }); - // - // it('dragging the thumb doesn\'t works when disabled', () => { - // let defaultColor = parseColor('hsl(0, 100%, 50%)'); - // let {container: _container, getByRole} = render(); - // let slider = getByRole('slider'); - // let container = _container.firstChild.firstChild as HTMLElement; - // container.getBoundingClientRect = getBoundingClientRect; - // let thumb = slider.parentElement; - // - // expect(document.activeElement).not.toBe(slider); - // start(thumb, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); - // expect(onChangeSpy).toHaveBeenCalledTimes(0); - // expect(document.activeElement).not.toBe(slider); - // - // move(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - // expect(onChangeSpy).toHaveBeenCalledTimes(0); - // expect(document.activeElement).not.toBe(slider); - // - // end(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - // expect(onChangeSpy).toHaveBeenCalledTimes(0); - // expect(document.activeElement).not.toBe(slider); - // }); - // - // it('dragging the thumb respects the step', () => { - // let defaultColor = parseColor('hsl(0, 100%, 50%)'); - // let {container: _container, getByRole} = render(); - // let slider = getByRole('slider'); - // let container = _container.firstChild.firstChild as HTMLElement; - // let thumb = slider.parentElement; - // container.getBoundingClientRect = getBoundingClientRect; - // - // start(thumb, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); - // expect(onChangeSpy).toHaveBeenCalledTimes(0); - // move(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - // expect(onChangeSpy).toHaveBeenCalledTimes(1); - // expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 120).toString('hsla')); - // end(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - // expect(onChangeSpy).toHaveBeenCalledTimes(1); - // }); - // - // it('clicking and dragging on the track works', () => { - // let defaultColor = parseColor('hsl(0, 100%, 50%)'); - // let {container: _container, getByRole} = render(); - // let slider = getByRole('slider'); - // let thumb = slider.parentElement; - // let container = _container.firstChild.firstChild as HTMLElement; - // container.getBoundingClientRect = getBoundingClientRect; - // - // expect(document.activeElement).not.toBe(slider); - // start(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - // expect(onChangeSpy).toHaveBeenCalledTimes(1); - // expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 90).toString('hsla')); - // expect(document.activeElement).toBe(slider); - // - // move(thumb, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); - // expect(onChangeSpy).toHaveBeenCalledTimes(2); - // expect(onChangeSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 180).toString('hsla')); - // expect(document.activeElement).toBe(slider); - // - // end(thumb, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); - // expect(onChangeSpy).toHaveBeenCalledTimes(2); - // expect(document.activeElement).toBe(slider); - // }); - // - // it('clicking and dragging on the track doesn\'t work when disabled', () => { - // let defaultColor = parseColor('hsl(0, 100%, 50%)'); - // let {container: _container, getByRole} = render(); - // let slider = getByRole('slider'); - // let container = _container.firstChild.firstChild as HTMLElement; - // container.getBoundingClientRect = getBoundingClientRect; - // - // expect(document.activeElement).not.toBe(slider); - // start(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - // expect(onChangeSpy).toHaveBeenCalledTimes(0); - // expect(document.activeElement).not.toBe(slider); - // - // move(container, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); - // expect(onChangeSpy).toHaveBeenCalledTimes(0); - // expect(document.activeElement).not.toBe(slider); - // - // end(container, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); - // expect(onChangeSpy).toHaveBeenCalledTimes(0); - // expect(document.activeElement).not.toBe(slider); - // }); - // - // it('clicking and dragging on the track respects the step', () => { - // let defaultColor = parseColor('hsl(0, 100%, 50%)'); - // let {container: _container, getByRole} = render(); - // let slider = getByRole('slider'); - // let thumb = slider.parentElement; - // let container = _container.firstChild.firstChild as HTMLElement; - // container.getBoundingClientRect = getBoundingClientRect; - // - // start(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - // expect(onChangeSpy).toHaveBeenCalledTimes(1); - // expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 120).toString('hsla')); - // move(thumb, {pageX: CENTER, pageY: CENTER - THUMB_RADIUS}); - // expect(onChangeSpy).toHaveBeenCalledTimes(2); - // expect(onChangeSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 240).toString('hsla')); - // end(thumb, {pageX: CENTER, pageY: CENTER - THUMB_RADIUS}); - // expect(onChangeSpy).toHaveBeenCalledTimes(2); - // }); - // }); }); }); diff --git a/packages/@react-stately/color/src/useColorAreaState.ts b/packages/@react-stately/color/src/useColorAreaState.ts index 11ef62fdfd1..7b22188efe1 100644 --- a/packages/@react-stately/color/src/useColorAreaState.ts +++ b/packages/@react-stately/color/src/useColorAreaState.ts @@ -114,20 +114,26 @@ export function useColorAreaState(props: ColorAreaProps): ColorAreaState { }, [xChannel, yChannel]); if (isNaN(xChannelStep)) { - xChannelStep = color.getChannelRange(xChannel).step; + xChannelStep = color.getChannelRange(channels.xChannel).step; } if (isNaN(yChannelStep)) { - yChannelStep = color.getChannelRange(yChannel).step; + yChannelStep = color.getChannelRange(channels.yChannel).step; } let [isDragging, setDragging] = useState(false); let isDraggingRef = useRef(false).current; - let xValue = color.getChannelValue(xChannel); - let yValue = color.getChannelValue(yChannel); - let setXValue = (v: number) => setColor(color.withChannelValue(xChannel, v)); - let setYValue = (v: number) => setColor(color.withChannelValue(yChannel, v)); + let xValue = color.getChannelValue(channels.xChannel); + let yValue = color.getChannelValue(channels.yChannel); + let setXValue = (v: number) => { + valueRef.current = color.withChannelValue(channels.xChannel, v); + setColor(valueRef.current); + }; + let setYValue = (v: number) => { + valueRef.current = color.withChannelValue(channels.yChannel, v); + setColor(valueRef.current); + }; return { channels, @@ -144,34 +150,34 @@ export function useColorAreaState(props: ColorAreaProps): ColorAreaState { yValue, setYValue, setColorFromPoint(x: number, y: number) { - let {minValue: minValueX, maxValue: maxValueX} = color.getChannelRange(xChannel); - let {minValue: minValueY, maxValue: maxValueY} = color.getChannelRange(yChannel); + let {minValue: minValueX, maxValue: maxValueX} = color.getChannelRange(channels.xChannel); + let {minValue: minValueY, maxValue: maxValueY} = color.getChannelRange(channels.yChannel); let newXValue = minValueX + clamp(x, 0, 1) * (maxValueX - minValueX); let newYValue = minValueY + (1 - clamp(y, 0, 1)) * (maxValueY - minValueY); let newColor:Color; if (newXValue !== xValue) { // Round new value to multiple of step, clamp value between min and max newXValue = snapValueToStep(newXValue, minValueX, maxValueX, xChannelStep); - newColor = color.withChannelValue(xChannel, newXValue); + newColor = color.withChannelValue(channels.xChannel, newXValue); } if (newYValue !== yValue) { // Round new value to multiple of step, clamp value between min and max newYValue = snapValueToStep(newYValue, minValueY, maxValueY, yChannelStep); - newColor = (newColor || color).withChannelValue(yChannel, newYValue); + newColor = (newColor || color).withChannelValue(channels.yChannel, newYValue); } if (newColor) { setColor(newColor); } }, getThumbPosition() { - let {minValue, maxValue} = color.getChannelRange(xChannel); - let {minValue: minValueY, maxValue: maxValueY} = color.getChannelRange(yChannel); - let x = (xValue - minValue) / (maxValue - minValue); + let {minValue: minValueX, maxValue: maxValueX} = color.getChannelRange(channels.xChannel); + let {minValue: minValueY, maxValue: maxValueY} = color.getChannelRange(channels.yChannel); + let x = (xValue - minValueX) / (maxValueX - minValueX); let y = 1 - (yValue - minValueY) / (maxValueY - minValueY); return {x, y}; }, incrementX() { - let range = color.getChannelRange(xChannel); + let range = color.getChannelRange(channels.xChannel); let stepSize = xChannelStep; setXValue( clamp( @@ -182,7 +188,7 @@ export function useColorAreaState(props: ColorAreaProps): ColorAreaState { ); }, incrementY() { - let range = color.getChannelRange(yChannel); + let range = color.getChannelRange(channels.yChannel); let stepSize = yChannelStep; setYValue( clamp( @@ -193,7 +199,7 @@ export function useColorAreaState(props: ColorAreaProps): ColorAreaState { ); }, decrementX() { - let range = color.getChannelRange(xChannel); + let range = color.getChannelRange(channels.xChannel); let stepSize = xChannelStep; setXValue( clamp( @@ -204,7 +210,7 @@ export function useColorAreaState(props: ColorAreaProps): ColorAreaState { ); }, decrementY() { - let range = color.getChannelRange(yChannel); + let range = color.getChannelRange(channels.yChannel); let stepSize = yChannelStep; setYValue( clamp( From 9971bed4638858cc345565f8a4263b9d299d8fb7 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Thu, 21 Oct 2021 17:54:30 -0700 Subject: [PATCH 06/23] Add uncontrolled to tests --- .../color/stories/ColorArea.stories.tsx | 4 +- .../color/test/ColorArea.test.tsx | 771 ++++++++++-------- .../color/src/useColorAreaState.ts | 2 +- 3 files changed, 424 insertions(+), 353 deletions(-) diff --git a/packages/@react-spectrum/color/stories/ColorArea.stories.tsx b/packages/@react-spectrum/color/stories/ColorArea.stories.tsx index f184572b13b..13dd3a3be19 100644 --- a/packages/@react-spectrum/color/stories/ColorArea.stories.tsx +++ b/packages/@react-spectrum/color/stories/ColorArea.stories.tsx @@ -37,8 +37,8 @@ let difference = (a, b): Set => new Set([...a].filter(x => !b.has( function ColorAreaExample(props: SpectrumColorAreaProps) { let {xChannel, yChannel, isDisabled} = props; let channels = new Set([xChannel, yChannel]); - let zChannel: ColorChannel = difference(RGB, channels).keys().next().value as ColorChannel; - let [color, setColor] = useState(parseColor('#ff00ff')); + let zChannel: ColorChannel = difference(RGB, channels).keys().next().value; + let [color, setColor] = useState(props.defaultValue || parseColor('#ff00ff')); return (
diff --git a/packages/@react-spectrum/color/test/ColorArea.test.tsx b/packages/@react-spectrum/color/test/ColorArea.test.tsx index f8b743d6946..2fa6a69c44d 100644 --- a/packages/@react-spectrum/color/test/ColorArea.test.tsx +++ b/packages/@react-spectrum/color/test/ColorArea.test.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -// import {ColorArea} from '../'; +import {ColorArea} from '../'; import {XBlueYGreen as DefaultColorArea} from '../stories/ColorArea.stories'; import {fireEvent, render} from '@testing-library/react'; import {installMouseEvent, installPointerEvent} from '@react-spectrum/test-utils'; @@ -52,7 +52,425 @@ describe('ColorArea', () => { onChangeEndSpy.mockClear(); }); - describe('attributes', () => { + // get group corresponds to the index returned by getAllByRole('group') + describe.each` + Name | Component | groupIndex + ${'Controlled'} | ${DefaultColorArea} | ${1} + ${'Uncontrolled'} | ${ColorArea} | ${0} + `('$Name', ({Component, groupIndex}) => { + describe('attributes', () => { + it('sets input props', () => { + let {getAllByRole} = render(); + let sliders = getAllByRole('slider'); + + let [xSlider, ySlider] = sliders; + + expect(xSlider).toHaveAttribute('type', 'range'); + expect(xSlider).toHaveAttribute('aria-label', 'Blue / Green'); + expect(xSlider).toHaveAttribute('min', '0'); + expect(xSlider).toHaveAttribute('max', '255'); + expect(xSlider).toHaveAttribute('step', '1'); + expect(xSlider).toHaveAttribute('aria-valuetext', 'Blue: 255, Green: 0'); + + expect(ySlider).toHaveAttribute('type', 'range'); + expect(ySlider).toHaveAttribute('aria-label', 'Blue / Green'); + expect(ySlider).toHaveAttribute('min', '0'); + expect(ySlider).toHaveAttribute('max', '255'); + expect(ySlider).toHaveAttribute('step', '1'); + expect(ySlider).toHaveAttribute('aria-valuetext', 'Green: 0, Blue: 255'); + }); + + it('disabled', () => { + let {getAllByRole} = render(
+ + + +
); + let sliders = getAllByRole('slider'); + let [buttonA, buttonB] = getAllByRole('button'); + sliders.forEach(slider => { + expect(slider).toHaveAttribute('disabled'); + }); + + userEvent.tab(); + expect(document.activeElement).toBe(buttonA); + userEvent.tab(); + expect(document.activeElement).toBe(buttonB); + userEvent.tab({shift: true}); + expect(document.activeElement).toBe(buttonA); + }); + + // TODO: don't know how to do this yet + describe.skip('labelling', () => { + it('should support a custom aria-label', () => { + let {getAllByRole} = render(); + let slider = getAllByRole('slider'); + + expect(slider).toHaveAttribute('aria-label', 'Color hue'); + expect(slider).not.toHaveAttribute('aria-labelledby'); + }); + + it('should support a custom aria-labelledby', () => { + let {getAllByRole} = render(); + let slider = getAllByRole('slider'); + + expect(slider).not.toHaveAttribute('aria-label'); + expect(slider).toHaveAttribute('aria-labelledby', 'label-id'); + }); + }); + }); + + describe('behaviors', () => { + let pressKey = (element, options) => { + fireEvent.keyDown(element, options); + fireEvent.keyUp(element, options); + }; + describe('keyboard events', () => { + it.each` + Name | props | actions | result + ${'left/right'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'Left'}), backward: (elem) => pressKey(elem, {key: 'Right'})}} | ${parseColor('#ff00fe')} + ${'up/down'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'Up'}), backward: (elem) => pressKey(elem, {key: 'Down'})}} | ${parseColor('#ff01ff')} + `('$Name', ({props, actions: {forward, backward}, result}) => { + let {getAllByRole} = render( + + ); + let sliders = getAllByRole('slider'); + userEvent.tab(); + + forward(sliders[0], {key: 'Left'}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(result.toString('rgba')); + expect(onChangeEndSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy.mock.calls[0][0].toString('rgba')).toBe(result.toString('rgba')); + + backward(sliders[0], {key: 'Right'}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeSpy.mock.calls[1][0].toString('rgba')).toBe(props.defaultValue.toString('rgba')); + expect(onChangeEndSpy).toHaveBeenCalledTimes(2); + expect(onChangeEndSpy.mock.calls[1][0].toString('rgba')).toBe(props.defaultValue.toString('rgba')); + }); + + it('no events when disabled', () => { + let defaultColor = parseColor('#ff00ff'); + let {getAllByRole, getByRole} = render(
+ + +
); + let buttonA = getByRole('button'); + let sliders = getAllByRole('slider'); + userEvent.tab(); + expect(buttonA).toBe(document.activeElement); + + pressKey(sliders[0], {key: 'Left'}); + expect(onChangeSpy).not.toHaveBeenCalled(); + expect(onChangeEndSpy).not.toHaveBeenCalled(); + pressKey(sliders[0], {key: 'Right'}); + expect(onChangeSpy).not.toHaveBeenCalled(); + expect(onChangeEndSpy).not.toHaveBeenCalled(); + }); + + it.each` + Name | props | actions | result + ${'left/right'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'Left'}), backward: (elem) => pressKey(elem, {key: 'Right'})}} | ${parseColor('#ff00f0')} + ${'up/down'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'Up'}), backward: (elem) => pressKey(elem, {key: 'Down'})}} | ${parseColor('#ff0fff')} + `('$Name with step', ({props, actions: {forward, backward}, result}) => { + let {getAllByRole} = render( + + ); + let sliders = getAllByRole('slider'); + userEvent.tab(); + + forward(sliders[0], {key: 'Left'}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(result.toString('rgba')); + expect(onChangeEndSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy.mock.calls[0][0].toString('rgba')).toBe(result.toString('rgba')); + + backward(sliders[0], {key: 'Right'}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeSpy.mock.calls[1][0].toString('rgba')).toBe(props.defaultValue.toString('rgba')); + expect(onChangeEndSpy).toHaveBeenCalledTimes(2); + expect(onChangeEndSpy.mock.calls[1][0].toString('rgba')).toBe(props.defaultValue.toString('rgba')); + }); + }); + + describe.each` + type | prepare | actions + ${'Mouse Events'} | ${installMouseEvent} | ${[ + (el, {pageX, pageY}) => fireEvent.mouseDown(el, {button: 0, pageX, pageY, clientX: pageX, clientY: pageY}), + (el, {pageX, pageY}) => fireEvent.mouseMove(el, {button: 0, pageX, pageY, clientX: pageX, clientY: pageY}), + (el, {pageX, pageY}) => fireEvent.mouseUp(el, {button: 0, pageX, pageY, clientX: pageX, clientY: pageY}) + ]} + ${'Pointer Events'} | ${installPointerEvent}| ${[ + (el, {pageX, pageY}) => fireEvent.pointerDown(el, {button: 0, pointerId: 1, pageX, pageY, clientX: pageX, clientY: pageY}), + (el, {pageX, pageY}) => fireEvent.pointerMove(el, {button: 0, pointerId: 1, pageX, pageY, clientX: pageX, clientY: pageY}), + (el, {pageX, pageY}) => fireEvent.pointerUp(el, {button: 0, pointerId: 1, pageX, pageY, clientX: pageX, clientY: pageY}) + ]} + ${'Touch Events'} | ${() => {}} | ${[ + (el, {pageX, pageY}) => fireEvent.touchStart(el, {changedTouches: [{identifier: 1, pageX, pageY, clientX: pageX, clientY: pageY}]}), + (el, {pageX, pageY}) => fireEvent.touchMove(el, {changedTouches: [{identifier: 1, pageX, pageY, clientX: pageX, clientY: pageY}]}), + (el, {pageX, pageY}) => fireEvent.touchEnd(el, {changedTouches: [{identifier: 1, pageX, pageY, clientX: pageX, clientY: pageY}]}) + ]} + `('$type', ({actions: [start, move, end], prepare}) => { + prepare(); + + it('clicking on the area chooses the color at that point', () => { + let defaultColor = parseColor('#ff00ff'); + let {getAllByRole} = render( + + ); + let sliders = getAllByRole('slider'); + let groups = getAllByRole('group'); + let container = groups[groupIndex]; + container.getBoundingClientRect = getBoundingClientRect; + + expect(document.activeElement).not.toBe(sliders[0]); + start(container, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy).toHaveBeenCalledTimes(0); + expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff80EC').toString('rgba')); + expect(document.activeElement).toBe(sliders[0]); + + end(container, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff80EC').toString('rgba')); + expect(document.activeElement).toBe(sliders[0]); + }); + + it('dragging the thumb works', () => { + let defaultColor = parseColor('#ff00ff'); + let {getAllByRole} = render( + + ); + let sliders = getAllByRole('slider'); + let groups = getAllByRole('group'); + let thumb = sliders[0].parentElement; + let container = groups[groupIndex]; + container.getBoundingClientRect = getBoundingClientRect; + + expect(document.activeElement).not.toBe(sliders[0]); + start(thumb, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(onChangeEndSpy).toHaveBeenCalledTimes(0); + expect(document.activeElement).toBe(sliders[0]); + + move(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff0093').toString('rgba')); + expect(onChangeEndSpy).toHaveBeenCalledTimes(0); + expect(document.activeElement).toBe(sliders[0]); + + end(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff0093').toString('rgba')); + expect(document.activeElement).toBe(sliders[0]); + }); + + it('dragging the thumb doesn\'t works when disabled', () => { + let defaultColor = parseColor('#ff00ff'); + let {getAllByRole} = render( + + ); + let sliders = getAllByRole('slider'); + let groups = getAllByRole('group'); + let thumb = sliders[0].parentElement; + let container = groups[groupIndex]; + container.getBoundingClientRect = getBoundingClientRect; + + expect(document.activeElement).not.toBe(sliders[0]); + start(thumb, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + + move(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + + end(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(onChangeEndSpy).toHaveBeenCalledTimes(0); + }); + + // TODO: Should it? + it('dragging the thumb respects the step', () => { + let defaultColor = parseColor('#ff00ff'); + let {getAllByRole} = render( + + ); + let sliders = getAllByRole('slider'); + let groups = getAllByRole('group'); + let thumb = sliders[0].parentElement; + let container = groups[groupIndex]; + container.getBoundingClientRect = getBoundingClientRect; + + start(thumb, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + + move(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff0090').toString('rgba')); + + end(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + }); + + it('clicking and dragging on the track works', () => { + let defaultColor = parseColor('#ff00ff'); + let {getAllByRole} = render( + + ); + let sliders = getAllByRole('slider'); + let groups = getAllByRole('group'); + let container = groups[groupIndex]; + container.getBoundingClientRect = getBoundingClientRect; + + expect(document.activeElement).not.toBe(sliders[0]); + start(container, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy).toHaveBeenCalledTimes(0); + expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff80EC').toString('rgba')); + expect(document.activeElement).toBe(sliders[0]); + + move(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeSpy.mock.calls[1][0].toString('rgba')).toBe(parseColor('#ff1480').toString('rgba')); + expect(document.activeElement).toBe(sliders[0]); + + end(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeEndSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff1480').toString('rgba')); + expect(document.activeElement).toBe(sliders[0]); + }); + + it('clicking and dragging on the track doesn\'t work when disabled', () => { + let defaultColor = parseColor('#ff00ff'); + let {getAllByRole} = render( + + ); + let sliders = getAllByRole('slider'); + let groups = getAllByRole('group'); + let container = groups[groupIndex]; + container.getBoundingClientRect = getBoundingClientRect; + + expect(document.activeElement).not.toBe(sliders[0]); + start(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(document.activeElement).not.toBe(sliders[0]); + + move(container, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(document.activeElement).not.toBe(sliders[0]); + + end(container, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(document.activeElement).not.toBe(sliders[0]); + }); + + it('clicking and dragging on the track respects the step', () => { + let defaultColor = parseColor('#ff00ff'); + let {getAllByRole} = render( + + ); + let groups = getAllByRole('group'); + let container = groups[groupIndex]; + container.getBoundingClientRect = getBoundingClientRect; + + start(container, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + + move(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff80f0').toString('rgba')); + + end(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + }); + }); + }); + }); + describe('defaults uncontrolled', () => { + it('sets input props', () => { + let {getAllByRole} = render(); + let sliders = getAllByRole('slider'); + + let [xSlider, ySlider] = sliders; + + expect(xSlider).toHaveAttribute('type', 'range'); + expect(xSlider).toHaveAttribute('aria-label', 'Blue / Green'); + expect(xSlider).toHaveAttribute('min', '0'); + expect(xSlider).toHaveAttribute('max', '255'); + expect(xSlider).toHaveAttribute('step', '1'); + expect(xSlider).toHaveAttribute('aria-valuetext', 'Blue: 255, Green: 255'); + + expect(ySlider).toHaveAttribute('type', 'range'); + expect(ySlider).toHaveAttribute('aria-label', 'Blue / Green'); + expect(ySlider).toHaveAttribute('min', '0'); + expect(ySlider).toHaveAttribute('max', '255'); + expect(ySlider).toHaveAttribute('step', '1'); + expect(ySlider).toHaveAttribute('aria-valuetext', 'Green: 255, Blue: 255'); + }); + + it('the slider is focusable', () => { + let {getAllByRole} = render(
+ + + +
); + let sliders = getAllByRole('slider'); + let [buttonA, buttonB] = getAllByRole('button'); + + userEvent.tab(); + expect(document.activeElement).toBe(buttonA); + userEvent.tab(); + expect(document.activeElement).toBe(sliders[0]); + userEvent.tab(); + expect(document.activeElement).toBe(buttonB); + userEvent.tab({shift: true}); + expect(document.activeElement).toBe(sliders[0]); + }); + }); + describe('full implementation controlled', () => { it('sets input props', () => { let {getAllByRole, getByLabelText} = render(); let sliders = getAllByRole('slider'); @@ -87,7 +505,7 @@ describe('ColorArea', () => { it('the slider is focusable', () => { let {getAllByRole} = render(
- +
); let sliders = getAllByRole('slider'); @@ -104,352 +522,5 @@ describe('ColorArea', () => { userEvent.tab({shift: true}); expect(document.activeElement).toBe(sliders[2]); }); - - it('disabled', () => { - let {getAllByRole} = render(
- - - -
); - let sliders = getAllByRole('slider'); - let [buttonA, buttonB] = getAllByRole('button'); - sliders.forEach(slider => { - expect(slider).toHaveAttribute('disabled'); - }); - - userEvent.tab(); - expect(document.activeElement).toBe(buttonA); - userEvent.tab(); - expect(document.activeElement).toBe(buttonB); - userEvent.tab({shift: true}); - expect(document.activeElement).toBe(buttonA); - }); - - // TODO: don't know how to do this yet - describe.skip('labelling', () => { - it('should support a custom aria-label', () => { - let {getAllByRole} = render(); - let slider = getAllByRole('slider'); - - expect(slider).toHaveAttribute('aria-label', 'Color hue'); - expect(slider).not.toHaveAttribute('aria-labelledby'); - }); - - it('should support a custom aria-labelledby', () => { - let {getAllByRole} = render(); - let slider = getAllByRole('slider'); - - expect(slider).not.toHaveAttribute('aria-label'); - expect(slider).toHaveAttribute('aria-labelledby', 'label-id'); - }); - }); - }); - - describe('behaviors', () => { - let pressKey = (element, options) => { - fireEvent.keyDown(element, options); - fireEvent.keyUp(element, options); - }; - describe('keyboard events', () => { - it.each` - Name | props | actions | result - ${'left/right'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'Left'}), backward: (elem) => pressKey(elem, {key: 'Right'})}} | ${parseColor('#ff00fe')} - ${'up/down'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'Up'}), backward: (elem) => pressKey(elem, {key: 'Down'})}} | ${parseColor('#ff01ff')} - `('$Name', ({props, actions: {forward, backward}, result}) => { - let {getAllByRole} = render( - - ); - let sliders = getAllByRole('slider'); - userEvent.tab(); - - forward(sliders[0], {key: 'Left'}); - expect(onChangeSpy).toHaveBeenCalledTimes(1); - expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(result.toString('rgba')); - expect(onChangeEndSpy).toHaveBeenCalledTimes(1); - expect(onChangeEndSpy.mock.calls[0][0].toString('rgba')).toBe(result.toString('rgba')); - - backward(sliders[0], {key: 'Right'}); - expect(onChangeSpy).toHaveBeenCalledTimes(2); - expect(onChangeSpy.mock.calls[1][0].toString('rgba')).toBe(props.defaultValue.toString('rgba')); - expect(onChangeEndSpy).toHaveBeenCalledTimes(2); - expect(onChangeEndSpy.mock.calls[1][0].toString('rgba')).toBe(props.defaultValue.toString('rgba')); - }); - - it('no events when disabled', () => { - let defaultColor = parseColor('#ff00ff'); - let {getAllByRole, getByRole} = render(
- - -
); - let buttonA = getByRole('button'); - let sliders = getAllByRole('slider'); - userEvent.tab(); - expect(buttonA).toBe(document.activeElement); - - pressKey(sliders[0], {key: 'Left'}); - expect(onChangeSpy).not.toHaveBeenCalled(); - expect(onChangeEndSpy).not.toHaveBeenCalled(); - pressKey(sliders[0], {key: 'Right'}); - expect(onChangeSpy).not.toHaveBeenCalled(); - expect(onChangeEndSpy).not.toHaveBeenCalled(); - }); - - it.each` - Name | props | actions | result - ${'left/right'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'Left'}), backward: (elem) => pressKey(elem, {key: 'Right'})}} | ${parseColor('#ff00f0')} - ${'up/down'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'Up'}), backward: (elem) => pressKey(elem, {key: 'Down'})}} | ${parseColor('#ff0fff')} - `('$Name with step', ({props, actions: {forward, backward}, result}) => { - let {getAllByRole} = render( - - ); - let sliders = getAllByRole('slider'); - userEvent.tab(); - - forward(sliders[0], {key: 'Left'}); - expect(onChangeSpy).toHaveBeenCalledTimes(1); - expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(result.toString('rgba')); - expect(onChangeEndSpy).toHaveBeenCalledTimes(1); - expect(onChangeEndSpy.mock.calls[0][0].toString('rgba')).toBe(result.toString('rgba')); - - backward(sliders[0], {key: 'Right'}); - expect(onChangeSpy).toHaveBeenCalledTimes(2); - expect(onChangeSpy.mock.calls[1][0].toString('rgba')).toBe(props.defaultValue.toString('rgba')); - expect(onChangeEndSpy).toHaveBeenCalledTimes(2); - expect(onChangeEndSpy.mock.calls[1][0].toString('rgba')).toBe(props.defaultValue.toString('rgba')); - }); - }); - - describe.each` - type | prepare | actions - ${'Mouse Events'} | ${installMouseEvent} | ${[ - (el, {pageX, pageY}) => fireEvent.mouseDown(el, {button: 0, pageX, pageY, clientX: pageX, clientY: pageY}), - (el, {pageX, pageY}) => fireEvent.mouseMove(el, {button: 0, pageX, pageY, clientX: pageX, clientY: pageY}), - (el, {pageX, pageY}) => fireEvent.mouseUp(el, {button: 0, pageX, pageY, clientX: pageX, clientY: pageY}) - ]} - ${'Pointer Events'} | ${installPointerEvent}| ${[ - (el, {pageX, pageY}) => fireEvent.pointerDown(el, {button: 0, pointerId: 1, pageX, pageY, clientX: pageX, clientY: pageY}), - (el, {pageX, pageY}) => fireEvent.pointerMove(el, {button: 0, pointerId: 1, pageX, pageY, clientX: pageX, clientY: pageY}), - (el, {pageX, pageY}) => fireEvent.pointerUp(el, {button: 0, pointerId: 1, pageX, pageY, clientX: pageX, clientY: pageY}) - ]} - ${'Touch Events'} | ${() => {}} | ${[ - (el, {pageX, pageY}) => fireEvent.touchStart(el, {changedTouches: [{identifier: 1, pageX, pageY, clientX: pageX, clientY: pageY}]}), - (el, {pageX, pageY}) => fireEvent.touchMove(el, {changedTouches: [{identifier: 1, pageX, pageY, clientX: pageX, clientY: pageY}]}), - (el, {pageX, pageY}) => fireEvent.touchEnd(el, {changedTouches: [{identifier: 1, pageX, pageY, clientX: pageX, clientY: pageY}]}) - ]} - `('$type', ({actions: [start, move, end], prepare}) => { - prepare(); - - it('clicking on the area chooses the color at that point', () => { - let defaultColor = parseColor('#ff00ff'); - let {getAllByRole} = render( - - ); - let sliders = getAllByRole('slider'); - let groups = getAllByRole('group'); - let container = groups[1]; - container.getBoundingClientRect = getBoundingClientRect; - - expect(document.activeElement).not.toBe(sliders[0]); - start(container, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); - expect(onChangeSpy).toHaveBeenCalledTimes(1); - expect(onChangeEndSpy).toHaveBeenCalledTimes(0); - expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff80EC').toString('rgba')); - expect(document.activeElement).toBe(sliders[0]); - - end(container, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); - expect(onChangeSpy).toHaveBeenCalledTimes(1); - expect(onChangeEndSpy).toHaveBeenCalledTimes(1); - expect(onChangeEndSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff80EC').toString('rgba')); - expect(document.activeElement).toBe(sliders[0]); - }); - - it('dragging the thumb works', () => { - let defaultColor = parseColor('#ff00ff'); - let {getAllByRole} = render( - - ); - let sliders = getAllByRole('slider'); - let groups = getAllByRole('group'); - let thumb = sliders[0].parentElement; - let container = groups[1]; - container.getBoundingClientRect = getBoundingClientRect; - - expect(document.activeElement).not.toBe(sliders[0]); - start(thumb, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); - expect(onChangeEndSpy).toHaveBeenCalledTimes(0); - expect(document.activeElement).toBe(sliders[0]); - - move(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(1); - expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff0093').toString('rgba')); - expect(onChangeEndSpy).toHaveBeenCalledTimes(0); - expect(document.activeElement).toBe(sliders[0]); - - end(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(1); - expect(onChangeEndSpy).toHaveBeenCalledTimes(1); - expect(onChangeEndSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff0093').toString('rgba')); - expect(document.activeElement).toBe(sliders[0]); - }); - - it('dragging the thumb doesn\'t works when disabled', () => { - let defaultColor = parseColor('#ff00ff'); - let {getAllByRole} = render( - - ); - let sliders = getAllByRole('slider'); - let groups = getAllByRole('group'); - let thumb = sliders[0].parentElement; - let container = groups[1]; - container.getBoundingClientRect = getBoundingClientRect; - - expect(document.activeElement).not.toBe(sliders[0]); - start(thumb, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); - - move(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); - - end(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); - expect(onChangeEndSpy).toHaveBeenCalledTimes(0); - }); - - // TODO: Should it? - it('dragging the thumb respects the step', () => { - let defaultColor = parseColor('#ff00ff'); - let {getAllByRole} = render( - - ); - let sliders = getAllByRole('slider'); - let groups = getAllByRole('group'); - let thumb = sliders[0].parentElement; - let container = groups[1]; - container.getBoundingClientRect = getBoundingClientRect; - - start(thumb, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); - - move(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(1); - expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff0090').toString('rgba')); - - end(thumb, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(1); - }); - - it('clicking and dragging on the track works', () => { - let defaultColor = parseColor('#ff00ff'); - let {getAllByRole} = render( - - ); - let sliders = getAllByRole('slider'); - let groups = getAllByRole('group'); - let container = groups[1]; - container.getBoundingClientRect = getBoundingClientRect; - - expect(document.activeElement).not.toBe(sliders[0]); - start(container, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); - expect(onChangeSpy).toHaveBeenCalledTimes(1); - expect(onChangeEndSpy).toHaveBeenCalledTimes(0); - expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff80EC').toString('rgba')); - expect(document.activeElement).toBe(sliders[0]); - - move(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(2); - expect(onChangeSpy.mock.calls[1][0].toString('rgba')).toBe(parseColor('#ff1480').toString('rgba')); - expect(document.activeElement).toBe(sliders[0]); - - end(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(2); - expect(onChangeEndSpy).toHaveBeenCalledTimes(1); - expect(onChangeEndSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff1480').toString('rgba')); - expect(document.activeElement).toBe(sliders[0]); - }); - - it('clicking and dragging on the track doesn\'t work when disabled', () => { - let defaultColor = parseColor('#ff00ff'); - let {getAllByRole} = render( - - ); - let sliders = getAllByRole('slider'); - let groups = getAllByRole('group'); - let container = groups[1]; - container.getBoundingClientRect = getBoundingClientRect; - - expect(document.activeElement).not.toBe(sliders[0]); - start(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); - expect(document.activeElement).not.toBe(sliders[0]); - - move(container, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); - expect(document.activeElement).not.toBe(sliders[0]); - - end(container, {pageX: CENTER - THUMB_RADIUS, pageY: CENTER}); - expect(onChangeSpy).toHaveBeenCalledTimes(0); - expect(document.activeElement).not.toBe(sliders[0]); - }); - - it('clicking and dragging on the track respects the step', () => { - let defaultColor = parseColor('#ff00ff'); - let {getAllByRole} = render( - - ); - let groups = getAllByRole('group'); - let container = groups[1]; - container.getBoundingClientRect = getBoundingClientRect; - - start(container, {pageX: CENTER + THUMB_RADIUS, pageY: CENTER}); - expect(onChangeSpy).toHaveBeenCalledTimes(1); - - move(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(2); - expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(parseColor('#ff80f0').toString('rgba')); - - end(container, {pageX: CENTER, pageY: CENTER + THUMB_RADIUS}); - expect(onChangeSpy).toHaveBeenCalledTimes(2); - }); - }); }); }); diff --git a/packages/@react-stately/color/src/useColorAreaState.ts b/packages/@react-stately/color/src/useColorAreaState.ts index 7b22188efe1..a37017a7886 100644 --- a/packages/@react-stately/color/src/useColorAreaState.ts +++ b/packages/@react-stately/color/src/useColorAreaState.ts @@ -61,7 +61,7 @@ export interface ColorAreaState { getDisplayColor(): Color } -const DEFAULT_COLOR = parseColor('hsb(0, 100%, 100%)'); +const DEFAULT_COLOR = parseColor('#ffffff'); const RGBSet: Set = new Set(['red', 'green', 'blue']); let difference = (a: Set, b: Set): Set => new Set([...a].filter(x => !b.has(x))); From fe8a88fc21623b497847dd8599806e65641295c5 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Thu, 21 Oct 2021 18:07:35 -0700 Subject: [PATCH 07/23] Add size story --- .../@react-spectrum/color/stories/ColorArea.stories.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/@react-spectrum/color/stories/ColorArea.stories.tsx b/packages/@react-spectrum/color/stories/ColorArea.stories.tsx index 13dd3a3be19..3c71cea91e6 100644 --- a/packages/@react-spectrum/color/stories/ColorArea.stories.tsx +++ b/packages/@react-spectrum/color/stories/ColorArea.stories.tsx @@ -41,7 +41,7 @@ function ColorAreaExample(props: SpectrumColorAreaProps) { let [color, setColor] = useState(props.defaultValue || parseColor('#ff00ff')); return (
- + Date: Fri, 22 Oct 2021 17:23:32 -0700 Subject: [PATCH 08/23] use snaptovalue and add page steps --- .../@react-aria/color/src/useColorArea.ts | 36 ++++++------ .../@react-aria/interactions/src/useMove.ts | 23 ++++++++ .../color/stories/ColorArea.stories.tsx | 14 ++++- .../color/test/ColorArea.test.tsx | 25 ++++---- packages/@react-stately/color/src/Color.ts | 16 +++--- .../color/src/useColorAreaState.ts | 57 ++++++------------- packages/@react-types/color/src/index.d.ts | 4 +- 7 files changed, 95 insertions(+), 80 deletions(-) diff --git a/packages/@react-aria/color/src/useColorArea.ts b/packages/@react-aria/color/src/useColorArea.ts index 1d039cad8e1..9f98023a2d6 100644 --- a/packages/@react-aria/color/src/useColorArea.ts +++ b/packages/@react-aria/color/src/useColorArea.ts @@ -75,28 +75,34 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i let {keyboardProps} = useKeyboard({ onKeyDown(e) { - if (!e.shiftKey && /^Arrow(?:Right|Left|Up|Down)$/.test(e.key)) { - return; - } + let isPage = e.shiftKey; switch (e.key) { case 'PageUp': + isPage = true; case 'ArrowUp': - stateRef.current.incrementY(); + case 'Up': + stateRef.current.incrementY(isPage); focusedInputRef.current = inputYRef.current; break; case 'PageDown': + isPage = true; case 'ArrowDown': - stateRef.current.decrementY(); + case 'Down': + stateRef.current.decrementY(isPage); focusedInputRef.current = inputYRef.current; break; case 'Home': + isPage = true; case 'ArrowLeft': - direction === 'rtl' ? stateRef.current.incrementY() : stateRef.current.decrementY(); + case 'Left': + direction === 'rtl' ? stateRef.current.incrementX(isPage) : stateRef.current.decrementX(isPage); focusedInputRef.current = inputXRef.current; break; case 'End': + isPage = true; case 'ArrowRight': - direction === 'rtl' ? stateRef.current.decrementY() : stateRef.current.incrementY(); + case 'Right': + direction === 'rtl' ? stateRef.current.decrementX(isPage) : stateRef.current.incrementX(isPage); focusedInputRef.current = inputXRef.current; break; } @@ -111,7 +117,7 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i let moveHandler = { onMoveStart() { currentPosition.current = null; - state.setDragging(true); + stateRef.current .setDragging(true); }, onMove({deltaX, deltaY, pointerType}) { if (currentPosition.current == null) { @@ -119,14 +125,6 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i } let {width, height} = containerRef.current.getBoundingClientRect(); if (pointerType === 'keyboard') { - deltaX = maxMinOrZero(deltaX, xChannelStep); - deltaY = maxMinOrZero(deltaY, yChannelStep); - if (deltaX !== 0) { - stateRef.current[`${deltaX > 0 ? 'increment' : 'decrement'}X`](); - } - if (deltaY !== 0) { - stateRef.current[`${deltaY < 0 ? 'increment' : 'decrement'}Y`](); - } // set the focused input based on which axis has the greater delta focusedInputRef.current = (deltaX !== 0 || deltaY !== 0) && Math.abs(deltaY) > Math.abs(deltaX) ? inputYRef.current : inputXRef.current; } @@ -138,7 +136,7 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i }, onMoveEnd() { isOnColorArea.current = undefined; - state.setDragging(false); + stateRef.current .setDragging(false); focusInput(focusedInputRef.current ? focusedInputRef : inputXRef); focusedInputRef.current = undefined; } @@ -276,7 +274,9 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i onThumbDown(e.changedTouches[0].identifier); } }) - }, movePropsThumb, keyboardProps); + }, keyboardProps, movePropsThumb); + // order matters, keyboard need to finish before move so that onChangeEnd is fired last + // after valueRef in stately has been updated let xInputLabellingProps = useLabels({ diff --git a/packages/@react-aria/interactions/src/useMove.ts b/packages/@react-aria/interactions/src/useMove.ts index d6ae2ace938..ba1b2d20943 100644 --- a/packages/@react-aria/interactions/src/useMove.ts +++ b/packages/@react-aria/interactions/src/useMove.ts @@ -204,6 +204,29 @@ export function useMove(props: MoveEvents): MoveResult { e.stopPropagation(); triggerKeyboardMove(0, 1); break; + // TODO: include page size as an option? these are page movements, also Shift + arrows count for that + // it can be different depending on the slider/s 2D sliders home/end&pageup/down are on two axis + // in 1D sliders, home/end go to min/max, vs pageup/down are page size increments + case 'PageUp': + e.preventDefault(); + e.stopPropagation(); + triggerKeyboardMove(0, 1); + break; + case 'PageDown': + e.preventDefault(); + e.stopPropagation(); + triggerKeyboardMove(0, -1); + break; + case 'Home': + e.preventDefault(); + e.stopPropagation(); + triggerKeyboardMove(1, 0); + break; + case 'End': + e.preventDefault(); + e.stopPropagation(); + triggerKeyboardMove(-1, 0); + break; } }; diff --git a/packages/@react-spectrum/color/stories/ColorArea.stories.tsx b/packages/@react-spectrum/color/stories/ColorArea.stories.tsx index 3c71cea91e6..814291e3a89 100644 --- a/packages/@react-spectrum/color/stories/ColorArea.stories.tsx +++ b/packages/@react-spectrum/color/stories/ColorArea.stories.tsx @@ -51,7 +51,17 @@ function ColorAreaExample(props: SpectrumColorAreaProps) { } setColor(e); }} /> - + { + if (props.onChange) { + props.onChange(e); + } + setColor(e); + }} + onChangeEnd={props.onChangeEnd} + channel={zChannel} + isDisabled={isDisabled} />
@@ -63,7 +73,7 @@ function ColorAreaExample(props: SpectrumColorAreaProps) { export let XBlueYGreen = Template.bind({}); XBlueYGreen.storyName = 'RGB xChannel="blue", yChannel="green"'; -XBlueYGreen.args = {xChannel: 'blue', yChannel: 'green', onChange: action('onChange')}; +XBlueYGreen.args = {xChannel: 'blue', yChannel: 'green', onChange: action('onChange'), onChangeEnd: action('onChangeEnd')}; export let XGreenYBlue = Template.bind({}); XGreenYBlue.storyName = 'RGB xChannel="green", yChannel="blue"'; diff --git a/packages/@react-spectrum/color/test/ColorArea.test.tsx b/packages/@react-spectrum/color/test/ColorArea.test.tsx index 2fa6a69c44d..ada0088e9ba 100644 --- a/packages/@react-spectrum/color/test/ColorArea.test.tsx +++ b/packages/@react-spectrum/color/test/ColorArea.test.tsx @@ -128,26 +128,29 @@ describe('ColorArea', () => { describe('keyboard events', () => { it.each` Name | props | actions | result - ${'left/right'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'Left'}), backward: (elem) => pressKey(elem, {key: 'Right'})}} | ${parseColor('#ff00fe')} - ${'up/down'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'Up'}), backward: (elem) => pressKey(elem, {key: 'Down'})}} | ${parseColor('#ff01ff')} + ${'left/right'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'ArrowLeft'}), backward: (elem) => pressKey(elem, {key: 'ArrowRight'})}} | ${parseColor('#ff00fe')} + ${'up/down'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'ArrowUp'}), backward: (elem) => pressKey(elem, {key: 'ArrowDown'})}} | ${parseColor('#ff01ff')} + ${'shiftleft/shiftright'} | ${{defaultValue: parseColor('#f000f0')}} | ${{forward: (elem) => pressKey(elem, {key: 'ArrowLeft', shiftKey: true}), backward: (elem) => pressKey(elem, {key: 'ArrowRight', shiftKey: true})}} | ${parseColor('#f000e0')} + ${'shiftup/shiftdown'} | ${{defaultValue: parseColor('#f000f0')}} | ${{forward: (elem) => pressKey(elem, {key: 'ArrowUp', shiftKey: true}), backward: (elem) => pressKey(elem, {key: 'ArrowDown', shiftKey: true})}} | ${parseColor('#f010f0')} + ${'pageup/pagedown'} | ${{defaultValue: parseColor('#f000f0')}} | ${{forward: (elem) => pressKey(elem, {key: 'PageUp'}), backward: (elem) => pressKey(elem, {key: 'PageDown'})}} | ${parseColor('#f010f0')} + ${'home/end'} | ${{defaultValue: parseColor('#f000f0')}} | ${{forward: (elem) => pressKey(elem, {key: 'Home'}), backward: (elem) => pressKey(elem, {key: 'End'})}} | ${parseColor('#f000e0')} `('$Name', ({props, actions: {forward, backward}, result}) => { let {getAllByRole} = render( ); let sliders = getAllByRole('slider'); userEvent.tab(); - forward(sliders[0], {key: 'Left'}); + forward(sliders[0]); expect(onChangeSpy).toHaveBeenCalledTimes(1); expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(result.toString('rgba')); expect(onChangeEndSpy).toHaveBeenCalledTimes(1); expect(onChangeEndSpy.mock.calls[0][0].toString('rgba')).toBe(result.toString('rgba')); - backward(sliders[0], {key: 'Right'}); + backward(sliders[0]); expect(onChangeSpy).toHaveBeenCalledTimes(2); expect(onChangeSpy.mock.calls[1][0].toString('rgba')).toBe(props.defaultValue.toString('rgba')); expect(onChangeEndSpy).toHaveBeenCalledTimes(2); @@ -169,18 +172,18 @@ describe('ColorArea', () => { userEvent.tab(); expect(buttonA).toBe(document.activeElement); - pressKey(sliders[0], {key: 'Left'}); + pressKey(sliders[0], {key: 'LeftArrow'}); expect(onChangeSpy).not.toHaveBeenCalled(); expect(onChangeEndSpy).not.toHaveBeenCalled(); - pressKey(sliders[0], {key: 'Right'}); + pressKey(sliders[0], {key: 'RightArrow'}); expect(onChangeSpy).not.toHaveBeenCalled(); expect(onChangeEndSpy).not.toHaveBeenCalled(); }); it.each` Name | props | actions | result - ${'left/right'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'Left'}), backward: (elem) => pressKey(elem, {key: 'Right'})}} | ${parseColor('#ff00f0')} - ${'up/down'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'Up'}), backward: (elem) => pressKey(elem, {key: 'Down'})}} | ${parseColor('#ff0fff')} + ${'left/right'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'ArrowLeft'}), backward: (elem) => pressKey(elem, {key: 'ArrowRight'})}} | ${parseColor('#ff00f0')} + ${'up/down'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'ArrowUp'}), backward: (elem) => pressKey(elem, {key: 'ArrowDown'})}} | ${parseColor('#ff0fff')} `('$Name with step', ({props, actions: {forward, backward}, result}) => { let {getAllByRole} = render( { let sliders = getAllByRole('slider'); userEvent.tab(); - forward(sliders[0], {key: 'Left'}); + forward(sliders[0]); expect(onChangeSpy).toHaveBeenCalledTimes(1); expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(result.toString('rgba')); expect(onChangeEndSpy).toHaveBeenCalledTimes(1); expect(onChangeEndSpy.mock.calls[0][0].toString('rgba')).toBe(result.toString('rgba')); - backward(sliders[0], {key: 'Right'}); + backward(sliders[0]); expect(onChangeSpy).toHaveBeenCalledTimes(2); expect(onChangeSpy.mock.calls[1][0].toString('rgba')).toBe(props.defaultValue.toString('rgba')); expect(onChangeEndSpy).toHaveBeenCalledTimes(2); diff --git a/packages/@react-stately/color/src/Color.ts b/packages/@react-stately/color/src/Color.ts index b45e6947ab8..ddf29410103 100644 --- a/packages/@react-stately/color/src/Color.ts +++ b/packages/@react-stately/color/src/Color.ts @@ -145,9 +145,9 @@ class RGBColor extends Color { case 'red': case 'green': case 'blue': - return {minValue: 0, maxValue: 255, step: 1}; + return {minValue: 0, maxValue: 255, step: 1, pageSize: 0x10}; case 'alpha': - return {minValue: 0, maxValue: 1, step: 0.01}; + return {minValue: 0, maxValue: 1, step: 0.01, pageSize: 0.1}; default: throw new Error('Unknown color channel: ' + channel); } @@ -241,12 +241,12 @@ class HSBColor extends Color { getChannelRange(channel: ColorChannel): ColorChannelRange { switch (channel) { case 'hue': - return {minValue: 0, maxValue: 360, step: 1}; + return {minValue: 0, maxValue: 360, step: 1, pageSize: 15}; case 'saturation': case 'brightness': - return {minValue: 0, maxValue: 100, step: 1}; + return {minValue: 0, maxValue: 100, step: 1, pageSize: 10}; case 'alpha': - return {minValue: 0, maxValue: 1, step: 0.01}; + return {minValue: 0, maxValue: 1, step: 0.01, pageSize: 0.1}; default: throw new Error('Unknown color channel: ' + channel); } @@ -328,12 +328,12 @@ class HSLColor extends Color { getChannelRange(channel: ColorChannel): ColorChannelRange { switch (channel) { case 'hue': - return {minValue: 0, maxValue: 360, step: 1}; + return {minValue: 0, maxValue: 360, step: 1, pageSize: 15}; case 'saturation': case 'lightness': - return {minValue: 0, maxValue: 100, step: 1}; + return {minValue: 0, maxValue: 100, step: 1, pageSize: 10}; case 'alpha': - return {minValue: 0, maxValue: 1, step: 0.01}; + return {minValue: 0, maxValue: 1, step: 0.01, pageSize: 0.1}; default: throw new Error('Unknown color channel: ' + channel); } diff --git a/packages/@react-stately/color/src/useColorAreaState.ts b/packages/@react-stately/color/src/useColorAreaState.ts index a37017a7886..0d0589bb843 100644 --- a/packages/@react-stately/color/src/useColorAreaState.ts +++ b/packages/@react-stately/color/src/useColorAreaState.ts @@ -38,14 +38,14 @@ export interface ColorAreaState { getThumbPosition(): {x: number, y: number}, /** Increments the value of the horizontal axis channel by the given amount (defaults to 1). */ - incrementX(): void, + incrementX(isMultistep?: boolean): void, /** Decrements the value of the horizontal axis channel by the given amount (defaults to 1). */ - decrementX(): void, + decrementX(isMultistep?: boolean): void, /** Increments the value of the vertical axis channel by the given amount (defaults to 1). */ - incrementY(): void, + incrementY(isMultistep?: boolean): void, /** Decrements the value of the vertical axis channel by the given amount (defaults to 1). */ - decrementY(): void, + decrementY(isMultistep?: boolean): void, /** Whether the color area is currently being dragged. */ readonly isDragging: boolean, @@ -70,6 +70,7 @@ let difference = (a: Set, b: Set): Set => new Set([...a].filter(x => * Color area allows users to adjust two channels of an HSL, HSB or RGB color value against a two-dimensional gradient background. */ export function useColorAreaState(props: ColorAreaProps): ColorAreaState { + // TODO: docs say the step props should be one, but should it be two different values? let {value, defaultValue, xChannel, yChannel, onChange, onChangeEnd, xChannelStep, yChannelStep} = props; if (!value && !defaultValue) { @@ -176,49 +177,25 @@ export function useColorAreaState(props: ColorAreaProps): ColorAreaState { let y = 1 - (yValue - minValueY) / (maxValueY - minValueY); return {x, y}; }, - incrementX() { + incrementX(isMultistep) { let range = color.getChannelRange(channels.xChannel); - let stepSize = xChannelStep; - setXValue( - clamp( - (Math.floor(xValue / stepSize) + 1) * stepSize, - range.minValue, - range.maxValue - ) - ); + let stepSize = isMultistep ? Math.max(range.pageSize, xChannelStep) : xChannelStep; + setXValue(snapValueToStep(xValue + stepSize, range.minValue, range.maxValue, stepSize)); }, - incrementY() { + incrementY(isMultistep) { let range = color.getChannelRange(channels.yChannel); - let stepSize = yChannelStep; - setYValue( - clamp( - (Math.floor(yValue / stepSize) + 1) * stepSize, - range.minValue, - range.maxValue - ) - ); + let stepSize = isMultistep ? Math.max(range.pageSize, yChannelStep) : yChannelStep; + setYValue(snapValueToStep(yValue + stepSize, range.minValue, range.maxValue, stepSize)); }, - decrementX() { + decrementX(isMultistep) { let range = color.getChannelRange(channels.xChannel); - let stepSize = xChannelStep; - setXValue( - clamp( - (Math.ceil(xValue / stepSize) - 1) * stepSize, - range.minValue, - range.maxValue - ) - ); + let stepSize = isMultistep ? Math.max(range.pageSize, xChannelStep) : xChannelStep; + setXValue(snapValueToStep(xValue - stepSize, range.minValue, range.maxValue, stepSize)); }, - decrementY() { + decrementY(isMultistep) { let range = color.getChannelRange(channels.yChannel); - let stepSize = yChannelStep; - setYValue( - clamp( - (Math.ceil(yValue / stepSize) - 1) * stepSize, - range.minValue, - range.maxValue - ) - ); + let stepSize = isMultistep ? Math.max(range.pageSize, yChannelStep) : yChannelStep; + setYValue(snapValueToStep(yValue - stepSize, range.minValue, range.maxValue, stepSize)); }, setDragging(isDragging) { let wasDragging = isDraggingRef; diff --git a/packages/@react-types/color/src/index.d.ts b/packages/@react-types/color/src/index.d.ts index c9165d49172..23bbbe85485 100644 --- a/packages/@react-types/color/src/index.d.ts +++ b/packages/@react-types/color/src/index.d.ts @@ -40,7 +40,9 @@ export interface ColorChannelRange { /** The maximum value of the color channel. */ maxValue: number, /** The step value of the color channel, used when incrementing and decrementing. */ - step: number + step: number, + /** The page step value of the color channel, used when incrementing and decrementing. */ + pageSize: number } /** Represents a color value. */ From f1f734f95a8ff03566f38906e0d3b31c4d565efe Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Fri, 22 Oct 2021 17:25:43 -0700 Subject: [PATCH 09/23] remove dead code --- packages/@react-aria/color/src/useColorArea.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/@react-aria/color/src/useColorArea.ts b/packages/@react-aria/color/src/useColorArea.ts index 9f98023a2d6..9b889f7b786 100644 --- a/packages/@react-aria/color/src/useColorArea.ts +++ b/packages/@react-aria/color/src/useColorArea.ts @@ -36,14 +36,6 @@ interface ColorAreaAria { yInputProps: InputHTMLAttributes } - -function maxMinOrZero(value1: number, value2: number): number { - if (value1 === 0) { - return 0; - } - return value1 < 0 ? Math.min(value1, -1 * value2) : Math.max(value1, value2); -} - /** * Provides the behavior and accessibility implementation for a color wheel component. * Color wheels allow users to adjust the hue of an HSL or HSB color value on a circular track. From 7cb5b88f72f8448b81dd8d0e2b376981adffe1ae Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Fri, 22 Oct 2021 17:28:59 -0700 Subject: [PATCH 10/23] rename variable to make more sense, fix description --- .../color/src/useColorAreaState.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/@react-stately/color/src/useColorAreaState.ts b/packages/@react-stately/color/src/useColorAreaState.ts index 0d0589bb843..6c25fbeb29d 100644 --- a/packages/@react-stately/color/src/useColorAreaState.ts +++ b/packages/@react-stately/color/src/useColorAreaState.ts @@ -37,15 +37,15 @@ export interface ColorAreaState { /** Returns the coordinates of the thumb relative to the upper left corner of the color area as a percentage. */ getThumbPosition(): {x: number, y: number}, - /** Increments the value of the horizontal axis channel by the given amount (defaults to 1). */ - incrementX(isMultistep?: boolean): void, - /** Decrements the value of the horizontal axis channel by the given amount (defaults to 1). */ - decrementX(isMultistep?: boolean): void, + /** Increments the value of the horizontal axis channel by the channel step or page amount. */ + incrementX(isPageStep?: boolean): void, + /** Decrements the value of the horizontal axis channel by the channel step or page amount. */ + decrementX(isPageStep?: boolean): void, - /** Increments the value of the vertical axis channel by the given amount (defaults to 1). */ - incrementY(isMultistep?: boolean): void, - /** Decrements the value of the vertical axis channel by the given amount (defaults to 1). */ - decrementY(isMultistep?: boolean): void, + /** Increments the value of the vertical axis channel by the channel step or page amount. */ + incrementY(isPageStep?: boolean): void, + /** Decrements the value of the vertical axis channel by the channel step or page amount. */ + decrementY(isPageStep?: boolean): void, /** Whether the color area is currently being dragged. */ readonly isDragging: boolean, @@ -177,24 +177,24 @@ export function useColorAreaState(props: ColorAreaProps): ColorAreaState { let y = 1 - (yValue - minValueY) / (maxValueY - minValueY); return {x, y}; }, - incrementX(isMultistep) { + incrementX(isPageStep) { let range = color.getChannelRange(channels.xChannel); - let stepSize = isMultistep ? Math.max(range.pageSize, xChannelStep) : xChannelStep; + let stepSize = isPageStep ? Math.max(range.pageSize, xChannelStep) : xChannelStep; setXValue(snapValueToStep(xValue + stepSize, range.minValue, range.maxValue, stepSize)); }, - incrementY(isMultistep) { + incrementY(isPageStep) { let range = color.getChannelRange(channels.yChannel); - let stepSize = isMultistep ? Math.max(range.pageSize, yChannelStep) : yChannelStep; + let stepSize = isPageStep ? Math.max(range.pageSize, yChannelStep) : yChannelStep; setYValue(snapValueToStep(yValue + stepSize, range.minValue, range.maxValue, stepSize)); }, - decrementX(isMultistep) { + decrementX(isPageStep) { let range = color.getChannelRange(channels.xChannel); - let stepSize = isMultistep ? Math.max(range.pageSize, xChannelStep) : xChannelStep; + let stepSize = isPageStep ? Math.max(range.pageSize, xChannelStep) : xChannelStep; setXValue(snapValueToStep(xValue - stepSize, range.minValue, range.maxValue, stepSize)); }, - decrementY(isMultistep) { + decrementY(isPageStep) { let range = color.getChannelRange(channels.yChannel); - let stepSize = isMultistep ? Math.max(range.pageSize, yChannelStep) : yChannelStep; + let stepSize = isPageStep ? Math.max(range.pageSize, yChannelStep) : yChannelStep; setYValue(snapValueToStep(yValue - stepSize, range.minValue, range.maxValue, stepSize)); }, setDragging(isDragging) { From 87da7c44171c0593d1ec79d3a6e84e1b4e6a6229 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Fri, 22 Oct 2021 17:37:06 -0700 Subject: [PATCH 11/23] Add isDisabled based on Spectrum designs --- .../@react-spectrum/color/src/ColorArea.tsx | 115 +++++++++--------- 1 file changed, 58 insertions(+), 57 deletions(-) diff --git a/packages/@react-spectrum/color/src/ColorArea.tsx b/packages/@react-spectrum/color/src/ColorArea.tsx index bc71fa2510e..2ecfd794774 100644 --- a/packages/@react-spectrum/color/src/ColorArea.tsx +++ b/packages/@react-spectrum/color/src/ColorArea.tsx @@ -45,7 +45,7 @@ function ColorArea(props: SpectrumColorAreaProps, ref: FocusableRef Date: Fri, 22 Oct 2021 17:40:48 -0700 Subject: [PATCH 12/23] add min size --- .../@adobe/spectrum-css-temp/components/colorarea/index.css | 6 ++++-- .../@react-spectrum/color/stories/ColorArea.stories.tsx | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/@adobe/spectrum-css-temp/components/colorarea/index.css b/packages/@adobe/spectrum-css-temp/components/colorarea/index.css index db0c9c08907..3b750639bfe 100644 --- a/packages/@adobe/spectrum-css-temp/components/colorarea/index.css +++ b/packages/@adobe/spectrum-css-temp/components/colorarea/index.css @@ -1,8 +1,10 @@ .spectrum-ColorArea { position: relative; display: inline-block; - width: var(--spectrum-colorarea-default-width); - height: var(--spectrum-colorarea-default-height); + inline-size: var(--spectrum-colorarea-default-width); + block-size: var(--spectrum-colorarea-default-height); + min-inline-size: var(--spectrum-colorarea-min-width); + min-block-size: var(--spectrum-colorarea-min-height); border-radius: var(--spectrum-colorarea-border-radius); diff --git a/packages/@react-spectrum/color/stories/ColorArea.stories.tsx b/packages/@react-spectrum/color/stories/ColorArea.stories.tsx index 814291e3a89..9582336e912 100644 --- a/packages/@react-spectrum/color/stories/ColorArea.stories.tsx +++ b/packages/@react-spectrum/color/stories/ColorArea.stories.tsx @@ -112,3 +112,7 @@ XBlueYGreenAriaLabelled.args = {...XBlueYGreen.args, label: undefined, ariaLabel export let XBlueYGreenSize3000 = Template.bind({}); XBlueYGreenSize3000.storyName = 'RGB xChannel="blue", yChannel="green", size="size-3000"'; XBlueYGreenSize3000.args = {...XBlueYGreen.args, size: 'size-3000'}; + +export let XBlueYGreenSize600 = Template.bind({}); +XBlueYGreenSize600.storyName = 'RGB xChannel="blue", yChannel="green", size="size-600"'; +XBlueYGreenSize600.args = {...XBlueYGreen.args, size: 'size-600'}; From 2bb5b2a5386ac5cd40e121057e0e7c0a1d53e02d Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Fri, 22 Oct 2021 17:44:11 -0700 Subject: [PATCH 13/23] add prop description --- packages/@react-types/color/src/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@react-types/color/src/index.d.ts b/packages/@react-types/color/src/index.d.ts index 23bbbe85485..d84e0c1380a 100644 --- a/packages/@react-types/color/src/index.d.ts +++ b/packages/@react-types/color/src/index.d.ts @@ -157,5 +157,6 @@ export interface ColorAreaProps extends ValueBase { export interface AriaColorAreaProps extends ColorAreaProps, DOMProps, AriaLabelingProps {} export interface SpectrumColorAreaProps extends AriaColorAreaProps, Omit { + /** Size of the Color Area. */ size?: DimensionValue } From 9c2769f417054b07abb332f3431dcccc1ba2740e Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Mon, 25 Oct 2021 11:22:12 -0700 Subject: [PATCH 14/23] Get back to no useMove changes --- .../@react-aria/color/src/useColorArea.ts | 16 +++++++++-- .../@react-aria/interactions/src/useMove.ts | 27 +++---------------- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/packages/@react-aria/color/src/useColorArea.ts b/packages/@react-aria/color/src/useColorArea.ts index 9b889f7b786..886b3b81fe8 100644 --- a/packages/@react-aria/color/src/useColorArea.ts +++ b/packages/@react-aria/color/src/useColorArea.ts @@ -67,6 +67,11 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i let {keyboardProps} = useKeyboard({ onKeyDown(e) { + // these are the cases that useMove doesn't handle + if (!((e.shiftKey && /^Arrow(?:Right|Left|Up|Down)$/.test(e.key)) || /^(PageUp|PageDown|Home|End)$/.test(e.key))) { + return; + } + stateRef.current.setDragging(true); let isPage = e.shiftKey; switch (e.key) { case 'PageUp': @@ -98,6 +103,7 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i focusedInputRef.current = inputXRef.current; break; } + stateRef.current.setDragging(false); if (focusedInputRef.current) { e.preventDefault(); focusInput(focusedInputRef.current ? focusedInputRef : inputXRef); @@ -109,7 +115,7 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i let moveHandler = { onMoveStart() { currentPosition.current = null; - stateRef.current .setDragging(true); + stateRef.current.setDragging(true); }, onMove({deltaX, deltaY, pointerType}) { if (currentPosition.current == null) { @@ -117,6 +123,12 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i } let {width, height} = containerRef.current.getBoundingClientRect(); if (pointerType === 'keyboard') { + if (deltaX > 0 || deltaX < 0) { + stateRef.current[`${deltaX > 0 ? 'increment' : 'decrement'}X`](); + } + if (deltaY > 0 || deltaY < 0) { + stateRef.current[`${deltaY < 0 ? 'increment' : 'decrement'}Y`](); + } // set the focused input based on which axis has the greater delta focusedInputRef.current = (deltaX !== 0 || deltaY !== 0) && Math.abs(deltaY) > Math.abs(deltaX) ? inputYRef.current : inputXRef.current; } @@ -128,7 +140,7 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i }, onMoveEnd() { isOnColorArea.current = undefined; - stateRef.current .setDragging(false); + stateRef.current.setDragging(false); focusInput(focusedInputRef.current ? focusedInputRef : inputXRef); focusedInputRef.current = undefined; } diff --git a/packages/@react-aria/interactions/src/useMove.ts b/packages/@react-aria/interactions/src/useMove.ts index ba1b2d20943..bf12333ecd1 100644 --- a/packages/@react-aria/interactions/src/useMove.ts +++ b/packages/@react-aria/interactions/src/useMove.ts @@ -179,6 +179,10 @@ export function useMove(props: MoveEvents): MoveResult { }; moveProps.onKeyDown = (e) => { + // don't want useMove to handle shift key + arrow events because it doesn't do anything + if (e.shiftKey) { + return; + } switch (e.key) { case 'Left': case 'ArrowLeft': @@ -204,29 +208,6 @@ export function useMove(props: MoveEvents): MoveResult { e.stopPropagation(); triggerKeyboardMove(0, 1); break; - // TODO: include page size as an option? these are page movements, also Shift + arrows count for that - // it can be different depending on the slider/s 2D sliders home/end&pageup/down are on two axis - // in 1D sliders, home/end go to min/max, vs pageup/down are page size increments - case 'PageUp': - e.preventDefault(); - e.stopPropagation(); - triggerKeyboardMove(0, 1); - break; - case 'PageDown': - e.preventDefault(); - e.stopPropagation(); - triggerKeyboardMove(0, -1); - break; - case 'Home': - e.preventDefault(); - e.stopPropagation(); - triggerKeyboardMove(1, 0); - break; - case 'End': - e.preventDefault(); - e.stopPropagation(); - triggerKeyboardMove(-1, 0); - break; } }; From 7d49f0f043e3963c98c2085899b53cac788c0e9d Mon Sep 17 00:00:00 2001 From: Michael Jordan Date: Mon, 8 Nov 2021 17:15:18 -0500 Subject: [PATCH 15/23] Combine the useMove frequently appears with useKeyboard (#2508) * Combine the useMove frequently appears with useKeyboard * fix merge mistake * Remove isPage from useMove, add modifier keys * continue propagation for keys we don't handle * have aria pass through the step size Co-authored-by: Rob Snow --- .../@react-aria/color/src/useColorArea.ts | 52 ++++++-------- .../@react-aria/color/src/useColorWheel.ts | 51 ++++++++------ .../color/test/useColorWheel.test.tsx | 10 +-- .../@react-aria/interactions/src/useMove.ts | 67 +++++++++++-------- .../interactions/test/useMove.test.js | 39 ++++++----- .../color/test/ColorWheel.test.tsx | 58 +++++++++++++--- .../color/src/useColorAreaState.ts | 26 +++---- .../color/src/useColorWheelState.ts | 30 ++++----- packages/@react-types/shared/src/events.d.ts | 11 ++- 9 files changed, 200 insertions(+), 144 deletions(-) diff --git a/packages/@react-aria/color/src/useColorArea.ts b/packages/@react-aria/color/src/useColorArea.ts index 886b3b81fe8..9dcb821ec21 100644 --- a/packages/@react-aria/color/src/useColorArea.ts +++ b/packages/@react-aria/color/src/useColorArea.ts @@ -68,44 +68,34 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i let {keyboardProps} = useKeyboard({ onKeyDown(e) { // these are the cases that useMove doesn't handle - if (!((e.shiftKey && /^Arrow(?:Right|Left|Up|Down)$/.test(e.key)) || /^(PageUp|PageDown|Home|End)$/.test(e.key))) { + if (!/^(PageUp|PageDown|Home|End)$/.test(e.key)) { + e.continuePropagation(); return; } + // same handling as useMove, don't need to stop propagation, useKeyboard will do that for us + e.preventDefault(); + // remember to set this and unset it so that onChangeEnd is fired stateRef.current.setDragging(true); - let isPage = e.shiftKey; switch (e.key) { case 'PageUp': - isPage = true; - case 'ArrowUp': - case 'Up': - stateRef.current.incrementY(isPage); + stateRef.current.incrementY(stateRef.current.yChannelPageStep); focusedInputRef.current = inputYRef.current; break; case 'PageDown': - isPage = true; - case 'ArrowDown': - case 'Down': - stateRef.current.decrementY(isPage); + stateRef.current.decrementY(stateRef.current.yChannelPageStep); focusedInputRef.current = inputYRef.current; break; case 'Home': - isPage = true; - case 'ArrowLeft': - case 'Left': - direction === 'rtl' ? stateRef.current.incrementX(isPage) : stateRef.current.decrementX(isPage); + direction === 'rtl' ? stateRef.current.incrementX(stateRef.current.xChannelPageStep) : stateRef.current.decrementX(stateRef.current.xChannelPageStep); focusedInputRef.current = inputXRef.current; break; case 'End': - isPage = true; - case 'ArrowRight': - case 'Right': - direction === 'rtl' ? stateRef.current.decrementX(isPage) : stateRef.current.incrementX(isPage); + direction === 'rtl' ? stateRef.current.decrementX(stateRef.current.xChannelPageStep) : stateRef.current.incrementX(stateRef.current.xChannelPageStep); focusedInputRef.current = inputXRef.current; break; } stateRef.current.setDragging(false); if (focusedInputRef.current) { - e.preventDefault(); focusInput(focusedInputRef.current ? focusedInputRef : inputXRef); focusedInputRef.current = undefined; } @@ -117,24 +107,26 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i currentPosition.current = null; stateRef.current.setDragging(true); }, - onMove({deltaX, deltaY, pointerType}) { + onMove({deltaX, deltaY, pointerType, shiftKey}) { if (currentPosition.current == null) { currentPosition.current = stateRef.current.getThumbPosition(); } let {width, height} = containerRef.current.getBoundingClientRect(); if (pointerType === 'keyboard') { - if (deltaX > 0 || deltaX < 0) { - stateRef.current[`${deltaX > 0 ? 'increment' : 'decrement'}X`](); - } - if (deltaY > 0 || deltaY < 0) { - stateRef.current[`${deltaY < 0 ? 'increment' : 'decrement'}Y`](); + if (deltaX > 0) { + stateRef.current.incrementX(shiftKey ? stateRef.current.xChannelPageStep : stateRef.current.xChannelStep); + } else if (deltaX < 0) { + stateRef.current.decrementX(shiftKey ? stateRef.current.xChannelPageStep : stateRef.current.xChannelStep); + } else if (deltaY > 0) { + stateRef.current.decrementY(shiftKey ? stateRef.current.yChannelPageStep : stateRef.current.yChannelStep); + } else if (deltaY < 0) { + stateRef.current.incrementY(shiftKey ? stateRef.current.yChannelPageStep : stateRef.current.yChannelStep); } // set the focused input based on which axis has the greater delta focusedInputRef.current = (deltaX !== 0 || deltaY !== 0) && Math.abs(deltaY) > Math.abs(deltaX) ? inputYRef.current : inputXRef.current; - } - currentPosition.current.x += (direction === 'rtl' ? -1 : 1) * deltaX / width ; - currentPosition.current.y += deltaY / height; - if (pointerType !== 'keyboard') { + } else { + currentPosition.current.x += (direction === 'rtl' ? -1 : 1) * deltaX / width ; + currentPosition.current.y += deltaY / height; stateRef.current.setColorFromPoint(currentPosition.current.x, currentPosition.current.y); } }, @@ -279,8 +271,6 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i } }) }, keyboardProps, movePropsThumb); - // order matters, keyboard need to finish before move so that onChangeEnd is fired last - // after valueRef in stately has been updated let xInputLabellingProps = useLabels({ diff --git a/packages/@react-aria/color/src/useColorWheel.ts b/packages/@react-aria/color/src/useColorWheel.ts index ac49ee4d21a..8ea75aa7523 100644 --- a/packages/@react-aria/color/src/useColorWheel.ts +++ b/packages/@react-aria/color/src/useColorWheel.ts @@ -33,8 +33,6 @@ interface ColorWheelAria { inputProps: InputHTMLAttributes } -const PAGE_MIN_STEP_SIZE = 6; - /** * Provides the behavior and accessibility implementation for a color wheel component. * Color wheels allow users to adjust the hue of an HSL or HSB color value on a circular track. @@ -62,12 +60,38 @@ export function useColorWheel(props: ColorWheelAriaProps, state: ColorWheelState stateRef.current = state; let currentPosition = useRef<{x: number, y: number}>(null); + + let {keyboardProps} = useKeyboard({ + onKeyDown(e) { + // these are the cases that useMove doesn't handle + if (!/^(PageUp|PageDown)$/.test(e.key)) { + e.continuePropagation(); + return; + } + // same handling as useMove, don't need to stop propagation, useKeyboard will do that for us + e.preventDefault(); + // remember to set this and unset it so that onChangeEnd is fired + stateRef.current.setDragging(true); + switch (e.key) { + case 'PageUp': + e.preventDefault(); + state.increment(stateRef.current.pageStep); + break; + case 'PageDown': + e.preventDefault(); + state.decrement(stateRef.current.pageStep); + break; + } + stateRef.current.setDragging(false); + } + }); + let moveHandler = { onMoveStart() { currentPosition.current = null; state.setDragging(true); }, - onMove({deltaX, deltaY, pointerType}) { + onMove({deltaX, deltaY, pointerType, shiftKey}) { if (currentPosition.current == null) { currentPosition.current = stateRef.current.getThumbPosition(thumbRadius); } @@ -75,9 +99,9 @@ export function useColorWheel(props: ColorWheelAriaProps, state: ColorWheelState currentPosition.current.y += deltaY; if (pointerType === 'keyboard') { if (deltaX > 0 || deltaY < 0) { - state.increment(); + state.increment(shiftKey ? stateRef.current.pageStep : stateRef.current.step); } else if (deltaX < 0 || deltaY > 0) { - state.decrement(); + state.decrement(shiftKey ? stateRef.current.pageStep : stateRef.current.step); } } else { stateRef.current.setHueFromPoint(currentPosition.current.x, currentPosition.current.y, thumbRadius); @@ -169,21 +193,6 @@ export function useColorWheel(props: ColorWheelAriaProps, state: ColorWheelState } }; - let {keyboardProps} = useKeyboard({ - onKeyDown(e) { - switch (e.key) { - case 'PageUp': - e.preventDefault(); - state.increment(PAGE_MIN_STEP_SIZE); - break; - case 'PageDown': - e.preventDefault(); - state.decrement(PAGE_MIN_STEP_SIZE); - break; - } - } - }); - let trackInteractions = isDisabled ? {} : mergeProps({ onMouseDown: (e: React.MouseEvent) => { if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey) { @@ -218,7 +227,7 @@ export function useColorWheel(props: ColorWheelAriaProps, state: ColorWheelState onTouchStart: (e: React.TouchEvent) => { onThumbDown(e.changedTouches[0].identifier); } - }, movePropsThumb, keyboardProps); + }, keyboardProps, movePropsThumb); let {x, y} = state.getThumbPosition(thumbRadius); // Provide a default aria-label if none is given diff --git a/packages/@react-aria/color/test/useColorWheel.test.tsx b/packages/@react-aria/color/test/useColorWheel.test.tsx index 3dc198621fc..13e94abf7a8 100644 --- a/packages/@react-aria/color/test/useColorWheel.test.tsx +++ b/packages/@react-aria/color/test/useColorWheel.test.tsx @@ -52,24 +52,18 @@ function ColorWheel(props: ColorWheelProps) { describe('useColorWheel', () => { let onChangeSpy = jest.fn(); - afterEach(() => { - onChangeSpy.mockClear(); - }); - beforeAll(() => { // @ts-ignore - jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => cb()); - jest.useFakeTimers(); + jest.useFakeTimers('modern'); }); afterAll(() => { jest.useRealTimers(); - // @ts-ignore - window.requestAnimationFrame.mockRestore(); }); afterEach(() => { // for restoreTextSelection jest.runAllTimers(); + onChangeSpy.mockClear(); }); it('sets input props', () => { diff --git a/packages/@react-aria/interactions/src/useMove.ts b/packages/@react-aria/interactions/src/useMove.ts index bf12333ecd1..e514eb75979 100644 --- a/packages/@react-aria/interactions/src/useMove.ts +++ b/packages/@react-aria/interactions/src/useMove.ts @@ -20,6 +20,13 @@ interface MoveResult { moveProps: HTMLAttributes } +interface EventBase { + shiftKey: boolean, + ctrlKey: boolean, + metaKey: boolean, + altKey: boolean +} + /** * Handles move interactions across mouse, touch, and keyboard, including dragging with * the mouse or touch, and using the arrow keys. Normalizes behavior across browsers and @@ -43,7 +50,7 @@ export function useMove(props: MoveEvents): MoveResult { disableTextSelection(); state.current.didMove = false; }; - let move = (pointerType: PointerType, deltaX: number, deltaY: number) => { + let move = (originalEvent: EventBase, pointerType: PointerType, deltaX: number, deltaY: number) => { if (deltaX === 0 && deltaY === 0) { return; } @@ -52,22 +59,34 @@ export function useMove(props: MoveEvents): MoveResult { state.current.didMove = true; onMoveStart?.({ type: 'movestart', - pointerType + pointerType, + shiftKey: originalEvent.shiftKey, + metaKey: originalEvent.metaKey, + ctrlKey: originalEvent.ctrlKey, + altKey: originalEvent.altKey }); } onMove({ type: 'move', pointerType, deltaX: deltaX, - deltaY: deltaY + deltaY: deltaY, + shiftKey: originalEvent.shiftKey, + metaKey: originalEvent.metaKey, + ctrlKey: originalEvent.ctrlKey, + altKey: originalEvent.altKey }); }; - let end = (pointerType: PointerType) => { + let end = (originalEvent: EventBase, pointerType: PointerType) => { restoreTextSelection(); if (state.current.didMove) { onMoveEnd?.({ type: 'moveend', - pointerType + pointerType, + shiftKey: originalEvent.shiftKey, + metaKey: originalEvent.metaKey, + ctrlKey: originalEvent.ctrlKey, + altKey: originalEvent.altKey }); } }; @@ -75,13 +94,13 @@ export function useMove(props: MoveEvents): MoveResult { if (typeof PointerEvent === 'undefined') { let onMouseMove = (e: MouseEvent) => { if (e.button === 0) { - move('mouse', e.pageX - state.current.lastPosition.pageX, e.pageY - state.current.lastPosition.pageY); + move(e, 'mouse', e.pageX - state.current.lastPosition.pageX, e.pageY - state.current.lastPosition.pageY); state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY}; } }; let onMouseUp = (e: MouseEvent) => { if (e.button === 0) { - end('mouse'); + end(e, 'mouse'); removeGlobalListener(window, 'mousemove', onMouseMove, false); removeGlobalListener(window, 'mouseup', onMouseUp, false); } @@ -98,19 +117,17 @@ export function useMove(props: MoveEvents): MoveResult { }; let onTouchMove = (e: TouchEvent) => { - // @ts-ignore let touch = [...e.changedTouches].findIndex(({identifier}) => identifier === state.current.id); if (touch >= 0) { let {pageX, pageY} = e.changedTouches[touch]; - move('touch', pageX - state.current.lastPosition.pageX, pageY - state.current.lastPosition.pageY); + move(e, 'touch', pageX - state.current.lastPosition.pageX, pageY - state.current.lastPosition.pageY); state.current.lastPosition = {pageX, pageY}; } }; let onTouchEnd = (e: TouchEvent) => { - // @ts-ignore let touch = [...e.changedTouches].findIndex(({identifier}) => identifier === state.current.id); if (touch >= 0) { - end('touch'); + end(e, 'touch'); state.current.id = null; removeGlobalListener(window, 'touchmove', onTouchMove); removeGlobalListener(window, 'touchend', onTouchEnd); @@ -135,22 +152,20 @@ export function useMove(props: MoveEvents): MoveResult { } else { let onPointerMove = (e: PointerEvent) => { if (e.pointerId === state.current.id) { - // @ts-ignore - let pointerType: PointerType = e.pointerType || 'mouse'; + let pointerType = (e.pointerType || 'mouse') as PointerType; // Problems with PointerEvent#movementX/movementY: // 1. it is always 0 on macOS Safari. // 2. On Chrome Android, it's scaled by devicePixelRatio, but not on Chrome macOS - move(pointerType, e.pageX - state.current.lastPosition.pageX, e.pageY - state.current.lastPosition.pageY); + move(e, pointerType, e.pageX - state.current.lastPosition.pageX, e.pageY - state.current.lastPosition.pageY); state.current.lastPosition = {pageX: e.pageX, pageY: e.pageY}; } }; let onPointerUp = (e: PointerEvent) => { if (e.pointerId === state.current.id) { - // @ts-ignore - let pointerType: PointerType = e.pointerType || 'mouse'; - end(pointerType); + let pointerType = (e.pointerType || 'mouse') as PointerType; + end(e, pointerType); state.current.id = null; removeGlobalListener(window, 'pointermove', onPointerMove, false); removeGlobalListener(window, 'pointerup', onPointerUp, false); @@ -172,41 +187,37 @@ export function useMove(props: MoveEvents): MoveResult { }; } - let triggerKeyboardMove = (deltaX: number, deltaY: number) => { + let triggerKeyboardMove = (e: EventBase, deltaX: number, deltaY: number) => { start(); - move('keyboard', deltaX, deltaY); - end('keyboard'); + move(e, 'keyboard', deltaX, deltaY); + end(e, 'keyboard'); }; moveProps.onKeyDown = (e) => { - // don't want useMove to handle shift key + arrow events because it doesn't do anything - if (e.shiftKey) { - return; - } switch (e.key) { case 'Left': case 'ArrowLeft': e.preventDefault(); e.stopPropagation(); - triggerKeyboardMove(-1, 0); + triggerKeyboardMove(e, -1, 0); break; case 'Right': case 'ArrowRight': e.preventDefault(); e.stopPropagation(); - triggerKeyboardMove(1, 0); + triggerKeyboardMove(e, 1, 0); break; case 'Up': case 'ArrowUp': e.preventDefault(); e.stopPropagation(); - triggerKeyboardMove(0, -1); + triggerKeyboardMove(e, 0, -1); break; case 'Down': case 'ArrowDown': e.preventDefault(); e.stopPropagation(); - triggerKeyboardMove(0, 1); + triggerKeyboardMove(e, 0, 1); break; } }; diff --git a/packages/@react-aria/interactions/test/useMove.test.js b/packages/@react-aria/interactions/test/useMove.test.js index b097705aa2a..b2d43ff4ce7 100644 --- a/packages/@react-aria/interactions/test/useMove.test.js +++ b/packages/@react-aria/interactions/test/useMove.test.js @@ -36,6 +36,11 @@ describe('useMove', function () { // for restoreTextSelection jest.runAllTimers(); }); + let altKey = false; + let ctrlKey = false; + let metaKey = false; + let shiftKey = false; + let defaultModifiers = {altKey, ctrlKey, metaKey, shiftKey}; describe('mouse events', function () { installMouseEvent(); @@ -55,9 +60,9 @@ describe('useMove', function () { fireEvent.mouseDown(el, {button: 0, pageX: 1, pageY: 30}); expect(events).toStrictEqual([]); fireEvent.mouseMove(el, {button: 0, pageX: 10, pageY: 25}); - expect(events).toStrictEqual([{type: 'movestart', pointerType: 'mouse'}, {type: 'move', pointerType: 'mouse', deltaX: 9, deltaY: -5}]); + expect(events).toStrictEqual([{type: 'movestart', pointerType: 'mouse', ...defaultModifiers}, {type: 'move', pointerType: 'mouse', deltaX: 9, deltaY: -5, ...defaultModifiers}]); fireEvent.mouseUp(el); - expect(events).toStrictEqual([{type: 'movestart', pointerType: 'mouse'}, {type: 'move', pointerType: 'mouse', deltaX: 9, deltaY: -5}, {type: 'moveend', pointerType: 'mouse'}]); + expect(events).toStrictEqual([{type: 'movestart', pointerType: 'mouse', ...defaultModifiers}, {type: 'move', pointerType: 'mouse', deltaX: 9, deltaY: -5, ...defaultModifiers}, {type: 'moveend', pointerType: 'mouse', ...defaultModifiers}]); }); it('doesn\'t respond to right click', function () { @@ -114,9 +119,9 @@ describe('useMove', function () { fireEvent.touchStart(el, {changedTouches: [{identifier: 1, pageX: 1, pageY: 30}]}); expect(events).toStrictEqual([]); fireEvent.touchMove(el, {changedTouches: [{identifier: 1, pageX: 10, pageY: 25}]}); - expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch'}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5}]); + expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch', ...defaultModifiers}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5, ...defaultModifiers}]); fireEvent.touchEnd(el, {changedTouches: [{identifier: 1, pageX: 10, pageY: 25}]}); - expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch'}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5}, {type: 'moveend', pointerType: 'touch'}]); + expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch', ...defaultModifiers}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5, ...defaultModifiers}, {type: 'moveend', pointerType: 'touch', ...defaultModifiers}]); }); it('ends with touchcancel', function () { @@ -134,9 +139,9 @@ describe('useMove', function () { fireEvent.touchStart(el, {changedTouches: [{identifier: 1, pageX: 1, pageY: 30}]}); expect(events).toStrictEqual([]); fireEvent.touchMove(el, {changedTouches: [{identifier: 1, pageX: 10, pageY: 25}]}); - expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch'}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5}]); + expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch', ...defaultModifiers}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5, ...defaultModifiers}]); fireEvent.touchCancel(el, {changedTouches: [{identifier: 1, pageX: 10, pageY: 25}]}); - expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch'}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5}, {type: 'moveend', pointerType: 'touch'}]); + expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch', ...defaultModifiers}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5, ...defaultModifiers}, {type: 'moveend', pointerType: 'touch', ...defaultModifiers}]); }); it('doesn\'t fire anything when tapping', function () { @@ -174,9 +179,9 @@ describe('useMove', function () { fireEvent.touchEnd(el, {changedTouches: [{identifier: 2, pageX: 10, pageY: 40}]}); expect(events).toStrictEqual([]); fireEvent.touchMove(el, {changedTouches: [{identifier: 1, pageX: 10, pageY: 25}]}); - expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch'}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5}]); + expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch', ...defaultModifiers}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5, ...defaultModifiers}]); fireEvent.touchEnd(el, {changedTouches: [{identifier: 1, pageX: 10, pageY: 25}]}); - expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch'}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5}, {type: 'moveend', pointerType: 'touch'}]); + expect(events).toStrictEqual([{type: 'movestart', pointerType: 'touch', ...defaultModifiers}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5, ...defaultModifiers}, {type: 'moveend', pointerType: 'touch', ...defaultModifiers}]); }); }); @@ -265,9 +270,9 @@ describe('useMove', function () { fireEvent.touchStart(el, {changedTouches: [{identifier: 1, pageX: 1, pageY: 30}]}); expect(eventsChild).toStrictEqual([]); fireEvent.touchMove(el, {changedTouches: [{identifier: 1, pageX: 10, pageY: 25}]}); - expect(eventsChild).toStrictEqual([{type: 'movestart', pointerType: 'touch'}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5}]); + expect(eventsChild).toStrictEqual([{type: 'movestart', pointerType: 'touch', ...defaultModifiers}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5, ...defaultModifiers}]); fireEvent.touchEnd(el, {changedTouches: [{identifier: 1, pageX: 10, pageY: 25}]}); - expect(eventsChild).toStrictEqual([{type: 'movestart', pointerType: 'touch'}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5}, {type: 'moveend', pointerType: 'touch'}]); + expect(eventsChild).toStrictEqual([{type: 'movestart', pointerType: 'touch', ...defaultModifiers}, {type: 'move', pointerType: 'touch', deltaX: 9, deltaY: -5, ...defaultModifiers}, {type: 'moveend', pointerType: 'touch', ...defaultModifiers}]); expect(eventsParent).toStrictEqual([]); }); @@ -292,7 +297,7 @@ describe('useMove', function () { let el = tree.getByTestId(EXAMPLE_ELEMENT_TESTID); fireEvent.keyDown(el, {key: Key}); - expect(events).toStrictEqual([{type: 'movestart', pointerType: 'keyboard'}, {type: 'move', pointerType: 'keyboard', ...Result}, {type: 'moveend', pointerType: 'keyboard'}]); + expect(events).toStrictEqual([{type: 'movestart', pointerType: 'keyboard', ...defaultModifiers}, {type: 'move', pointerType: 'keyboard', ...defaultModifiers, ...Result}, {type: 'moveend', pointerType: 'keyboard', ...defaultModifiers}]); }); it('allows handling other key events', function () { @@ -333,9 +338,9 @@ describe('useMove', function () { fireEvent.pointerDown(el, {pointerType: 'pen', pointerId: 1, pageX: 1, pageY: 30}); expect(events).toStrictEqual([]); fireEvent.pointerMove(el, {pointerType: 'pen', pointerId: 1, pageX: 10, pageY: 25}); - expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen'}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5}]); + expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen', ...defaultModifiers}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5, ...defaultModifiers}]); fireEvent.pointerUp(el, {pointerType: 'pen', pointerId: 1}); - expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen'}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5}, {type: 'moveend', pointerType: 'pen'}]); + expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen', ...defaultModifiers}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5, ...defaultModifiers}, {type: 'moveend', pointerType: 'pen', ...defaultModifiers}]); }); it('doesn\'t respond to right click', function () { @@ -373,9 +378,9 @@ describe('useMove', function () { fireEvent.pointerDown(el, {pointerType: 'pen', pointerId: 1, pageX: 1, pageY: 30}); expect(events).toStrictEqual([]); fireEvent.pointerMove(el, {pointerType: 'pen', pointerId: 1, pageX: 10, pageY: 25}); - expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen'}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5}]); + expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen', ...defaultModifiers}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5, ...defaultModifiers}]); fireEvent.pointerCancel(el, {pointerType: 'pen', pointerId: 1}); - expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen'}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5}, {type: 'moveend', pointerType: 'pen'}]); + expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen', ...defaultModifiers}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5, ...defaultModifiers}, {type: 'moveend', pointerType: 'pen', ...defaultModifiers}]); }); it('doesn\'t fire anything when tapping', function () { @@ -415,9 +420,9 @@ describe('useMove', function () { expect(events).toStrictEqual([]); fireEvent.pointerMove(el, {pointerType: 'pen', pointerId: 1, pageX: 10, pageY: 25}); - expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen'}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5}]); + expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen', ...defaultModifiers}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5, ...defaultModifiers}]); fireEvent.pointerUp(el, {pointerType: 'pen', pointerId: 1}); - expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen'}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5}, {type: 'moveend', pointerType: 'pen'}]); + expect(events).toStrictEqual([{type: 'movestart', pointerType: 'pen', ...defaultModifiers}, {type: 'move', pointerType: 'pen', deltaX: 9, deltaY: -5, ...defaultModifiers}, {type: 'moveend', pointerType: 'pen', ...defaultModifiers}]); }); }); }); diff --git a/packages/@react-spectrum/color/test/ColorWheel.test.tsx b/packages/@react-spectrum/color/test/ColorWheel.test.tsx index 48e73b5f553..079653575c6 100644 --- a/packages/@react-spectrum/color/test/ColorWheel.test.tsx +++ b/packages/@react-spectrum/color/test/ColorWheel.test.tsx @@ -33,28 +33,22 @@ describe('ColorWheel', () => { let onChangeSpy = jest.fn(); let onChangeEndSpy = jest.fn(); - afterEach(() => { - onChangeSpy.mockClear(); - onChangeEndSpy.mockClear(); - }); - beforeAll(() => { jest.spyOn(window.HTMLElement.prototype, 'offsetWidth', 'get').mockImplementation(() => SIZE); // @ts-ignore - jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => cb()); - jest.useFakeTimers(); + jest.useFakeTimers('modern'); }); afterAll(() => { // @ts-ignore window.HTMLElement.prototype.offsetWidth.mockReset(); jest.useRealTimers(); - // @ts-ignore - window.requestAnimationFrame.mockReset(); }); afterEach(() => { // for restoreTextSelection jest.runAllTimers(); + onChangeSpy.mockClear(); + onChangeEndSpy.mockClear(); }); it('sets input props', () => { @@ -168,16 +162,60 @@ describe('ColorWheel', () => { it('respects step', () => { let defaultColor = parseColor('hsl(0, 100%, 50%)'); - let {getByRole} = render(); + let {getByRole} = render(); let slider = getByRole('slider'); act(() => {slider.focus();}); fireEvent.keyDown(slider, {key: 'Right'}); expect(onChangeSpy).toHaveBeenCalledTimes(1); expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 45).toString('hsla')); + expect(onChangeEndSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 45).toString('hsla')); fireEvent.keyDown(slider, {key: 'Left'}); expect(onChangeSpy).toHaveBeenCalledTimes(2); expect(onChangeSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 0).toString('hsla')); + expect(onChangeEndSpy).toHaveBeenCalledTimes(2); + expect(onChangeEndSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 0).toString('hsla')); + }); + + it('respects page steps', () => { + let defaultColor = parseColor('hsl(0, 100%, 50%)'); + let {getByRole} = render(); + let slider = getByRole('slider'); + act(() => {slider.focus();}); + + fireEvent.keyDown(slider, {key: 'PageUp'}); + fireEvent.keyUp(slider, {key: 'PageUp'}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 6).toString('hsla')); + expect(onChangeEndSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 6).toString('hsla')); + fireEvent.keyDown(slider, {key: 'PageDown'}); + fireEvent.keyUp(slider, {key: 'PageDown'}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 0).toString('hsla')); + expect(onChangeEndSpy).toHaveBeenCalledTimes(2); + expect(onChangeEndSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 0).toString('hsla')); + }); + + it('respects page steps from shift arrow', () => { + let defaultColor = parseColor('hsl(0, 100%, 50%)'); + let {getByRole} = render(); + let slider = getByRole('slider'); + act(() => {slider.focus();}); + + fireEvent.keyDown(slider, {key: 'Right', shiftKey: true}); + fireEvent.keyUp(slider, {key: 'Right', shiftKey: true}); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 6).toString('hsla')); + expect(onChangeEndSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy.mock.calls[0][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 6).toString('hsla')); + fireEvent.keyDown(slider, {key: 'Left', shiftKey: true}); + fireEvent.keyUp(slider, {key: 'Left', shiftKey: true}); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 0).toString('hsla')); + expect(onChangeEndSpy).toHaveBeenCalledTimes(2); + expect(onChangeEndSpy.mock.calls[1][0].toString('hsla')).toBe(defaultColor.withChannelValue('hue', 0).toString('hsla')); }); }); diff --git a/packages/@react-stately/color/src/useColorAreaState.ts b/packages/@react-stately/color/src/useColorAreaState.ts index 6c25fbeb29d..974da26db31 100644 --- a/packages/@react-stately/color/src/useColorAreaState.ts +++ b/packages/@react-stately/color/src/useColorAreaState.ts @@ -38,14 +38,14 @@ export interface ColorAreaState { getThumbPosition(): {x: number, y: number}, /** Increments the value of the horizontal axis channel by the channel step or page amount. */ - incrementX(isPageStep?: boolean): void, + incrementX(stepSize?: number): void, /** Decrements the value of the horizontal axis channel by the channel step or page amount. */ - decrementX(isPageStep?: boolean): void, + decrementX(stepSize?: number): void, /** Increments the value of the vertical axis channel by the channel step or page amount. */ - incrementY(isPageStep?: boolean): void, + incrementY(stepSize?: number): void, /** Decrements the value of the vertical axis channel by the channel step or page amount. */ - decrementY(isPageStep?: boolean): void, + decrementY(stepSize?: number): void, /** Whether the color area is currently being dragged. */ readonly isDragging: boolean, @@ -56,6 +56,8 @@ export interface ColorAreaState { channels: {xChannel: ColorChannel, yChannel: ColorChannel, zChannel: ColorChannel}, xChannelStep: number, yChannelStep: number, + xChannelPageStep: number, + yChannelPageStep: number, /** Returns the color that should be displayed in the color area thumb instead of `value`. */ getDisplayColor(): Color @@ -135,11 +137,15 @@ export function useColorAreaState(props: ColorAreaProps): ColorAreaState { valueRef.current = color.withChannelValue(channels.yChannel, v); setColor(valueRef.current); }; + let xChannelPageStep = Math.max(color.getChannelRange(channels.xChannel).pageSize, xChannelStep); + let yChannelPageStep = Math.max(color.getChannelRange(channels.yChannel).pageSize, yChannelStep); return { channels, xChannelStep, yChannelStep, + xChannelPageStep, + yChannelPageStep, value: color, setValue(value) { let c = normalizeColor(value); @@ -177,24 +183,20 @@ export function useColorAreaState(props: ColorAreaProps): ColorAreaState { let y = 1 - (yValue - minValueY) / (maxValueY - minValueY); return {x, y}; }, - incrementX(isPageStep) { + incrementX(stepSize) { let range = color.getChannelRange(channels.xChannel); - let stepSize = isPageStep ? Math.max(range.pageSize, xChannelStep) : xChannelStep; setXValue(snapValueToStep(xValue + stepSize, range.minValue, range.maxValue, stepSize)); }, - incrementY(isPageStep) { + incrementY(stepSize) { let range = color.getChannelRange(channels.yChannel); - let stepSize = isPageStep ? Math.max(range.pageSize, yChannelStep) : yChannelStep; setYValue(snapValueToStep(yValue + stepSize, range.minValue, range.maxValue, stepSize)); }, - decrementX(isPageStep) { + decrementX(stepSize) { let range = color.getChannelRange(channels.xChannel); - let stepSize = isPageStep ? Math.max(range.pageSize, xChannelStep) : xChannelStep; setXValue(snapValueToStep(xValue - stepSize, range.minValue, range.maxValue, stepSize)); }, - decrementY(isPageStep) { + decrementY(stepSize) { let range = color.getChannelRange(channels.yChannel); - let stepSize = isPageStep ? Math.max(range.pageSize, yChannelStep) : yChannelStep; setYValue(snapValueToStep(yValue - stepSize, range.minValue, range.maxValue, stepSize)); }, setDragging(isDragging) { diff --git a/packages/@react-stately/color/src/useColorWheelState.ts b/packages/@react-stately/color/src/useColorWheelState.ts index e1d10539a1c..59ba51a5c7d 100644 --- a/packages/@react-stately/color/src/useColorWheelState.ts +++ b/packages/@react-stately/color/src/useColorWheelState.ts @@ -11,7 +11,7 @@ */ import {Color, ColorWheelProps} from '@react-types/color'; -import {parseColor} from './Color'; +import {normalizeColor, parseColor} from './Color'; import {useControlledState} from '@react-stately/utils'; import {useRef, useState} from 'react'; @@ -32,24 +32,18 @@ export interface ColorWheelState { getThumbPosition(radius: number): {x: number, y: number}, /** Increments the hue by the given amount (defaults to 1). */ - increment(minStepSize?: number): void, + increment(stepSize?: number): void, /** Decrements the hue by the given amount (defaults to 1). */ - decrement(minStepSize?: number): void, + decrement(stepSize?: number): void, /** Whether the color wheel is currently being dragged. */ readonly isDragging: boolean, /** Sets whether the color wheel is being dragged. */ setDragging(value: boolean): void, /** Returns the color that should be displayed in the color wheel instead of `value`. */ - getDisplayColor(): Color -} - -function normalizeColor(v: string | Color) { - if (typeof v === 'string') { - return parseColor(v); - } else { - return v; - } + getDisplayColor(): Color, + step: number, + pageStep: number } const DEFAULT_COLOR = parseColor('hsl(0, 100%, 50%)'); @@ -91,6 +85,7 @@ function cartesianToAngle(x: number, y: number, radius: number): number { let deg = radToDeg(Math.atan2(y / radius, x / radius)); return (deg + 360) % 360; } +const PAGE_MIN_STEP_SIZE = 6; /** * Provides state management for a color wheel component. @@ -121,8 +116,11 @@ export function useColorWheelState(props: ColorWheelProps): ColorWheelState { } } + let pageStep = PAGE_MIN_STEP_SIZE; return { value, + step, + pageStep, setValue(v) { let color = normalizeColor(v); valueRef.current = color; @@ -136,16 +134,16 @@ export function useColorWheelState(props: ColorWheelProps): ColorWheelState { getThumbPosition(radius) { return angleToCartesian(value.getChannelValue('hue'), radius); }, - increment(minStepSize: number = 0) { - let newValue = hue + Math.max(minStepSize, step); + increment(stepSize) { + let newValue = hue + Math.max(stepSize, step); if (newValue > 360) { // Make sure you can always get back to 0. newValue = 0; } setHue(newValue); }, - decrement(minStepSize: number = 0) { - let s = Math.max(minStepSize, step); + decrement(stepSize) { + let s = Math.max(stepSize, step); if (hue === 0) { // We can't just subtract step because this might be the case: // |(previous step) - 0| < step size diff --git a/packages/@react-types/shared/src/events.d.ts b/packages/@react-types/shared/src/events.d.ts index af92120dc29..d5015d28942 100644 --- a/packages/@react-types/shared/src/events.d.ts +++ b/packages/@react-types/shared/src/events.d.ts @@ -110,7 +110,15 @@ export interface FocusableProps extends FocusEvents, KeyboardEvents { interface BaseMoveEvent { /** The pointer type that triggered the move event. */ - pointerType: PointerType + pointerType: PointerType, + /** Whether the shift keyboard modifier was held during the move event. */ + shiftKey: boolean, + /** Whether the ctrl keyboard modifier was held during the move event. */ + ctrlKey: boolean, + /** Whether the meta keyboard modifier was held during the move event. */ + metaKey: boolean, + /** Whether the alt keyboard modifier was held during the move event. */ + altKey: boolean } export interface MoveStartEvent extends BaseMoveEvent { @@ -125,6 +133,7 @@ export interface MoveMoveEvent extends BaseMoveEvent { deltaX: number, /** The amount moved in the Y direction since the last event. */ deltaY: number + } export interface MoveEndEvent extends BaseMoveEvent { From 1e58328d240c1b663abe3919e847e9302ac6d33d Mon Sep 17 00:00:00 2001 From: Michael Jordan Date: Thu, 11 Nov 2021 13:31:10 -0500 Subject: [PATCH 16/23] ColorArea: remove aria-roledescription on iOS/Android (#2547) * ColorArea: remove aria-roledescription on iOS/Android To better reflect behavior of slider inputs with mobile screen readers on iOS and Android, each input should be labeled by its individual channel name and we should remove aria-roledescription so the input is identified simply as a slider control. * ColorArea: change aria-valuetext for iOS/Android On iOS/Android, the aria-valuetext for each slider within the ColorArea control should just be the value for that channel, and need not include the value for the second channel. The value for all three channels is included as the title attribute. * ColorArea: include channel name in aria-valuetext for iOS/Android --- packages/@react-aria/color/src/useColorArea.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/@react-aria/color/src/useColorArea.ts b/packages/@react-aria/color/src/useColorArea.ts index 9dcb821ec21..46933fececb 100644 --- a/packages/@react-aria/color/src/useColorArea.ts +++ b/packages/@react-aria/color/src/useColorArea.ts @@ -12,7 +12,7 @@ import {AriaColorAreaProps} from '@react-types/color'; import {ColorAreaState} from '@react-stately/color'; -import {focusWithoutScrolling, mergeProps, useGlobalListeners, useLabels} from '@react-aria/utils'; +import {focusWithoutScrolling, isAndroid, isIOS, mergeProps, useGlobalListeners, useLabels} from '@react-aria/utils'; // @ts-ignore import intlMessages from '../intl/*.json'; import {MessageDictionary} from '@internationalized/message'; @@ -272,15 +272,16 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i }) }, keyboardProps, movePropsThumb); + let isMobile = isIOS() || isAndroid(); let xInputLabellingProps = useLabels({ ...props, - 'aria-label': `${state.value.getChannelName(xChannel, locale)} / ${state.value.getChannelName(yChannel, locale)}` + 'aria-label': isMobile ? state.value.getChannelName(xChannel, locale) : `${state.value.getChannelName(xChannel, locale)} / ${state.value.getChannelName(yChannel, locale)}` }); let yInputLabellingProps = useLabels({ ...props, - 'aria-label': `${state.value.getChannelName(xChannel, locale)} / ${state.value.getChannelName(yChannel, locale)}` + 'aria-label': isMobile ? state.value.getChannelName(yChannel, locale) : `${state.value.getChannelName(xChannel, locale)} / ${state.value.getChannelName(yChannel, locale)}` }); let colorAriaLabellingProps = useLabels(props); @@ -291,7 +292,7 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i `${state.value.getChannelName('blue', locale)}: ${state.value.formatChannelValue('blue', locale)}` ].join(', '); - let ariaRoleDescription = messages.getStringForLocale('twoDimensionalSlider', locale); + let ariaRoleDescription = isMobile ? null : messages.getStringForLocale('twoDimensionalSlider', locale); let {visuallyHiddenProps} = useVisuallyHidden({style: { opacity: '0.0001', @@ -318,10 +319,10 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i max: state.value.getChannelRange(xChannel).maxValue, step: xChannelStep, 'aria-roledescription': ariaRoleDescription, - 'aria-valuetext': [ + 'aria-valuetext': (isMobile ? `${state.value.getChannelName(xChannel, locale)}: ${state.value.formatChannelValue(xChannel, locale)}` : [ `${state.value.getChannelName(xChannel, locale)}: ${state.value.formatChannelValue(xChannel, locale)}`, `${state.value.getChannelName(yChannel, locale)}: ${state.value.formatChannelValue(yChannel, locale)}` - ].join(', '), + ].join(', ')), title: getValueTitle(), disabled: isDisabled, value: state.value.getChannelValue(xChannel), @@ -338,10 +339,10 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i max: state.value.getChannelRange(yChannel).maxValue, step: yChannelStep, 'aria-roledescription': ariaRoleDescription, - 'aria-valuetext': [ + 'aria-valuetext': (isMobile ? `${state.value.getChannelName(yChannel, locale)}: ${state.value.formatChannelValue(yChannel, locale)}` : [ `${state.value.getChannelName(yChannel, locale)}: ${state.value.formatChannelValue(yChannel, locale)}`, `${state.value.getChannelName(xChannel, locale)}: ${state.value.formatChannelValue(xChannel, locale)}` - ].join(', '), + ].join(', ')), 'aria-orientation': 'vertical', title: getValueTitle(), disabled: isDisabled, From cfa9a100953b3ccadd16823c67c208b6baf77ac5 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Thu, 11 Nov 2021 11:12:15 -0800 Subject: [PATCH 17/23] Add formatting for color name and value based on locale --- packages/@react-aria/color/intl/en-US.json | 5 +++-- packages/@react-aria/color/src/useColorArea.ts | 15 ++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/@react-aria/color/intl/en-US.json b/packages/@react-aria/color/intl/en-US.json index ba3b22fee50..48d64262238 100644 --- a/packages/@react-aria/color/intl/en-US.json +++ b/packages/@react-aria/color/intl/en-US.json @@ -1,3 +1,4 @@ { - "twoDimensionalSlider": "2D slider" -} \ No newline at end of file + "twoDimensionalSlider": "2D slider", + "colorNameAndValue": "{name}: {value}" +} diff --git a/packages/@react-aria/color/src/useColorArea.ts b/packages/@react-aria/color/src/useColorArea.ts index 46933fececb..83bb684ddcb 100644 --- a/packages/@react-aria/color/src/useColorArea.ts +++ b/packages/@react-aria/color/src/useColorArea.ts @@ -44,6 +44,7 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i let { isDisabled } = props; + let formatMessage = useMessageFormatter(intlMessages); let {addGlobalListener, removeGlobalListener} = useGlobalListeners(); @@ -292,7 +293,7 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i `${state.value.getChannelName('blue', locale)}: ${state.value.formatChannelValue('blue', locale)}` ].join(', '); - let ariaRoleDescription = isMobile ? null : messages.getStringForLocale('twoDimensionalSlider', locale); + let ariaRoleDescription = isMobile ? null : formatMessage('twoDimensionalSlider'); let {visuallyHiddenProps} = useVisuallyHidden({style: { opacity: '0.0001', @@ -319,9 +320,9 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i max: state.value.getChannelRange(xChannel).maxValue, step: xChannelStep, 'aria-roledescription': ariaRoleDescription, - 'aria-valuetext': (isMobile ? `${state.value.getChannelName(xChannel, locale)}: ${state.value.formatChannelValue(xChannel, locale)}` : [ - `${state.value.getChannelName(xChannel, locale)}: ${state.value.formatChannelValue(xChannel, locale)}`, - `${state.value.getChannelName(yChannel, locale)}: ${state.value.formatChannelValue(yChannel, locale)}` + 'aria-valuetext': (isMobile ? formatMessage('colorNameAndValue', {name: state.value.getChannelName(xChannel, locale), value: state.value.formatChannelValue(xChannel, locale)}) : [ + formatMessage('colorNameAndValue', {name: state.value.getChannelName(xChannel, locale), value: state.value.formatChannelValue(xChannel, locale)}), + formatMessage('colorNameAndValue', {name: state.value.getChannelName(yChannel, locale), value: state.value.formatChannelValue(yChannel, locale)}) ].join(', ')), title: getValueTitle(), disabled: isDisabled, @@ -339,9 +340,9 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i max: state.value.getChannelRange(yChannel).maxValue, step: yChannelStep, 'aria-roledescription': ariaRoleDescription, - 'aria-valuetext': (isMobile ? `${state.value.getChannelName(yChannel, locale)}: ${state.value.formatChannelValue(yChannel, locale)}` : [ - `${state.value.getChannelName(yChannel, locale)}: ${state.value.formatChannelValue(yChannel, locale)}`, - `${state.value.getChannelName(xChannel, locale)}: ${state.value.formatChannelValue(xChannel, locale)}` + 'aria-valuetext': (isMobile ? formatMessage('colorNameAndValue', {name: state.value.getChannelName(yChannel, locale), value: state.value.formatChannelValue(yChannel, locale)}) : [ + formatMessage('colorNameAndValue', {name: state.value.getChannelName(yChannel, locale), value: state.value.formatChannelValue(yChannel, locale)}), + formatMessage('colorNameAndValue', {name: state.value.getChannelName(xChannel, locale), value: state.value.formatChannelValue(xChannel, locale)}) ].join(', ')), 'aria-orientation': 'vertical', title: getValueTitle(), From 2b81e33fe6b6667658b9253e4417100580797a07 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Thu, 11 Nov 2021 13:20:03 -0800 Subject: [PATCH 18/23] allow for differences in formatting strings --- packages/@react-aria/color/intl/en-US.json | 3 ++- packages/@react-aria/color/src/useColorArea.ts | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/@react-aria/color/intl/en-US.json b/packages/@react-aria/color/intl/en-US.json index 48d64262238..c3d72686b82 100644 --- a/packages/@react-aria/color/intl/en-US.json +++ b/packages/@react-aria/color/intl/en-US.json @@ -1,4 +1,5 @@ { "twoDimensionalSlider": "2D slider", - "colorNameAndValue": "{name}: {value}" + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/src/useColorArea.ts b/packages/@react-aria/color/src/useColorArea.ts index 83bb684ddcb..7dbe1702395 100644 --- a/packages/@react-aria/color/src/useColorArea.ts +++ b/packages/@react-aria/color/src/useColorArea.ts @@ -277,20 +277,20 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i let xInputLabellingProps = useLabels({ ...props, - 'aria-label': isMobile ? state.value.getChannelName(xChannel, locale) : `${state.value.getChannelName(xChannel, locale)} / ${state.value.getChannelName(yChannel, locale)}` + 'aria-label': isMobile ? state.value.getChannelName(xChannel, locale) : formatMessage('x/y', {x: state.value.getChannelName(xChannel, locale), y: state.value.getChannelName(yChannel, locale)}) }); let yInputLabellingProps = useLabels({ ...props, - 'aria-label': isMobile ? state.value.getChannelName(yChannel, locale) : `${state.value.getChannelName(xChannel, locale)} / ${state.value.getChannelName(yChannel, locale)}` + 'aria-label': isMobile ? state.value.getChannelName(yChannel, locale) : formatMessage('x/y', {x: state.value.getChannelName(xChannel, locale), y: state.value.getChannelName(yChannel, locale)}) }); let colorAriaLabellingProps = useLabels(props); let getValueTitle = () => [ - `${state.value.getChannelName('red', locale)}: ${state.value.formatChannelValue('red', locale)}`, - `${state.value.getChannelName('green', locale)}: ${state.value.formatChannelValue('green', locale)}`, - `${state.value.getChannelName('blue', locale)}: ${state.value.formatChannelValue('blue', locale)}` + formatMessage('colorNameAndValue', {name: state.value.getChannelName('red', locale), value: state.value.formatChannelValue('red', locale)}), + formatMessage('colorNameAndValue', {name: state.value.getChannelName('green', locale), value: state.value.formatChannelValue('green', locale)}), + formatMessage('colorNameAndValue', {name: state.value.getChannelName('blue', locale), value: state.value.formatChannelValue('blue', locale)}) ].join(', '); let ariaRoleDescription = isMobile ? null : formatMessage('twoDimensionalSlider'); From a05ff19e668479dac6dda0394a5efbbc9d43ed2b Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Thu, 11 Nov 2021 13:22:21 -0800 Subject: [PATCH 19/23] fix lint --- packages/@react-aria/color/src/useColorArea.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/@react-aria/color/src/useColorArea.ts b/packages/@react-aria/color/src/useColorArea.ts index 7dbe1702395..2dbd2e36b23 100644 --- a/packages/@react-aria/color/src/useColorArea.ts +++ b/packages/@react-aria/color/src/useColorArea.ts @@ -15,14 +15,11 @@ import {ColorAreaState} from '@react-stately/color'; import {focusWithoutScrolling, isAndroid, isIOS, mergeProps, useGlobalListeners, useLabels} from '@react-aria/utils'; // @ts-ignore import intlMessages from '../intl/*.json'; -import {MessageDictionary} from '@internationalized/message'; import React, {ChangeEvent, HTMLAttributes, InputHTMLAttributes, RefObject, useCallback, useRef} from 'react'; import {useKeyboard, useMove} from '@react-aria/interactions'; -import {useLocale} from '@react-aria/i18n'; +import {useLocale, useMessageFormatter} from '@react-aria/i18n'; import {useVisuallyHidden} from '@react-aria/visually-hidden'; -const messages = new MessageDictionary(intlMessages); - interface ColorAreaAria { /** Props for the color area container element. */ colorAreaProps: HTMLAttributes, From affe41b691dc51841fe4a4415e934ce95576d383 Mon Sep 17 00:00:00 2001 From: Michael Jordan Date: Fri, 4 Feb 2022 17:20:03 -0500 Subject: [PATCH 20/23] @trivial remove extra whitespace/redundant afterEach --- packages/@react-aria/color/test/useColorWheel.test.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/@react-aria/color/test/useColorWheel.test.tsx b/packages/@react-aria/color/test/useColorWheel.test.tsx index 02f6765f1cd..86f4897cfdc 100644 --- a/packages/@react-aria/color/test/useColorWheel.test.tsx +++ b/packages/@react-aria/color/test/useColorWheel.test.tsx @@ -52,7 +52,7 @@ function ColorWheel(props: ColorWheelProps) { describe('useColorWheel', () => { let onChangeSpy = jest.fn(); - beforeAll(() => { + beforeAll(() => { // @ts-ignore jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => cb()); // @ts-ignore @@ -65,13 +65,6 @@ describe('useColorWheel', () => { jest.runAllTimers(); }); - afterEach(() => { - onChangeSpy.mockClear(); - // for restoreTextSelection - jest.runAllTimers(); - onChangeSpy.mockClear(); - }); - it('sets input props', () => { let {getByRole} = render(); let slider = getByRole('slider'); From ee10c743901e205ac80d1e4f5c961bc9b1986f66 Mon Sep 17 00:00:00 2001 From: Michael Jordan Date: Fri, 4 Feb 2022 18:05:05 -0500 Subject: [PATCH 21/23] fix: tests --- packages/@react-spectrum/color/test/ColorArea.test.tsx | 1 - packages/@react-spectrum/color/test/ColorWheel.test.tsx | 7 ++----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/@react-spectrum/color/test/ColorArea.test.tsx b/packages/@react-spectrum/color/test/ColorArea.test.tsx index ada0088e9ba..c36d854bbf3 100644 --- a/packages/@react-spectrum/color/test/ColorArea.test.tsx +++ b/packages/@react-spectrum/color/test/ColorArea.test.tsx @@ -41,7 +41,6 @@ describe('ColorArea', () => { }); afterAll(() => { // @ts-ignore - window.HTMLElement.prototype.offsetWidth.mockReset(); jest.useRealTimers(); }); diff --git a/packages/@react-spectrum/color/test/ColorWheel.test.tsx b/packages/@react-spectrum/color/test/ColorWheel.test.tsx index 354026f75c0..abf317f6766 100644 --- a/packages/@react-spectrum/color/test/ColorWheel.test.tsx +++ b/packages/@react-spectrum/color/test/ColorWheel.test.tsx @@ -34,11 +34,6 @@ describe('ColorWheel', () => { let onChangeSpy = jest.fn(); let onChangeEndSpy = jest.fn(); - afterEach(() => { - onChangeSpy.mockClear(); - onChangeEndSpy.mockClear(); - }); - beforeAll(() => { jest.spyOn(window.HTMLElement.prototype, 'offsetWidth', 'get').mockImplementation(() => SIZE); // @ts-ignore @@ -50,6 +45,8 @@ describe('ColorWheel', () => { afterEach(() => { // for restoreTextSelection jest.runAllTimers(); + onChangeSpy.mockClear(); + onChangeEndSpy.mockClear(); }); it('sets input props', () => { From a911911f4e80fd415d9a4570c83a28446c9fe01d Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 16 Feb 2022 18:01:41 -0800 Subject: [PATCH 22/23] fix remaining issues before merges on main make it in --- packages/@react-aria/color/package.json | 4 ++-- .../@react-spectrum/searchwithin/test/SearchWithin.test.js | 3 +-- packages/@react-spectrum/sidenav/package.json | 2 +- packages/@react-stately/color/package.json | 2 +- packages/@react-types/sidenav/package.json | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/@react-aria/color/package.json b/packages/@react-aria/color/package.json index 579bab9c205..69d4110ef3e 100644 --- a/packages/@react-aria/color/package.json +++ b/packages/@react-aria/color/package.json @@ -26,8 +26,8 @@ "@react-aria/textfield": "^3.5.0", "@react-aria/utils": "^3.11.0", "@react-aria/visually-hidden": "^3.2.3", - "@react-stately/color": "3.0.0-beta.5", - "@react-types/color": "3.0.0-beta.3", + "@react-stately/color": "3.0.0-beta.7", + "@react-types/color": "3.0.0-beta.5", "@react-types/shared": "^3.10.1", "@react-types/slider": "^3.0.2" }, diff --git a/packages/@react-spectrum/searchwithin/test/SearchWithin.test.js b/packages/@react-spectrum/searchwithin/test/SearchWithin.test.js index 4eeb7943b41..a00c629d049 100644 --- a/packages/@react-spectrum/searchwithin/test/SearchWithin.test.js +++ b/packages/@react-spectrum/searchwithin/test/SearchWithin.test.js @@ -212,7 +212,7 @@ describe('SearchWithin labeling', function () { }); it('aria-labelledby = {id}', function () { - let {getByRole, debug, getByText} = render( + let {getByRole, getByText} = render( ); - debug(); let group = getByRole('group'); let searchfield = getByRole('searchbox'); diff --git a/packages/@react-spectrum/sidenav/package.json b/packages/@react-spectrum/sidenav/package.json index f71b570fdd9..4d2b42931fb 100644 --- a/packages/@react-spectrum/sidenav/package.json +++ b/packages/@react-spectrum/sidenav/package.json @@ -45,7 +45,7 @@ "@react-stately/collections": "^3.1.0", "@react-stately/layout": "^3.2.1", "@react-stately/tree": "^3.1.0", - "@react-stately/virtualizer": "3.1.7-alpha.0", + "@react-stately/virtualizer": "^3.1.7", "@react-types/shared": "^3.1.0", "@react-types/sidenav": "3.0.0-alpha.1" }, diff --git a/packages/@react-stately/color/package.json b/packages/@react-stately/color/package.json index 61c9b190b85..c81f120784f 100644 --- a/packages/@react-stately/color/package.json +++ b/packages/@react-stately/color/package.json @@ -23,7 +23,7 @@ "@react-aria/utils": "^3.9.0", "@react-stately/slider": "^3.0.3", "@react-stately/utils": "^3.3.0", - "@react-types/color": "3.0.0-beta.3", + "@react-types/color": "3.0.0-beta.5", "@react-types/numberfield": "^3.1.0", "@react-types/shared": "^3.9.0" }, diff --git a/packages/@react-types/sidenav/package.json b/packages/@react-types/sidenav/package.json index 77ff06308be..d15a10a7bd1 100644 --- a/packages/@react-types/sidenav/package.json +++ b/packages/@react-types/sidenav/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@react-types/shared": "^3.1.0", - "@react-stately/virtualizer": "3.1.7-alpha.0" + "@react-stately/virtualizer": "^3.1.7" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1" From 57dc904e8eee4be74d8659b81f39f58bb34db331 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Thu, 17 Feb 2022 15:16:27 -0800 Subject: [PATCH 23/23] incorporate michaels a11y and locale fixes --- packages/@react-aria/color/intl/ar-AE.json | 4 +- packages/@react-aria/color/intl/bg-BG.json | 4 +- packages/@react-aria/color/intl/cs-CZ.json | 4 +- packages/@react-aria/color/intl/da-DK.json | 4 +- packages/@react-aria/color/intl/de-DE.json | 4 +- packages/@react-aria/color/intl/el-GR.json | 4 +- packages/@react-aria/color/intl/es-ES.json | 4 +- packages/@react-aria/color/intl/et-EE.json | 4 +- packages/@react-aria/color/intl/fi-FI.json | 4 +- packages/@react-aria/color/intl/fr-FR.json | 4 +- packages/@react-aria/color/intl/he-IL.json | 4 +- packages/@react-aria/color/intl/hr-HR.json | 4 +- packages/@react-aria/color/intl/hu-HU.json | 4 +- packages/@react-aria/color/intl/it-IT.json | 4 +- packages/@react-aria/color/intl/ja-JP.json | 4 +- packages/@react-aria/color/intl/ko-KR.json | 4 +- packages/@react-aria/color/intl/lt-LT.json | 4 +- packages/@react-aria/color/intl/lv-LV.json | 4 +- packages/@react-aria/color/intl/nb-NO.json | 4 +- packages/@react-aria/color/intl/nl-NL.json | 4 +- packages/@react-aria/color/intl/pl-PL.json | 4 +- packages/@react-aria/color/intl/pt-BR.json | 4 +- packages/@react-aria/color/intl/pt-PT.json | 4 +- packages/@react-aria/color/intl/ro-RO.json | 4 +- packages/@react-aria/color/intl/ru-RU.json | 4 +- packages/@react-aria/color/intl/sk-SK.json | 4 +- packages/@react-aria/color/intl/sl-SI.json | 4 +- packages/@react-aria/color/intl/sr-SP.json | 4 +- packages/@react-aria/color/intl/sv-SE.json | 4 +- packages/@react-aria/color/intl/tr-TR.json | 4 +- packages/@react-aria/color/intl/uk-UA.json | 4 +- packages/@react-aria/color/intl/zh-CN.json | 4 +- packages/@react-aria/color/intl/zh-TW.json | 4 +- .../@react-aria/color/src/useColorArea.ts | 63 +++++++++++++------ .../@react-spectrum/color/src/ColorArea.tsx | 2 +- .../color/stories/ColorArea.stories.tsx | 14 +++-- .../color/test/ColorArea.test.tsx | 45 +++++++++++-- .../color/src/useColorAreaState.ts | 34 +++++----- 38 files changed, 214 insertions(+), 76 deletions(-) diff --git a/packages/@react-aria/color/intl/ar-AE.json b/packages/@react-aria/color/intl/ar-AE.json index 2c4a69d4c60..e3adbfc6707 100644 --- a/packages/@react-aria/color/intl/ar-AE.json +++ b/packages/@react-aria/color/intl/ar-AE.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "شريط تمرير ثنائي الأبعاد" + "twoDimensionalSlider": "شريط تمرير ثنائي الأبعاد", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/bg-BG.json b/packages/@react-aria/color/intl/bg-BG.json index 124b7776de8..8299f4af79a 100644 --- a/packages/@react-aria/color/intl/bg-BG.json +++ b/packages/@react-aria/color/intl/bg-BG.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "2D плъзгач" + "twoDimensionalSlider": "2D плъзгач", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/cs-CZ.json b/packages/@react-aria/color/intl/cs-CZ.json index 15dfd16c849..d676773682e 100644 --- a/packages/@react-aria/color/intl/cs-CZ.json +++ b/packages/@react-aria/color/intl/cs-CZ.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "2D posuvník" + "twoDimensionalSlider": "2D posuvník", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/da-DK.json b/packages/@react-aria/color/intl/da-DK.json index c17a0058b2c..02bf3d40a29 100644 --- a/packages/@react-aria/color/intl/da-DK.json +++ b/packages/@react-aria/color/intl/da-DK.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "2D-skyder" + "twoDimensionalSlider": "2D-skyder", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/de-DE.json b/packages/@react-aria/color/intl/de-DE.json index 89ef078dc20..0face0024ec 100644 --- a/packages/@react-aria/color/intl/de-DE.json +++ b/packages/@react-aria/color/intl/de-DE.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "2D-Schieberegler" + "twoDimensionalSlider": "2D-Schieberegler", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/el-GR.json b/packages/@react-aria/color/intl/el-GR.json index 85a99c744c3..754e53316a3 100644 --- a/packages/@react-aria/color/intl/el-GR.json +++ b/packages/@react-aria/color/intl/el-GR.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "Ρυθμιστικό 2D" + "twoDimensionalSlider": "Ρυθμιστικό 2D", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/es-ES.json b/packages/@react-aria/color/intl/es-ES.json index 11bc234cdd1..ee0bd0f7a58 100644 --- a/packages/@react-aria/color/intl/es-ES.json +++ b/packages/@react-aria/color/intl/es-ES.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "Control deslizante en 2D" + "twoDimensionalSlider": "Control deslizante en 2D", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/et-EE.json b/packages/@react-aria/color/intl/et-EE.json index 8cb808cd272..2859d654c80 100644 --- a/packages/@react-aria/color/intl/et-EE.json +++ b/packages/@react-aria/color/intl/et-EE.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "2D-liugur" + "twoDimensionalSlider": "2D-liugur", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/fi-FI.json b/packages/@react-aria/color/intl/fi-FI.json index 11e27b2cc1c..ca3b11689ce 100644 --- a/packages/@react-aria/color/intl/fi-FI.json +++ b/packages/@react-aria/color/intl/fi-FI.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "2D-liukusäädin" + "twoDimensionalSlider": "2D-liukusäädin", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/fr-FR.json b/packages/@react-aria/color/intl/fr-FR.json index 7fce4f30892..29fc2c866e1 100644 --- a/packages/@react-aria/color/intl/fr-FR.json +++ b/packages/@react-aria/color/intl/fr-FR.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "Curseur 2D" + "twoDimensionalSlider": "Curseur 2D", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/he-IL.json b/packages/@react-aria/color/intl/he-IL.json index 51d213388c7..d3affd743a4 100644 --- a/packages/@react-aria/color/intl/he-IL.json +++ b/packages/@react-aria/color/intl/he-IL.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "מחוון דו-ממדי" + "twoDimensionalSlider": "מחוון דו-ממדי", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/hr-HR.json b/packages/@react-aria/color/intl/hr-HR.json index 690c8b20cda..1eed1913084 100644 --- a/packages/@react-aria/color/intl/hr-HR.json +++ b/packages/@react-aria/color/intl/hr-HR.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "2D kliznik" + "twoDimensionalSlider": "2D kliznik", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/hu-HU.json b/packages/@react-aria/color/intl/hu-HU.json index 9d1747751f1..efed81ba033 100644 --- a/packages/@react-aria/color/intl/hu-HU.json +++ b/packages/@react-aria/color/intl/hu-HU.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "2D csúszka" + "twoDimensionalSlider": "2D csúszka", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/it-IT.json b/packages/@react-aria/color/intl/it-IT.json index d6abe384322..561daf2fb94 100644 --- a/packages/@react-aria/color/intl/it-IT.json +++ b/packages/@react-aria/color/intl/it-IT.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "Dispositivo di scorrimento 2D" + "twoDimensionalSlider": "Dispositivo di scorrimento 2D", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/ja-JP.json b/packages/@react-aria/color/intl/ja-JP.json index e5f4bfc0c47..76f16b8bccb 100644 --- a/packages/@react-aria/color/intl/ja-JP.json +++ b/packages/@react-aria/color/intl/ja-JP.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "2D スライダー" + "twoDimensionalSlider": "2D スライダー", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/ko-KR.json b/packages/@react-aria/color/intl/ko-KR.json index 775390ef610..58e94a866c3 100644 --- a/packages/@react-aria/color/intl/ko-KR.json +++ b/packages/@react-aria/color/intl/ko-KR.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "2D 슬라이더" + "twoDimensionalSlider": "2D 슬라이더", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/lt-LT.json b/packages/@react-aria/color/intl/lt-LT.json index 0cc8009a9d5..d13d94a0663 100644 --- a/packages/@react-aria/color/intl/lt-LT.json +++ b/packages/@react-aria/color/intl/lt-LT.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "2D slankiklis" + "twoDimensionalSlider": "2D slankiklis", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/lv-LV.json b/packages/@react-aria/color/intl/lv-LV.json index 981f5c8cfe6..402af4930f8 100644 --- a/packages/@react-aria/color/intl/lv-LV.json +++ b/packages/@react-aria/color/intl/lv-LV.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "Plaknes slīdnis" + "twoDimensionalSlider": "Plaknes slīdnis", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/nb-NO.json b/packages/@react-aria/color/intl/nb-NO.json index 88624e00e74..b3a77570622 100644 --- a/packages/@react-aria/color/intl/nb-NO.json +++ b/packages/@react-aria/color/intl/nb-NO.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "2D-glidebryter" + "twoDimensionalSlider": "2D-glidebryter", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/nl-NL.json b/packages/@react-aria/color/intl/nl-NL.json index 3c553ec0684..e2baafafd32 100644 --- a/packages/@react-aria/color/intl/nl-NL.json +++ b/packages/@react-aria/color/intl/nl-NL.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "2D-schuifregelaar" + "twoDimensionalSlider": "2D-schuifregelaar", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/pl-PL.json b/packages/@react-aria/color/intl/pl-PL.json index 17561805b95..4053c546cf3 100644 --- a/packages/@react-aria/color/intl/pl-PL.json +++ b/packages/@react-aria/color/intl/pl-PL.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "Suwak 2D" + "twoDimensionalSlider": "Suwak 2D", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/pt-BR.json b/packages/@react-aria/color/intl/pt-BR.json index f3f99a977c5..550bbb505bc 100644 --- a/packages/@react-aria/color/intl/pt-BR.json +++ b/packages/@react-aria/color/intl/pt-BR.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "Controle deslizante 2D" + "twoDimensionalSlider": "Controle deslizante 2D", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/pt-PT.json b/packages/@react-aria/color/intl/pt-PT.json index d4158c3fe81..33d9da707d2 100644 --- a/packages/@react-aria/color/intl/pt-PT.json +++ b/packages/@react-aria/color/intl/pt-PT.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "Controlo de deslize 2D" + "twoDimensionalSlider": "Controlo de deslize 2D", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/ro-RO.json b/packages/@react-aria/color/intl/ro-RO.json index 7acde457ab1..90eba883c3c 100644 --- a/packages/@react-aria/color/intl/ro-RO.json +++ b/packages/@react-aria/color/intl/ro-RO.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "Cursor 2D" + "twoDimensionalSlider": "Cursor 2D", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/ru-RU.json b/packages/@react-aria/color/intl/ru-RU.json index 26007c38cc7..52bd24d769e 100644 --- a/packages/@react-aria/color/intl/ru-RU.json +++ b/packages/@react-aria/color/intl/ru-RU.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "Двумерный ползунок" + "twoDimensionalSlider": "Двумерный ползунок", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/sk-SK.json b/packages/@react-aria/color/intl/sk-SK.json index f36345a42f3..8aa9d695694 100644 --- a/packages/@react-aria/color/intl/sk-SK.json +++ b/packages/@react-aria/color/intl/sk-SK.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "2D jazdec" + "twoDimensionalSlider": "2D jazdec", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/sl-SI.json b/packages/@react-aria/color/intl/sl-SI.json index 9e524397af3..02845036219 100644 --- a/packages/@react-aria/color/intl/sl-SI.json +++ b/packages/@react-aria/color/intl/sl-SI.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "2D-drsnik" + "twoDimensionalSlider": "2D-drsnik", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/sr-SP.json b/packages/@react-aria/color/intl/sr-SP.json index cc2bd776eb7..a4b9bc4b6cb 100644 --- a/packages/@react-aria/color/intl/sr-SP.json +++ b/packages/@react-aria/color/intl/sr-SP.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "2D клизач" + "twoDimensionalSlider": "2D клизач", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/sv-SE.json b/packages/@react-aria/color/intl/sv-SE.json index cbc522867fb..b0732a46bcf 100644 --- a/packages/@react-aria/color/intl/sv-SE.json +++ b/packages/@react-aria/color/intl/sv-SE.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "2D-reglage" + "twoDimensionalSlider": "2D-reglage", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/tr-TR.json b/packages/@react-aria/color/intl/tr-TR.json index b6ecba53326..0a69975643a 100644 --- a/packages/@react-aria/color/intl/tr-TR.json +++ b/packages/@react-aria/color/intl/tr-TR.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "2B slayt gösterisi" + "twoDimensionalSlider": "2B slayt gösterisi", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/uk-UA.json b/packages/@react-aria/color/intl/uk-UA.json index c175dc51e4e..6bd3ac3b2bc 100644 --- a/packages/@react-aria/color/intl/uk-UA.json +++ b/packages/@react-aria/color/intl/uk-UA.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "Повзунок 2D" + "twoDimensionalSlider": "Повзунок 2D", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/zh-CN.json b/packages/@react-aria/color/intl/zh-CN.json index 579d2f6729f..171ba64dcb0 100644 --- a/packages/@react-aria/color/intl/zh-CN.json +++ b/packages/@react-aria/color/intl/zh-CN.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "2D 滑块" + "twoDimensionalSlider": "2D 滑块", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/intl/zh-TW.json b/packages/@react-aria/color/intl/zh-TW.json index d09b5f80c2e..d8c87cc8bd8 100644 --- a/packages/@react-aria/color/intl/zh-TW.json +++ b/packages/@react-aria/color/intl/zh-TW.json @@ -1,3 +1,5 @@ { - "twoDimensionalSlider": "2D 滑桿" + "twoDimensionalSlider": "2D 滑桿", + "colorNameAndValue": "{name}: {value}", + "x/y": "{x} / {y}" } diff --git a/packages/@react-aria/color/src/useColorArea.ts b/packages/@react-aria/color/src/useColorArea.ts index 2dbd2e36b23..db31620e441 100644 --- a/packages/@react-aria/color/src/useColorArea.ts +++ b/packages/@react-aria/color/src/useColorArea.ts @@ -106,26 +106,40 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i stateRef.current.setDragging(true); }, onMove({deltaX, deltaY, pointerType, shiftKey}) { + let { + incrementX, + decrementX, + incrementY, + decrementY, + xChannelPageStep, + xChannelStep, + yChannelPageStep, + yChannelStep, + getThumbPosition, + setColorFromPoint + } = stateRef.current; if (currentPosition.current == null) { - currentPosition.current = stateRef.current.getThumbPosition(); + currentPosition.current = getThumbPosition(); } let {width, height} = containerRef.current.getBoundingClientRect(); if (pointerType === 'keyboard') { - if (deltaX > 0) { - stateRef.current.incrementX(shiftKey ? stateRef.current.xChannelPageStep : stateRef.current.xChannelStep); - } else if (deltaX < 0) { - stateRef.current.decrementX(shiftKey ? stateRef.current.xChannelPageStep : stateRef.current.xChannelStep); + let deltaXValue = shiftKey && xChannelPageStep > xChannelStep ? xChannelPageStep : xChannelStep; + let deltaYValue = shiftKey && yChannelPageStep > yChannelStep ? yChannelPageStep : yChannelStep; + if ((deltaX > 0 && direction === 'ltr') || (deltaX < 0 && direction === 'rtl')) { + incrementX(deltaXValue); + } else if ((deltaX < 0 && direction === 'ltr') || (deltaX > 0 && direction === 'rtl')) { + decrementX(deltaXValue); } else if (deltaY > 0) { - stateRef.current.decrementY(shiftKey ? stateRef.current.yChannelPageStep : stateRef.current.yChannelStep); + decrementY(deltaYValue); } else if (deltaY < 0) { - stateRef.current.incrementY(shiftKey ? stateRef.current.yChannelPageStep : stateRef.current.yChannelStep); + incrementY(deltaYValue); } // set the focused input based on which axis has the greater delta focusedInputRef.current = (deltaX !== 0 || deltaY !== 0) && Math.abs(deltaY) > Math.abs(deltaX) ? inputYRef.current : inputXRef.current; } else { currentPosition.current.x += (direction === 'rtl' ? -1 : 1) * deltaX / width ; currentPosition.current.y += deltaY / height; - stateRef.current.setColorFromPoint(currentPosition.current.x, currentPosition.current.y); + setColorFromPoint(currentPosition.current.x, currentPosition.current.y); } }, onMoveEnd() { @@ -305,9 +319,12 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i ...colorAreaInteractions, role: 'group' }, - gradientProps: {}, + gradientProps: { + role: 'presentation' + }, thumbProps: { - ...thumbInteractions + ...thumbInteractions, + role: 'presentation' }, xInputProps: { ...xInputLabellingProps, @@ -317,10 +334,15 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i max: state.value.getChannelRange(xChannel).maxValue, step: xChannelStep, 'aria-roledescription': ariaRoleDescription, - 'aria-valuetext': (isMobile ? formatMessage('colorNameAndValue', {name: state.value.getChannelName(xChannel, locale), value: state.value.formatChannelValue(xChannel, locale)}) : [ - formatMessage('colorNameAndValue', {name: state.value.getChannelName(xChannel, locale), value: state.value.formatChannelValue(xChannel, locale)}), - formatMessage('colorNameAndValue', {name: state.value.getChannelName(yChannel, locale), value: state.value.formatChannelValue(yChannel, locale)}) - ].join(', ')), + 'aria-valuetext': ( + isMobile ? + formatMessage('colorNameAndValue', {name: state.value.getChannelName(xChannel, locale), value: state.value.formatChannelValue(xChannel, locale)}) + : + [ + formatMessage('colorNameAndValue', {name: state.value.getChannelName(xChannel, locale), value: state.value.formatChannelValue(xChannel, locale)}), + formatMessage('colorNameAndValue', {name: state.value.getChannelName(yChannel, locale), value: state.value.formatChannelValue(yChannel, locale)}) + ].join(', ') + ), title: getValueTitle(), disabled: isDisabled, value: state.value.getChannelValue(xChannel), @@ -337,10 +359,15 @@ export function useColorArea(props: AriaColorAreaProps, state: ColorAreaState, i max: state.value.getChannelRange(yChannel).maxValue, step: yChannelStep, 'aria-roledescription': ariaRoleDescription, - 'aria-valuetext': (isMobile ? formatMessage('colorNameAndValue', {name: state.value.getChannelName(yChannel, locale), value: state.value.formatChannelValue(yChannel, locale)}) : [ - formatMessage('colorNameAndValue', {name: state.value.getChannelName(yChannel, locale), value: state.value.formatChannelValue(yChannel, locale)}), - formatMessage('colorNameAndValue', {name: state.value.getChannelName(xChannel, locale), value: state.value.formatChannelValue(xChannel, locale)}) - ].join(', ')), + 'aria-valuetext': ( + isMobile ? + formatMessage('colorNameAndValue', {name: state.value.getChannelName(yChannel, locale), value: state.value.formatChannelValue(yChannel, locale)}) + : + [ + formatMessage('colorNameAndValue', {name: state.value.getChannelName(yChannel, locale), value: state.value.formatChannelValue(yChannel, locale)}), + formatMessage('colorNameAndValue', {name: state.value.getChannelName(xChannel, locale), value: state.value.formatChannelValue(xChannel, locale)}) + ].join(', ') + ), 'aria-orientation': 'vertical', title: getValueTitle(), disabled: isDisabled, diff --git a/packages/@react-spectrum/color/src/ColorArea.tsx b/packages/@react-spectrum/color/src/ColorArea.tsx index 2ecfd794774..b649967cfa8 100644 --- a/packages/@react-spectrum/color/src/ColorArea.tsx +++ b/packages/@react-spectrum/color/src/ColorArea.tsx @@ -79,7 +79,7 @@ function ColorArea(props: SpectrumColorAreaProps, ref: FocusableRef -
+
diff --git a/packages/@react-spectrum/color/stories/ColorArea.stories.tsx b/packages/@react-spectrum/color/stories/ColorArea.stories.tsx index 9582336e912..a1e70542bff 100644 --- a/packages/@react-spectrum/color/stories/ColorArea.stories.tsx +++ b/packages/@react-spectrum/color/stories/ColorArea.stories.tsx @@ -77,28 +77,32 @@ XBlueYGreen.args = {xChannel: 'blue', yChannel: 'green', onChange: action('onCha export let XGreenYBlue = Template.bind({}); XGreenYBlue.storyName = 'RGB xChannel="green", yChannel="blue"'; -XGreenYBlue.args = {xChannel: 'green', yChannel: 'blue', onChange: action('onChange')}; +XGreenYBlue.args = {...XBlueYGreen.args, xChannel: 'green', yChannel: 'blue'}; export let XBlueYRed = Template.bind({}); XBlueYRed.storyName = 'RGB xChannel="blue", yChannel="red"'; -XBlueYRed.args = {xChannel: 'blue', yChannel: 'red', onChange: action('onChange')}; +XBlueYRed.args = {...XBlueYGreen.args, xChannel: 'blue', yChannel: 'red'}; export let XRedYBlue = Template.bind({}); XRedYBlue.storyName = 'GB xChannel="red", yChannel="blue"'; -XRedYBlue.args = {xChannel: 'red', yChannel: 'blue', onChange: action('onChange')}; +XRedYBlue.args = {...XBlueYGreen.args, xChannel: 'red', yChannel: 'blue'}; export let XRedYGreen = Template.bind({}); XRedYGreen.storyName = 'RGB xChannel="red", yChannel="green"'; -XRedYGreen.args = {xChannel: 'red', yChannel: 'green', onChange: action('onChange')}; +XRedYGreen.args = {...XBlueYGreen.args, xChannel: 'red', yChannel: 'green'}; export let XGreenYRed = Template.bind({}); XGreenYRed.storyName = 'RGB xChannel="green", yChannel="red"'; -XGreenYRed.args = {xChannel: 'green', yChannel: 'red', onChange: action('onChange')}; +XGreenYRed.args = {...XBlueYGreen.args, xChannel: 'green', yChannel: 'red'}; export let XBlueYGreenStep16 = Template.bind({}); XBlueYGreenStep16.storyName = 'RGB xChannel="blue", yChannel="green", step="16"'; XBlueYGreenStep16.args = {...XBlueYGreen.args, xChannelStep: 16, yChannelStep: 16}; +export let XBlueYGreenPageStep32 = Template.bind({}); +XBlueYGreenPageStep32.storyName = 'RGB xChannel="blue", yChannel="green", pageStep="32"'; +XBlueYGreenPageStep32.args = {...XBlueYGreen.args, xChannelPageStep: 32, yChannelPageStep: 32}; + /* TODO: what does a disabled color area look like? */ export let XBlueYGreenisDisabled = Template.bind({}); XBlueYGreenisDisabled.storyName = 'RGB xChannel="blue", yChannel="green", isDisabled'; diff --git a/packages/@react-spectrum/color/test/ColorArea.test.tsx b/packages/@react-spectrum/color/test/ColorArea.test.tsx index c36d854bbf3..a9ef7a834b9 100644 --- a/packages/@react-spectrum/color/test/ColorArea.test.tsx +++ b/packages/@react-spectrum/color/test/ColorArea.test.tsx @@ -12,9 +12,11 @@ import {ColorArea} from '../'; import {XBlueYGreen as DefaultColorArea} from '../stories/ColorArea.stories'; +import {defaultTheme} from '@adobe/react-spectrum'; import {fireEvent, render} from '@testing-library/react'; import {installMouseEvent, installPointerEvent} from '@react-spectrum/test-utils'; import {parseColor} from '@react-stately/color'; +import {Provider} from '@react-spectrum/provider'; import React from 'react'; import userEvent from '@testing-library/user-event'; @@ -126,13 +128,13 @@ describe('ColorArea', () => { }; describe('keyboard events', () => { it.each` - Name | props | actions | result - ${'left/right'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'ArrowLeft'}), backward: (elem) => pressKey(elem, {key: 'ArrowRight'})}} | ${parseColor('#ff00fe')} - ${'up/down'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'ArrowUp'}), backward: (elem) => pressKey(elem, {key: 'ArrowDown'})}} | ${parseColor('#ff01ff')} + Name | props | actions | result + ${'left/right'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'ArrowLeft'}), backward: (elem) => pressKey(elem, {key: 'ArrowRight'})}} | ${parseColor('#ff00fe')} + ${'up/down'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'ArrowUp'}), backward: (elem) => pressKey(elem, {key: 'ArrowDown'})}} | ${parseColor('#ff01ff')} ${'shiftleft/shiftright'} | ${{defaultValue: parseColor('#f000f0')}} | ${{forward: (elem) => pressKey(elem, {key: 'ArrowLeft', shiftKey: true}), backward: (elem) => pressKey(elem, {key: 'ArrowRight', shiftKey: true})}} | ${parseColor('#f000e0')} ${'shiftup/shiftdown'} | ${{defaultValue: parseColor('#f000f0')}} | ${{forward: (elem) => pressKey(elem, {key: 'ArrowUp', shiftKey: true}), backward: (elem) => pressKey(elem, {key: 'ArrowDown', shiftKey: true})}} | ${parseColor('#f010f0')} - ${'pageup/pagedown'} | ${{defaultValue: parseColor('#f000f0')}} | ${{forward: (elem) => pressKey(elem, {key: 'PageUp'}), backward: (elem) => pressKey(elem, {key: 'PageDown'})}} | ${parseColor('#f010f0')} - ${'home/end'} | ${{defaultValue: parseColor('#f000f0')}} | ${{forward: (elem) => pressKey(elem, {key: 'Home'}), backward: (elem) => pressKey(elem, {key: 'End'})}} | ${parseColor('#f000e0')} + ${'pageup/pagedown'} | ${{defaultValue: parseColor('#f000f0')}} | ${{forward: (elem) => pressKey(elem, {key: 'PageUp'}), backward: (elem) => pressKey(elem, {key: 'PageDown'})}} | ${parseColor('#f010f0')} + ${'home/end'} | ${{defaultValue: parseColor('#f000f0')}} | ${{forward: (elem) => pressKey(elem, {key: 'Home'}), backward: (elem) => pressKey(elem, {key: 'End'})}} | ${parseColor('#f000e0')} `('$Name', ({props, actions: {forward, backward}, result}) => { let {getAllByRole} = render( { expect(onChangeEndSpy.mock.calls[1][0].toString('rgba')).toBe(props.defaultValue.toString('rgba')); }); + it.each` + Name | props | actions | result + ${'left/right'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'ArrowRight'}), backward: (elem) => pressKey(elem, {key: 'ArrowLeft'})}} | ${parseColor('#ff00fe')} + ${'up/down'} | ${{defaultValue: parseColor('#ff00ff')}} | ${{forward: (elem) => pressKey(elem, {key: 'ArrowUp'}), backward: (elem) => pressKey(elem, {key: 'ArrowDown'})}} | ${parseColor('#ff01ff')} + ${'shiftleft/shiftright'} | ${{defaultValue: parseColor('#f000f0')}} | ${{forward: (elem) => pressKey(elem, {key: 'ArrowRight', shiftKey: true}), backward: (elem) => pressKey(elem, {key: 'ArrowLeft', shiftKey: true})}} | ${parseColor('#f000e0')} + ${'shiftup/shiftdown'} | ${{defaultValue: parseColor('#f000f0')}} | ${{forward: (elem) => pressKey(elem, {key: 'ArrowUp', shiftKey: true}), backward: (elem) => pressKey(elem, {key: 'ArrowDown', shiftKey: true})}} | ${parseColor('#f010f0')} + ${'pageup/pagedown'} | ${{defaultValue: parseColor('#f000f0')}} | ${{forward: (elem) => pressKey(elem, {key: 'PageUp'}), backward: (elem) => pressKey(elem, {key: 'PageDown'})}} | ${parseColor('#f010f0')} + ${'home/end'} | ${{defaultValue: parseColor('#f000f0')}} | ${{forward: (elem) => pressKey(elem, {key: 'End'}), backward: (elem) => pressKey(elem, {key: 'Home'})}} | ${parseColor('#f000e0')} + `('$Name RTL', ({props, actions: {forward, backward}, result}) => { + let {getAllByRole} = render( + + + + ); + let sliders = getAllByRole('slider'); + userEvent.tab(); + + forward(sliders[0]); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy.mock.calls[0][0].toString('rgba')).toBe(result.toString('rgba')); + expect(onChangeEndSpy).toHaveBeenCalledTimes(1); + expect(onChangeEndSpy.mock.calls[0][0].toString('rgba')).toBe(result.toString('rgba')); + + backward(sliders[0]); + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeSpy.mock.calls[1][0].toString('rgba')).toBe(props.defaultValue.toString('rgba')); + expect(onChangeEndSpy).toHaveBeenCalledTimes(2); + expect(onChangeEndSpy.mock.calls[1][0].toString('rgba')).toBe(props.defaultValue.toString('rgba')); + }); + it('no events when disabled', () => { let defaultColor = parseColor('#ff00ff'); let {getAllByRole, getByRole} = render(
diff --git a/packages/@react-stately/color/src/useColorAreaState.ts b/packages/@react-stately/color/src/useColorAreaState.ts index 974da26db31..1a82f98ea73 100644 --- a/packages/@react-stately/color/src/useColorAreaState.ts +++ b/packages/@react-stately/color/src/useColorAreaState.ts @@ -116,29 +116,41 @@ export function useColorAreaState(props: ColorAreaProps): ColorAreaState { return {xChannel, yChannel, zChannel}; }, [xChannel, yChannel]); + let xChannelRange = color.getChannelRange(channels.xChannel); + let yChannelRange = color.getChannelRange(channels.yChannel); + let {minValue: minValueX, maxValue: maxValueX, step: stepX, pageSize: pageSizeX} = xChannelRange; + let {minValue: minValueY, maxValue: maxValueY, step: stepY, pageSize: pageSizeY} = yChannelRange; + if (isNaN(xChannelStep)) { - xChannelStep = color.getChannelRange(channels.xChannel).step; + xChannelStep = stepX; } if (isNaN(yChannelStep)) { - yChannelStep = color.getChannelRange(channels.yChannel).step; + yChannelStep = stepY; } + let xChannelPageStep = Math.max(pageSizeX, xChannelStep); + let yChannelPageStep = Math.max(pageSizeY, yChannelStep); + let [isDragging, setDragging] = useState(false); let isDraggingRef = useRef(false).current; let xValue = color.getChannelValue(channels.xChannel); let yValue = color.getChannelValue(channels.yChannel); let setXValue = (v: number) => { + if (v === xValue) { + return; + } valueRef.current = color.withChannelValue(channels.xChannel, v); setColor(valueRef.current); }; let setYValue = (v: number) => { + if (v === yValue) { + return; + } valueRef.current = color.withChannelValue(channels.yChannel, v); setColor(valueRef.current); }; - let xChannelPageStep = Math.max(color.getChannelRange(channels.xChannel).pageSize, xChannelStep); - let yChannelPageStep = Math.max(color.getChannelRange(channels.yChannel).pageSize, yChannelStep); return { channels, @@ -177,27 +189,21 @@ export function useColorAreaState(props: ColorAreaProps): ColorAreaState { } }, getThumbPosition() { - let {minValue: minValueX, maxValue: maxValueX} = color.getChannelRange(channels.xChannel); - let {minValue: minValueY, maxValue: maxValueY} = color.getChannelRange(channels.yChannel); let x = (xValue - minValueX) / (maxValueX - minValueX); let y = 1 - (yValue - minValueY) / (maxValueY - minValueY); return {x, y}; }, incrementX(stepSize) { - let range = color.getChannelRange(channels.xChannel); - setXValue(snapValueToStep(xValue + stepSize, range.minValue, range.maxValue, stepSize)); + setXValue(snapValueToStep(xValue + stepSize, minValueX, maxValueX, stepSize)); }, incrementY(stepSize) { - let range = color.getChannelRange(channels.yChannel); - setYValue(snapValueToStep(yValue + stepSize, range.minValue, range.maxValue, stepSize)); + setYValue(snapValueToStep(yValue + stepSize, minValueY, maxValueY, stepSize)); }, decrementX(stepSize) { - let range = color.getChannelRange(channels.xChannel); - setXValue(snapValueToStep(xValue - stepSize, range.minValue, range.maxValue, stepSize)); + setXValue(snapValueToStep(xValue - stepSize, minValueX, maxValueX, stepSize)); }, decrementY(stepSize) { - let range = color.getChannelRange(channels.yChannel); - setYValue(snapValueToStep(yValue - stepSize, range.minValue, range.maxValue, stepSize)); + setYValue(snapValueToStep(yValue - stepSize, minValueY, maxValueY, stepSize)); }, setDragging(isDragging) { let wasDragging = isDraggingRef;