From eee2aaf6d66f5550ce920809112914408b433f9f Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Thu, 9 May 2024 16:26:19 -0700 Subject: [PATCH] Rework the swatch component --- .../app/components/product-tile/index.jsx | 13 +- .../app/components/swatch-group/index.jsx | 121 +++++++++++++++--- .../app/components/swatch-group/swatch.jsx | 46 ++----- 3 files changed, 122 insertions(+), 58 deletions(-) diff --git a/packages/template-retail-react-app/app/components/product-tile/index.jsx b/packages/template-retail-react-app/app/components/product-tile/index.jsx index 5d09e20791..7f32e01a07 100644 --- a/packages/template-retail-react-app/app/components/product-tile/index.jsx +++ b/packages/template-retail-react-app/app/components/product-tile/index.jsx @@ -157,7 +157,13 @@ const ProductTile = (props) => { ?.map(({id, values}) => { const attributeId = id return ( - + { + setSelectableAttributeValue(value) + }} + > {values?.map(({href, name, swatch, value}) => { const content = swatch ? ( { { - setSelectableAttributeValue(value) - }} value={value} name={name} variant={'circle'} - selected={value === selectableAttributeValue} + isFocusable={true} > {content} diff --git a/packages/template-retail-react-app/app/components/swatch-group/index.jsx b/packages/template-retail-react-app/app/components/swatch-group/index.jsx index 61f4af9959..9ad01767e3 100644 --- a/packages/template-retail-react-app/app/components/swatch-group/index.jsx +++ b/packages/template-retail-react-app/app/components/swatch-group/index.jsx @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import React from 'react' +import React, {Children, useCallback, useEffect, useRef, useState} from 'react' import PropTypes from 'prop-types' import { Flex, @@ -14,30 +14,107 @@ import { useStyleConfig } from '@salesforce/retail-react-app/app/components/shared/ui' import {FormattedMessage} from 'react-intl' +import {noop} from '@salesforce/retail-react-app/app/utils/utils' + +const DIRECTIONS = { + FORWARD: 1, + BACKWARD: -1 +} /** * SwatchGroup allows you to create a list of swatches * Each Swatch is a link with will direct to a href passed to them */ const SwatchGroup = (props) => { - const {displayName, children, label = ''} = props + const {displayName, children, label = '', value, handleChange = noop} = props + const styles = useStyleConfig('SwatchGroup') + const [selectedIndex, setSelectedIndex] = useState(0) + const wrapperRef = useRef(null) + + // Handle keyboard navigation. + const onKeyDown = useCallback( + (e) => { + const {key} = e + const move = (direction = DIRECTIONS.FORWARD) => { + let index = selectedIndex + direction // forward = +1 backwards = -1 + index = (selectedIndex + direction) % children.length // keep number in bounds of array with modulus + index = index < 0 ? children.length - Math.abs(index) : Math.abs(index) // We we are dealing with a negative we have to invert the index + + // Get a reference to the newly selected swatch as we are going to focus it later. + const swatchEl = wrapperRef?.current?.children[index] + + // Set the new index that is always in the arrays range. + setSelectedIndex(index) + + // Behave like a radio button a focus the new swatch. + swatchEl?.focus() + } + + switch (key) { + case 'ArrowUp': + case 'ArrowLeft': + e.preventDefault() + move(DIRECTIONS.BACKWARD) + break + case 'ArrowDown': + case 'ArrowRight': + e.preventDefault() + move(DIRECTIONS.FORWARD) + break + default: + break + } + }, + [selectedIndex] + ) + + // Initialize the component state on mount, this includes the selected index value. + useEffect(() => { + if (!value) { + return + } + const childrenArray = Children.toArray(children) + const index = childrenArray.findIndex(({props}) => props?.value === value) + + setSelectedIndex(index) + }, []) + + // Whenever the selected index changes ensure that we call the change handler. + useEffect(() => { + const childrenArray = Children.toArray(children) + const newValue = childrenArray[selectedIndex].props.value + + handleChange(newValue) + }, [selectedIndex]) + return ( - - {label && ( - - - - - {displayName} - - )} - {children} - + + + {label && ( + + + + + {displayName} + + )} + + {Children.toArray(children).map((child) => { + const selected = child.props.value === value + return React.cloneElement(child, { + handleSelect: handleChange, + selected, + isFocusable: selected + }) + })} + + + ) } @@ -55,7 +132,15 @@ SwatchGroup.propTypes = { /** * The Swatch options to choose between */ - children: PropTypes.node + children: PropTypes.node, + /** + * This function is called whenever the selected swatch changes. + */ + handleChange: PropTypes.func, + /** + * The currentvalue for the option. + */ + value: PropTypes.string } export default SwatchGroup diff --git a/packages/template-retail-react-app/app/components/swatch-group/swatch.jsx b/packages/template-retail-react-app/app/components/swatch-group/swatch.jsx index df25ed3277..035b63aa2f 100644 --- a/packages/template-retail-react-app/app/components/swatch-group/swatch.jsx +++ b/packages/template-retail-react-app/app/components/swatch-group/swatch.jsx @@ -29,53 +29,30 @@ const Swatch = ({ selected, isFocusable, value, - handleChange, + handleSelect, variant = 'square' }) => { const styles = useMultiStyleConfig('SwatchGroup', {variant, disabled, selected}) const isDesktop = useBreakpointValue({base: false, lg: true}) - const [changeHandlers, setChangeHandlers] = useState({}) + const [selectHandlers, setSelectHandlers] = useState({}) - const onKeyDown = useCallback((evt) => { - let sibling - // This is not a very react-y way implementation... ¯\_(ツ)_/¯ - switch (evt.key) { - case 'ArrowUp': - case 'ArrowLeft': - evt.preventDefault() - sibling = - evt.target.previousElementSibling || evt.target.parentElement.lastElementChild - break - case 'ArrowDown': - case 'ArrowRight': - evt.preventDefault() - sibling = - evt.target.nextElementSibling || evt.target.parentElement.firstElementChild - break - default: - break - } - sibling?.click() - sibling?.focus() - }, []) - - const onChange = useCallback( + const onSelect = useCallback( (e) => { e.preventDefault() - handleChange(value) + handleSelect(value) }, - [handleChange] + [handleSelect] ) useEffect(() => { - if (!handleChange) { + if (!handleSelect) { return } - setChangeHandlers({ - [isDesktop ? 'onMouseEnter' : 'onClick']: onChange + setSelectHandlers({ + [isDesktop ? 'onMouseEnter' : 'onClick']: onSelect }) - }, [onChange, isDesktop]) + }, [onSelect, isDesktop]) return (