From f6f61adc2592c0707005cf0aefdba4e5588df2bf Mon Sep 17 00:00:00 2001 From: cheton Date: Tue, 29 Aug 2023 15:52:39 +0800 Subject: [PATCH] feat: implement `:focus-visible` for the Tree component with targeted focus style for non-pointer devices --- packages/react/src/tree/TreeItemContent.js | 25 +- .../react/src/tree/__tests__/Tree.test.js | 2 + .../__tests__/__snapshots__/Tree.test.js.snap | 289 ++++++++++++++++++ packages/react/src/tree/styles.js | 10 +- 4 files changed, 303 insertions(+), 23 deletions(-) create mode 100644 packages/react/src/tree/__tests__/__snapshots__/Tree.test.js.snap diff --git a/packages/react/src/tree/TreeItemContent.js b/packages/react/src/tree/TreeItemContent.js index 189dd6f033..0bbc6ffd99 100644 --- a/packages/react/src/tree/TreeItemContent.js +++ b/packages/react/src/tree/TreeItemContent.js @@ -1,7 +1,6 @@ import { useMergeRefs } from '@tonic-ui/react-hooks'; import { callEventHandlers } from '@tonic-ui/utils'; -import { ensureArray } from 'ensure-type'; -import React, { forwardRef, useCallback, useMemo } from 'react'; +import React, { forwardRef, useCallback } from 'react'; import { Box } from '../box'; import { useTheme } from '../theme'; import { useTreeItemContentStyle } from './styles'; @@ -12,7 +11,7 @@ const TreeItemContent = forwardRef(( { onClick: onClickProp, onMouseDown: onMouseDownProp, - sx: sxProp, + style: styleProp, ...rest }, ref, @@ -62,30 +61,20 @@ const TreeItemContent = forwardRef(( } }, [isDisabled]); + const style = { + paddingLeft: `calc(${nodeDepth} * ${sizes['6x']} + ${sizes['3x']})`, + ...styleProp, + }; const tabIndex = -1; const styleProps = useTreeItemContentStyle({ isDisabled, isSelected, tabIndex }); - const sxProps = useMemo(() => ([ - { - pl: `calc(${nodeDepth} * ${sizes['6x']} + ${sizes['3x']})`, - pr: `calc(${sizes['3x']})`, - ':focus-visible': { - borderStyle: 'solid', - borderWidth: '1h', - pl: `calc(${nodeDepth} * ${sizes['6x']} + ${sizes['3x']} - ${sizes['1h']})`, - pr: `calc(${sizes['3x']} - ${sizes['1h']})`, - py: `calc(${sizes['2x']} - ${sizes['1h']})`, - }, - }, - ...ensureArray(sxProp), - ]), [nodeDepth, sizes, sxProp]); return ( diff --git a/packages/react/src/tree/__tests__/Tree.test.js b/packages/react/src/tree/__tests__/Tree.test.js index ebe1c14b00..164ca1dcbb 100644 --- a/packages/react/src/tree/__tests__/Tree.test.js +++ b/packages/react/src/tree/__tests__/Tree.test.js @@ -210,6 +210,8 @@ describe('Tree', () => { expect(screen.getByRole('tree')).toHaveAttribute('aria-multiselectable', 'true'); + expect(container).toMatchSnapshot(); + await testA11y(container); }); diff --git a/packages/react/src/tree/__tests__/__snapshots__/Tree.test.js.snap b/packages/react/src/tree/__tests__/__snapshots__/Tree.test.js.snap new file mode 100644 index 0000000000..aa46035b33 --- /dev/null +++ b/packages/react/src/tree/__tests__/__snapshots__/Tree.test.js.snap @@ -0,0 +1,289 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Tree should render correctly 1`] = ` +.emotion-0 { + outline: 0; +} + +.emotion-4 { + color: var(--tonic-colors-white-primary); + cursor: pointer; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-text-decoration: none; + text-decoration: none; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + outline: 0; + padding-left: var(--tonic-space-3x); + padding-right: var(--tonic-space-3x); + padding-top: var(--tonic-space-2x); + padding-bottom: var(--tonic-space-2x); + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + width: 100%; +} + +.emotion-4:focus-visible { + box-shadow: inset 0 0 0 .125rem #1e5ede; +} + +.emotion-4:hover, +.emotion-4[data-hover] { + background-color: rgba(255, 255, 255, 0.12); +} + +.emotion-4:hover+[role="group"] { + position: relative; +} + +.emotion-4:hover+[role="group"]::before { + background-color: rgba(255, 255, 255, 0.12); + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 19.5px; + width: 1px; +} + +.emotion-6 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex: none; + -ms-flex: none; + flex: none; + width: var(--tonic-sizes-6x); +} + +.emotion-8 { + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + appearance: none; + background-color: var(--tonic-colors-transparent); + border: none; + color: inherit; + cursor: pointer; + outline: 0; + padding: 0; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; +} + +.emotion-10 { + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + color: var(--tonic-colors-white-secondary); + -webkit-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -ms-transform: rotate(0deg); + transform: rotate(0deg); + -webkit-transition: -webkit-transform 93ms cubic-bezier(0.0, 0, 0.2, 1); + transition: transform 93ms cubic-bezier(0.0, 0, 0.2, 1); +} + +.emotion-10:hover, +.emotion-10[data-hover] { + color: var(--tonic-colors-white-primary); +} + +.emotion-12 { + width: var(--tonic-sizes-4x); + height: var(--tonic-sizes-4x); + fill: currentColor; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + vertical-align: middle; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; +} + +.emotion-12:not(:root) { + overflow: hidden; +} + +.emotion-14 { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + -webkit-flex: auto; + -ms-flex: auto; + flex: auto; + font-weight: var(--tonic-fontWeights-normal); +} + +
+
+
+
+
+ +
+
+ Node 1 +
+
+
+
+
+
+ +
+
+ Node 7 +
+
+
+
+
+
+ +
+
+ Node 9 +
+
+
+
+
+`; diff --git a/packages/react/src/tree/styles.js b/packages/react/src/tree/styles.js index fada0c06da..ab2a05f722 100644 --- a/packages/react/src/tree/styles.js +++ b/packages/react/src/tree/styles.js @@ -1,4 +1,5 @@ import { useColorMode } from '../color-mode'; +import { useTheme } from '../theme'; const useTreeItemStyle = () => { return { @@ -10,6 +11,7 @@ const useTreeItemContentStyle = ({ isSelected, tabIndex, }) => { + const { colors, sizes } = useTheme(); const [colorMode] = useColorMode(); const color = { dark: 'white:primary', @@ -19,7 +21,7 @@ const useTreeItemContentStyle = ({ dark: 'rgba(255, 255, 255, 0.12)', light: 'rgba(0, 0, 0, 0.12)', }[colorMode]; - const focusBorderColor = { + const focusBoxShadowColor = { dark: 'blue:60', light: 'blue:60', }[colorMode]; @@ -48,10 +50,8 @@ const useTreeItemContentStyle = ({ _hover: { backgroundColor: !isDisabled ? hoverBackgroundColor : undefined, }, - _focus: { - borderColor: focusBorderColor, - - // Note: The border will be added in TreeItemContent + _focusVisible: { + boxShadow: `inset 0 0 0 ${sizes?.['1h']} ${colors?.[focusBoxShadowColor]}`, }, }; };