@retailmenot/anchor
Version:
A React UI Library by RetailMeNot
216 lines (214 loc) • 9.22 kB
JavaScript
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