UNPKG

@shopgate/engage

Version:
291 lines (285 loc) • 9.19 kB
import _inheritsLoose from "@babel/runtime/helpers/inheritsLoose"; import React, { Component } from 'react'; import PropTypes from 'prop-types'; import isMatch from 'lodash/isMatch'; import isEqual from 'lodash/isEqual'; import { broadcastLiveMessage } from '@shopgate/engage/a11y'; import { responsiveCondition } from '@shopgate/engage/styles'; import connect from "./connector"; import VariantsContext from "./context"; import { isCharacteristicEnabled, getSelectedValue, prepareState, selectCharacteristics } from "./helpers"; /** * The ProductCharacteristics component. */ import { jsx as _jsx } from "react/jsx-runtime"; let ProductCharacteristics = /*#__PURE__*/function (_Component) { /** * @param {Object} props The component props. */ function ProductCharacteristics(_props) { var _this; _this = _Component.call(this, _props) || this; /** * Sets the refs to the characteristics selects. * @param {Object} props The props to check against. */ _this.setRefs = props => { const { variants } = props; if (variants) { variants.characteristics.forEach(char => { _this.refsStore[char.id] = /*#__PURE__*/React.createRef(); }); } }; /** * Checks if all selections have been made. * @return {boolean} */ _this.checkSelection = () => { const { characteristics } = _this.state; const { variants, variantId } = _this.props; if (!variants) { return true; } const filteredValues = Object.keys(characteristics).filter(key => !!characteristics[key]); const selected = !!(filteredValues.length === variants.characteristics.length && variantId); if (!selected) { const firstUnselected = _this.findUnselectedCharacteristic(); if (firstUnselected) { const ref = _this.refsStore[firstUnselected.id]; // Focus the item for screen readers and broadcast a related live message. ref.current.focus(); const option = ref.current.innerText; broadcastLiveMessage('product.pick_option_first', { params: { option } }); if (responsiveCondition('>xs', { webOnly: true })) { ref.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); } else { ref.current.scrollIntoView({ behavior: 'smooth' }); } _this.setState({ highlight: firstUnselected.id }); } } return selected; }; _this.checkSelectedCharacteristics = () => { const { characteristics } = _this.state; const { variantId, variants, finishTimeout } = _this.props; if (!variants) { return; } const filteredValues = Object.keys(characteristics).filter(key => !!characteristics[key]); if (filteredValues.length !== variants.characteristics.length) { return; } const products = variants.products.filter(product => isMatch(product.characteristics, characteristics)); if (!products.length) { return; } if (products[0].id === variantId) { return; } setTimeout(() => { _this.props.navigate(products[0].id); }, finishTimeout); }; /** * Stores a selected characteristic into the local state. * @param {Object} selection The selected item. */ _this.handleSelection = selection => { const { variants, setCharacteristics } = _this.props; const { id, value } = selection; _this.setState(({ characteristics }) => { const state = prepareState(id, value, characteristics, variants.characteristics, variants.products); setCharacteristics(state); return { characteristics: { ...state }, highlight: null }; }, _this.checkSelectedCharacteristics); }; /** * @param {Object} selections The selections stored in the state. * @param {string} charId The current characteristic ID. * @param {Array} values The characteristic values. * @param {number} charIndex The characteristic index. * @param {string|null} selectedValue selectedValue * @param {boolean} charDisabled Whether the characteristic for the values is disabled * @return {Array} */ _this.buildValues = (selections, charId, values, charIndex, selectedValue, charDisabled) => { // If this is the first characteristic then all values are selectable. if (charIndex === 0) { return values.map(value => ({ ...value, selectable: !charDisabled, selected: selectedValue === value.id })); } const { variants } = _this.props; const subset = {}; Object.keys(selections).forEach((item, index) => { if (index < charIndex) { subset[item] = selections[item]; } }); // Filter products that match or partially match the current characteristic selection. const products = variants.products.filter(({ characteristics }) => isMatch(characteristics, subset)); // Check if any of the values are present inside any of the matching products. return values.map(value => { const selectable = products.some(({ characteristics }) => isMatch(characteristics, { [charId]: value.id })); return { ...value, selectable: charDisabled ? false : selectable, selected: selectedValue === value.id }; }); }; /** * Resets the highlight state */ _this.resetHighlight = () => { _this.setState({ highlight: null }); }; _this.refsStore = {}; const _characteristics = selectCharacteristics(_props); _this.state = { highlight: null, characteristics: _characteristics }; _props.setCharacteristics(_characteristics); _this.setRefs(_props); _props.conditioner.addConditioner('product-variants', _this.checkSelection); return _this; } /** @inheritDoc */ _inheritsLoose(ProductCharacteristics, _Component); var _proto = ProductCharacteristics.prototype; _proto.componentDidMount = function componentDidMount() { this.checkSelectedCharacteristics(); } /** * @param {Object} nextProps The next component props. */; _proto.UNSAFE_componentWillReceiveProps = function UNSAFE_componentWillReceiveProps(nextProps) { if (!this.props.variants && nextProps.variants) { // Initialize refs and characteristics when the variants prop was updated with a valid value. this.setRefs(nextProps); const characteristics = selectCharacteristics(nextProps); this.setState({ characteristics }, this.checkSelectedCharacteristics); // Inform parent component about potential updates e.g. preselected characteristic values nextProps.setCharacteristics(characteristics); } else if (nextProps.characteristics && !isEqual(this.state.characteristics, nextProps.characteristics)) { // Sync back characteristics from parent if set this.setState({ characteristics: nextProps.characteristics }); } }; /** * Finds the first unselected characteristic. * @return {Object|null} */ _proto.findUnselectedCharacteristic = function findUnselectedCharacteristic() { const { characteristics } = this.state; const unselected = this.props.variants.characteristics.filter(char => !characteristics.hasOwnProperty(char.id)); if (unselected.length) { return unselected[0]; } return null; }; /** * @return {JSX} */ _proto.render = function render() { const { characteristics } = this.state; const { variants } = this.props; if (!variants) { return null; } return /*#__PURE__*/_jsx(VariantsContext.Provider, { value: this.state, children: variants.characteristics.map((char, index) => { const disabled = !isCharacteristicEnabled(characteristics, index); const selected = getSelectedValue(char.id, characteristics); const values = this.buildValues(characteristics, char.id, char.values, index, selected, disabled); return this.props.render({ charRef: this.refsStore[char.id], disabled, highlight: this.state.highlight === char.id, id: char.id, key: char.id, label: char.label, swatch: !!char.swatch, // BETA select: this.handleSelection, selected, values, resetHighlight: this.resetHighlight }); }) }); }; return ProductCharacteristics; }(Component); ProductCharacteristics.defaultProps = { finishTimeout: 0, variantId: null, variants: null, characteristics: null }; export default connect(ProductCharacteristics);