@shopgate/engage
Version:
Shopgate's ENGAGE library.
291 lines (285 loc) • 9.19 kB
JavaScript
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);