UNPKG

@retailmenot/anchor

Version:

A React UI Library by RetailMeNot

216 lines (214 loc) 9.22 kB
var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; // REACT import * as React from 'react'; // VENDOR import classNames from 'classnames'; import styled, { css } from '@xstyled/styled-components'; import { th } from '@xstyled/system'; import { List } from '../../List'; // UTILS const { useEffect, useState, forwardRef, useImperativeHandle } = React; const ResultContainerSpaceFromAutoComplete = { sm: '2.6rem', md: '3.25rem', lg: '3.25rem', }; const StyledResultsContainer = styled('div') ` background-color: white; position: absolute; width: inherit; z-index: 3; box-sizing: border-box; box-shadow: 0 0.5rem 0.75rem -0.375rem rgba(0, 0, 0, 0.2); border-radius: 0 0 ${th.radius('base')} ${th.radius('base')}; padding: 1rem; ${({ size = 'md' }) => css({ top: ResultContainerSpaceFromAutoComplete[size] })}; `; const createResult = (label, value) => ({ label: label, value: value || label, }); const generateResults = (results, currentIndex, setCurrentIndex, emitSelectedItem, term, setTerm) => { const cleanResults = []; if (Array.isArray(results) && results.length) { // Generic arrays are converted to DataItems if (typeof results[0] === 'string') { results.forEach((result, index) => { const itemIndex = relativeIndex(index); cleanResults.push(createResult(result, { label: result || '', // Add one to index so that the input is an index active: itemIndex === currentIndex, key: `anchor-result-${itemIndex}`, onMouseOver: () => { setCurrentIndex(itemIndex); setTerm(term); }, onSelect: () => emitSelectedItem({ label: result, value: { label: result, value: result }, }), })); }); } else if (typeof results[0] === 'object') { results.forEach((_a, index) => { var { label = '' } = _a, props = __rest(_a, ["label"]); const itemIndex = relativeIndex(index); cleanResults.push(createResult(label, Object.assign(Object.assign({ label }, props), { // Add one to index so that the input is an index active: itemIndex === currentIndex, key: `anchor-result-${itemIndex}`, onMouseOver: () => { setCurrentIndex(itemIndex); setTerm(term); }, onSelect: () => emitSelectedItem({ label, value: Object.assign({ label }, props), }) }))); }); } } return cleanResults; }; /* Entire List: [ 0, 1, 2, 3, 4 ] Iterable List: [ 1, 3, 4 ] forward: - go to next iterable index - unless at max; go to 0 (assign initialTerm) backward: - go to previous iterable index - unless at 0; go to max (assign initialTerm) both - store initialTerm - Emit initialTerm or currentLabel - Update currentIndex =================================================== getNext(currentTerm: string, max = iterableList.length - 1, min = 0, currentIndex, direction): number emitTermAndUpdateIndex(newIndex, values): void • updateIndex(emitActiveIndex, setCurrentIndex): void • updateTerm(emitActiveIndex, setCurrentIndex): void */ const relativeIndex = (index) => index + 1; export const ResultsContainer = forwardRef((_a, resultsContainerRef) => { var { className, dataSource = [], emitSelectedItem, emitActiveTerm, highlightFirst, term, resultTemplate } = _a, props = __rest(_a, ["className", "dataSource", "emitSelectedItem", "emitActiveTerm", "highlightFirst", "term", "resultTemplate"]); const [currentIndex, setCurrentIndex] = useState(0); const [initialTerm, setInitialTerm] = useState(''); const results = generateResults(dataSource, currentIndex, setCurrentIndex, emitSelectedItem, term, setInitialTerm); useEffect(() => { // Check if current term exists in results and capture the index let termIndex; if (term) { termIndex = relativeIndex(results.findIndex((result) => result.label === term)); } // If highlightFirst is true and term does not exist in results // update termIndex to the first result if (highlightFirst && !termIndex) { // If first item is a title, skip to next item termIndex = results[0].value.listItemType === 'title' ? 2 : 1; } // Set term index if it exists along with initial term if (termIndex) { setCurrentIndex(termIndex); setInitialTerm(term ? term : results[termIndex - 1].label); } }, []); const iterativeIndexes = [0]; results.forEach(({ value: { listItemType } }, index) => { if (!listItemType || listItemType === 'item') { // Add one to index to allow input to be item 0 iterativeIndexes.push(relativeIndex(index)); } }); const traverse = (range, index, forward) => { // Get the max range; the minimum index of any array is 0 const max = range.length - 1; // Determine the actual index relative to all iterable values const indexRelative = range.indexOf(index); // Increment/decrement accordingly let nextIndex = forward ? indexRelative + 1 : indexRelative - 1; // For 'next' iteration, reset to 0 if at max if (forward && nextIndex > max) { nextIndex = 0; } // For 'prev' iteration, reset to max if at 0 if (!forward && nextIndex < 0) { nextIndex = max; } return nextIndex; }; const handleTraversal = (currentTerm = '', forward) => { // Check if there's a initialTerm if (!initialTerm || initialTerm === '') { // If there's no initialTerm, assign the current input value setInitialTerm(currentTerm); } // Determine the next index const nextIndex = traverse(iterativeIndexes, currentIndex, forward); // Assign that index locally setCurrentIndex(iterativeIndexes[nextIndex]); if (nextIndex === 0) { // Emit the user's initial term emitActiveTerm(initialTerm); setInitialTerm(''); } else { // Emit the active term, but subtract one bc the input is not part of the original data set emitActiveTerm(results[iterativeIndexes[nextIndex] - 1].label); } }; const isInRange = currentIndex >= 0 && currentIndex <= results.length; // If currentIndex is 0, it's referring to the input field const adjustedForInputIndex = currentIndex ? currentIndex - 1 : 0; // Check for a title item index and skip it const checkForTitleIndex = (index) => { const shouldBumpIndex = results[index].value.listItemType === 'title' && // Check if title is last result index < results.length; return shouldBumpIndex ? index + 1 : index; }; useImperativeHandle(resultsContainerRef, () => ({ setActiveIndex: (itemIndex) => { setCurrentIndex(highlightFirst ? relativeIndex(checkForTitleIndex(itemIndex)) : itemIndex); }, clearInitialTerm: () => { setInitialTerm(''); }, handleNext: (currentTerm) => { handleTraversal(currentTerm, true); }, updateTerm: (currentTerm) => { if (isInRange) { emitActiveTerm(results[checkForTitleIndex(adjustedForInputIndex)].label); } }, handlePrevious: (currentTerm) => { handleTraversal(currentTerm, false); }, selectActive: () => { if (isInRange) { emitSelectedItem(results[adjustedForInputIndex]); } }, })); return (React.createElement(StyledResultsContainer, Object.assign({ className: classNames('anchor-auto-complete-results-container', className) }, props), resultTemplate ? (React.createElement("div", { className: "auto-complete-results" }, results.map(({ label, value }, index) => React.createElement(resultTemplate, { label, value, term, currentIndex, index: relativeIndex(index), key: relativeIndex(index), })))) : (React.createElement(List, { items: results, className: "auto-complete-results" })))); }); //# sourceMappingURL=ResultsContainer.component.js.map