diff --git a/packages/grid/src/elements/pane/components/Splitter.tsx b/packages/grid/src/elements/pane/components/Splitter.tsx index 21dd07f8f5b..6b60afab009 100644 --- a/packages/grid/src/elements/pane/components/Splitter.tsx +++ b/packages/grid/src/elements/pane/components/Splitter.tsx @@ -5,19 +5,10 @@ * found at http://www.apache.org/licenses/LICENSE-2.0. */ -import React, { - useContext, - useEffect, - forwardRef, - useMemo, - useState, - useRef, - HTMLAttributes -} from 'react'; +import React, { useContext, useEffect, forwardRef, useMemo, useRef, HTMLAttributes } from 'react'; import mergeRefs from 'react-merge-refs'; import PropTypes from 'prop-types'; import { ThemeContext } from 'styled-components'; -import { composeEventHandlers } from '@zendeskgarden/container-utilities'; import { useSplitter } from '@zendeskgarden/container-splitter'; import { usePaneProviderContextData } from '../../../utils/usePaneProviderContext'; import usePaneContext from '../../../utils/usePaneContext'; @@ -43,6 +34,7 @@ const orientationToDimension: Record = { const SplitterComponent = forwardRef( ( { + children, providerId, layoutKey, min, @@ -61,7 +53,6 @@ const SplitterComponent = forwardRef( const paneContext = usePaneContext(); const themeContext = useContext(ThemeContext); const environment = useDocument(themeContext); - const [isHovered, setIsHovered] = useState(false); const isRow = orientationToDimension[orientation!] === 'rows'; const separatorRef = useRef(null); @@ -131,14 +122,6 @@ const SplitterComponent = forwardRef( const size = isRow ? separatorRef.current?.clientWidth : separatorRef.current?.clientHeight; - const onMouseOver = useMemo( - () => - composeEventHandlers(props.onMouseOver, (event: MouseEvent) => - setIsHovered(event.target === separatorRef.current) - ), - [props.onMouseOver, separatorRef] - ); - return ( ( )} > + {children /* Splitter.Button is the only valid child */} ); } diff --git a/packages/grid/src/elements/pane/components/SplitterButton.tsx b/packages/grid/src/elements/pane/components/SplitterButton.tsx index f7083be72e9..b0b38b25bf3 100644 --- a/packages/grid/src/elements/pane/components/SplitterButton.tsx +++ b/packages/grid/src/elements/pane/components/SplitterButton.tsx @@ -8,7 +8,7 @@ import React, { forwardRef, useCallback } from 'react'; import { Tooltip } from '@zendeskgarden/react-tooltips'; import { composeEventHandlers } from '@zendeskgarden/container-utilities'; -import { StyledPaneSplitterButton } from '../../../styled'; +import { StyledPaneSplitterButton, StyledPaneSplitterButtonContainer } from '../../../styled'; import { ISplitterButtonProps } from '../../../types'; import usePaneSplitterContext from '../../../utils/usePaneSplitterContext'; import { usePaneProviderContextData } from '../../../utils/usePaneProviderContext'; @@ -62,20 +62,29 @@ const SplitterButtonComponent = forwardRef e.stopPropagation()}> - - + + e.stopPropagation()} + > + + + ); } ); diff --git a/packages/grid/src/styled/index.ts b/packages/grid/src/styled/index.ts index 5084a101826..48b99566ee8 100644 --- a/packages/grid/src/styled/index.ts +++ b/packages/grid/src/styled/index.ts @@ -13,3 +13,4 @@ export * from './pane/StyledPane'; export * from './pane/StyledPaneContent'; export * from './pane/StyledPaneSplitter'; export * from './pane/StyledPaneSplitterButton'; +export * from './pane/StyledPaneSplitterButtonContainer'; diff --git a/packages/grid/src/styled/pane/StyledPaneSplitter.ts b/packages/grid/src/styled/pane/StyledPaneSplitter.ts index 99a84a185ca..d26f2a1725e 100644 --- a/packages/grid/src/styled/pane/StyledPaneSplitter.ts +++ b/packages/grid/src/styled/pane/StyledPaneSplitter.ts @@ -19,7 +19,6 @@ import { Orientation } from '../../types'; const COMPONENT_ID = 'pane.splitter'; interface IStyledPaneSplitterProps { - isHovered: boolean; orientation?: Orientation; isFixed?: boolean; } @@ -35,7 +34,7 @@ const colorStyles = (props: IStyledPaneSplitterProps & ThemeProps) } &:hover::before { - background-color: ${props.isHovered && hoverColor}; + background-color: ${hoverColor}; } ${focusStyles({ @@ -48,7 +47,7 @@ const colorStyles = (props: IStyledPaneSplitterProps & ThemeProps) })} &:active::before { - background-color: ${props.isHovered && activeColor}; + background-color: ${activeColor}; } `; }; @@ -140,7 +139,7 @@ const sizeStyles = (props: IStyledPaneSplitterProps & ThemeProps) } &:hover::before { - ${dimensionProperty}: ${props.isHovered && separatorSize}; + ${dimensionProperty}: ${separatorSize}; } &:focus::before, diff --git a/packages/grid/src/styled/pane/StyledPaneSplitterButton.ts b/packages/grid/src/styled/pane/StyledPaneSplitterButton.ts index 06c3a2f286e..bb26b7d0ff3 100644 --- a/packages/grid/src/styled/pane/StyledPaneSplitterButton.ts +++ b/packages/grid/src/styled/pane/StyledPaneSplitterButton.ts @@ -6,27 +6,29 @@ */ import styled, { css, ThemeProps, DefaultTheme } from 'styled-components'; -import { math, stripUnit } from 'polished'; -import { - retrieveComponentStyles, - DEFAULT_THEME, - getColorV8, - focusStyles, - SELECTOR_FOCUS_VISIBLE -} from '@zendeskgarden/react-theming'; -import { ISplitterButtonProps, Orientation, PLACEMENT } from '../../types'; +import { retrieveComponentStyles, DEFAULT_THEME } from '@zendeskgarden/react-theming'; import { ChevronButton } from '@zendeskgarden/react-buttons'; -import { StyledPaneSplitter } from './StyledPaneSplitter'; +import { Orientation } from '../../types'; const COMPONENT_ID = 'pane.splitter_button'; -interface IStyledSplitterButtonProps extends ISplitterButtonProps { +interface IStyledSplitterButtonProps { orientation: Orientation; - placement: (typeof PLACEMENT)[number]; isRotated: boolean; - splitterSize: number; } +export const getSize = (theme: DefaultTheme) => theme.space.base * 6; + +const sizeStyles = ({ theme }: ThemeProps) => { + const size = `${getSize(theme)}px`; + + return css` + width: ${size}; + min-width: ${size}; + height: ${size}; + `; +}; + const transformStyles = (props: IStyledSplitterButtonProps & ThemeProps) => { let degrees = 0; @@ -49,70 +51,6 @@ const transformStyles = (props: IStyledSplitterButtonProps & ThemeProps) => { - const boxShadow = theme.shadows.lg( - `${theme.space.base}px`, - `${theme.space.base * 2}px`, - getColorV8('chromeHue', 600, theme, 0.15)! - ); - - return css` - box-shadow: ${boxShadow}; - - ${focusStyles({ - theme, - boxShadow - })} - `; -}; - -const sizeStyles = (props: IStyledSplitterButtonProps & ThemeProps) => { - const size = `${props.theme.space.base * 6}px`; - const display = - props.splitterSize <= - (stripUnit(math(`${props.theme.shadowWidths.md} * 2 + ${size}`)) as number) && 'none'; - const isVertical = props.orientation === 'start' || props.orientation === 'end'; - let top; - let left; - let right; - let bottom; - - if (props.splitterSize >= (stripUnit(math(`${size} * 3`)) as number)) { - if (props.placement === 'start') { - if (isVertical) { - top = size; - } else if (props.theme.rtl) { - right = size; - } else { - left = size; - } - } else if (props.placement === 'end') { - if (isVertical) { - bottom = size; - } else if (props.theme.rtl) { - left = size; - } else { - right = size; - } - } - } - - return css` - display: ${display}; - /* stylelint-disable declaration-block-no-redundant-longhand-properties */ - top: ${top}; - right: ${right}; - bottom: ${bottom}; - left: ${left}; - width: ${size}; - min-width: ${size}; - height: ${size}; - `; -}; - -/** - * 1. Opaque "dish" behind transparent button - */ export const StyledPaneSplitterButton = styled(ChevronButton).attrs({ 'data-garden-id': COMPONENT_ID, 'data-garden-version': PACKAGE_VERSION, @@ -120,36 +58,9 @@ export const StyledPaneSplitterButton = styled(ChevronButton).attrs` - position: absolute; - /* prettier-ignore */ - transition: - box-shadow 0.1s ease-in-out, - background-color 0.25s ease-in-out, - opacity 0.25s ease-in-out 0.1s; - opacity: 0; - ${sizeStyles}; - ${transformStyles}; - - /* [1] */ - &::before { - position: absolute; - z-index: -1; - background-color: ${props => getColorV8('background', 600 /* default shade */, props.theme)}; - width: 100%; - height: 100%; - content: ''; - } - - ${colorStyles}; - /* stylelint-disable selector-no-qualifying-type */ - ${StyledPaneSplitter}:hover &, - ${StyledPaneSplitter}:focus-visible &, - ${StyledPaneSplitter}[data-garden-focus-visible] &, - ${SELECTOR_FOCUS_VISIBLE} { - opacity: 1; - } + ${transformStyles}; ${props => retrieveComponentStyles(COMPONENT_ID, props)}; `; diff --git a/packages/grid/src/styled/pane/StyledPaneSplitterButtonContainer.ts b/packages/grid/src/styled/pane/StyledPaneSplitterButtonContainer.ts new file mode 100644 index 00000000000..a64db5471ec --- /dev/null +++ b/packages/grid/src/styled/pane/StyledPaneSplitterButtonContainer.ts @@ -0,0 +1,183 @@ +/** + * Copyright Zendesk, Inc. + * + * Use of this source code is governed under the Apache License, Version 2.0 + * found at http://www.apache.org/licenses/LICENSE-2.0. + */ + +import styled, { css, ThemeProps, DefaultTheme } from 'styled-components'; +import { math, stripUnit } from 'polished'; +import { DEFAULT_THEME, getColorV8, retrieveComponentStyles } from '@zendeskgarden/react-theming'; +import { StyledPaneSplitter } from './StyledPaneSplitter'; +import { getSize } from './StyledPaneSplitterButton'; +import { Orientation, PLACEMENT } from '../../types'; + +const COMPONENT_ID = 'pane.splitter_button_container'; + +interface IStyledSplitterButtonContainerProps { + orientation: Orientation; + placement: (typeof PLACEMENT)[number]; + splitterSize: number; +} + +const colorStyles = ({ theme }: ThemeProps) => { + const backgroundColor = getColorV8('background', 600 /* default shade */, theme); + const boxShadow = theme.shadows.lg( + `${theme.space.base}px`, + `${theme.space.base * 2}px`, + getColorV8('chromeHue', 600, theme, 0.15)! + ); + + return css` + box-shadow: ${boxShadow}; + background-color: ${backgroundColor}; + `; +}; + +const positionStyles = (props: IStyledSplitterButtonContainerProps & ThemeProps) => { + let top; + let left; + let right; + let bottom; + const size = getSize(props.theme); + const inset = `-${size / 2}px`; + + if (props.placement === 'center' || props.splitterSize < size * 3) { + const center = `${props.splitterSize / 2 - size / 2}px`; + + switch (`${props.orientation}-${props.theme.rtl ? 'rtl' : 'ltr'}`) { + case 'top-ltr': + case 'top-rtl': + top = inset; + left = center; + break; + + case 'start-ltr': + case 'end-rtl': + top = center; + left = inset; + break; + + case 'end-ltr': + case 'start-rtl': + top = center; + right = inset; + break; + + case 'bottom-ltr': + case 'bottom-rtl': + bottom = inset; + right = center; + break; + } + } else { + const offset = `${size}px`; + + /* istanbul ignore next */ + switch (`${props.orientation}-${props.placement}-${props.theme.rtl ? 'rtl' : 'ltr'}`) { + case 'top-end-ltr': + case 'top-end-rtl': + case 'top-start-rtl': + top = inset; + right = offset; + break; + + case 'bottom-end-ltr': + case 'bottom-end-rtl': + case 'bottom-start-rtl': + bottom = inset; + right = offset; + break; + + case 'start-start-ltr': + case 'end-start-rtl': + top = offset; + left = inset; + break; + + case 'start-end-ltr': + case 'end-end-rtl': + bottom = offset; + left = inset; + break; + + case 'end-start-ltr': + case 'start-start-rtl': + top = offset; + right = inset; + break; + + case 'end-end-ltr': + case 'start-end-rtl': + bottom = offset; + right = inset; + break; + + case 'top-start-ltr': + top = inset; + left = offset; + break; + + case 'bottom-start-ltr': + bottom = inset; + left = offset; + break; + } + } + + return css` + /* stylelint-disable declaration-block-no-redundant-longhand-properties */ + top: ${top}; + right: ${right}; + bottom: ${bottom}; + left: ${left}; + `; +}; + +const sizeStyles = ({ theme }: ThemeProps) => { + const size = getSize(theme); + + return css` + border-radius: ${size}px; + width: ${size}px; + height: ${size}px; + `; +}; + +const minimumSplitterSize = (theme: DefaultTheme) => + stripUnit(math(`${theme.shadowWidths.md} * 2 + ${getSize(theme)}`)) as number; + +/** + * 1. Match focused `Splitter` z-index + */ +export const StyledPaneSplitterButtonContainer = styled.div` + display: ${props => + props.splitterSize <= minimumSplitterSize(props.theme) ? 'none' : undefined}; + position: absolute; + /* prettier-ignore */ + transition: + box-shadow 0.1s ease-in-out, + opacity 0.25s ease-in-out 0.1s; + opacity: 0; + z-index: 2; /* [1] */ + + ${positionStyles}; + + ${sizeStyles}; + + ${colorStyles}; + + &:hover, + &:focus-within, + /* stylelint-disable selector-no-qualifying-type */ + ${StyledPaneSplitter}:hover ~ &, + ${StyledPaneSplitter}:focus-visible ~ & { + opacity: 1; + } + + ${props => retrieveComponentStyles(COMPONENT_ID, props)}; +`; + +StyledPaneSplitterButtonContainer.defaultProps = { + theme: DEFAULT_THEME +};