@workday/canvas-kit-labs-react
Version:
Canvas Kit Labs is an incubator for new and experimental components. Since we have a rather rigorous process for getting components in at a production level, it can be valuable to make them available earlier while we continuously iterate on the API/functi
347 lines (346 loc) • 15.4 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Combobox = exports.getTextFromElement = exports.getOptionId = exports.listBoxIdPart = void 0;
const react_1 = __importStar(require("react"));
const react_2 = require("@emotion/react");
const common_1 = require("@workday/canvas-kit-react/common");
const tokens_1 = require("@workday/canvas-kit-react/tokens");
const card_1 = require("@workday/canvas-kit-react/card");
const button_1 = require("@workday/canvas-kit-react/button");
const canvas_system_icons_web_1 = require("@workday/canvas-system-icons-web");
const lodash_flatten_1 = __importDefault(require("lodash.flatten"));
const AutocompleteList_1 = require("./AutocompleteList");
const Status_1 = require("./Status");
const Container = (0, common_1.styled)('div')({
display: 'inline-block',
}, ({ grow }) => ({
width: grow ? '100%' : 'auto',
}));
const InputContainer = (0, common_1.styled)('div')({
display: `flex`,
alignItems: `center`,
position: 'relative',
});
const fadeInKeyframes = (0, react_2.keyframes)({
'0%': {
opacity: 0,
},
'100%': {
opacity: 1,
},
});
const MenuContainer = (0, common_1.styled)(card_1.Card)({
position: 'absolute',
zIndex: 1,
left: 0,
top: '100%',
borderRadius: tokens_1.borderRadius.m,
background: tokens_1.commonColors.background,
border: `none`,
marginTop: `-${tokens_1.borderRadius.m}`,
width: '100%',
minWidth: 0,
animation: `${fadeInKeyframes} 200ms ease-out`,
maxHeight: 200,
overflow: 'hidden',
});
const ResetButton = (0, common_1.styled)(button_1.TertiaryButton, {
shouldForwardProp: (0, common_1.filterOutProps)(['shouldShow']),
})({
position: 'absolute',
margin: `auto ${tokens_1.space.xxxs}`,
top: 0,
bottom: 0,
right: 0,
padding: 0,
zIndex: 2,
transition: 'opacity 120ms',
}, ({ shouldShow }) => ({
visibility: shouldShow ? 'visible' : 'hidden',
opacity: shouldShow ? 1 : 0,
}));
exports.listBoxIdPart = `listbox`;
const optionIdPart = `option`;
const getOptionId = (baseId, index) => `${baseId}-${optionIdPart}-${index}`;
exports.getOptionId = getOptionId;
const getTextFromElement = (children) => {
let text = '';
react_1.default.Children.map(children, child => {
if (child == null || typeof child === 'boolean') {
text += '';
}
else if (typeof child === 'string' || typeof child === 'number') {
text += child.toString();
}
else if (Array.isArray(child)) {
text += (0, exports.getTextFromElement)(child);
}
else if ('props' in child) {
text += (0, exports.getTextFromElement)(child.props.children);
}
});
return text;
};
exports.getTextFromElement = getTextFromElement;
const buildStatusString = (listCount) => {
return `There ${listCount === 1 ? 'is' : 'are'} ${listCount} suggestion${listCount === 1 ? '' : 's'}.`;
};
const isValidSingleChild = (child) => {
return react_1.default.isValidElement(child) && react_1.default.Children.only(child);
};
const Combobox = ({ autocompleteItems, children, grow, initialValue, onChange, onFocus, onBlur, showClearButton, clearButtonVariant = undefined, clearButtonAriaLabel = `Reset Search Input`, labelId, getStatusText = buildStatusString, id, ...elemProps }) => {
const [isOpened, setIsOpened] = (0, react_1.useState)(false);
const [value, _setValue] = (0, react_1.useState)(''); // Don't call _setValue directly instead call setInputValue to make sure onChange fires correctly
const [showingAutocomplete, setShowingAutocomplete] = (0, react_1.useState)(false);
const [selectedAutocompleteIndex, setSelectedAutocompleteIndex] = (0, react_1.useState)(null);
const [interactiveAutocompleteItems, setInteractiveAutocompleteItems] = (0, react_1.useState)([]);
const [announcementText, setAnnouncementText] = (0, react_1.useState)('');
// Create a ref to the soon-to-be-created TextInput clone for internal use.
// Use useForkRef to combine it with the ref already assigned to the original
// TextInput (if it exists) to create a single callback ref which can be
// forwarded to the TextInput clone.
const inputRef = (0, react_1.useRef)(null);
// We need access to the original TextInput's ref _property_ (not prop) so we
// can combine it with the internal inputRef using useForkRef. ref isn't
// listed in the ReactElement interface, but it's there, so we cast children
// to satisfy TS.
const elementRef = (0, common_1.useForkRef)(children.ref, inputRef);
const comboboxRef = (0, react_1.useRef)(null);
const randomComponentId = (0, common_1.useUniqueId)();
const randomLabelId = (0, common_1.useUniqueId)();
const componentId = id || randomComponentId;
const formLabelId = labelId || randomLabelId;
const [showGroupText, setShowGroupText] = (0, react_1.useState)(false);
// We're using LayoutEffect here because of an issue with the Synthetic event system and typing a key
// after the listbox has been closed. Somehow the key is ignored unless we use `useLayoutEffect`
(0, react_1.useLayoutEffect)(() => {
const shouldShow = interactiveAutocompleteItems.length > 0 && isOpened;
setShowingAutocomplete(shouldShow);
if (shouldShow) {
setAnnouncementText(getStatusText(interactiveAutocompleteItems.length));
}
}, [getStatusText, interactiveAutocompleteItems, isOpened]);
// Used to set the position of the reset button and the padding direction inside the input container
const isRTL = (0, common_1.useIsRTL)();
const setInputValue = (0, react_1.useCallback)((newValue) => {
_setValue(newValue);
const inputDomElement = inputRef.current;
// Changing value prop programmatically doesn't fire an Synthetic event or trigger native onChange.
// We can not just update .value= in setState because React library overrides input value setter
// but we can call the function directly on the input as context.
// This will cause onChange events to fire no matter how value is updated.
if (inputDomElement) {
const nativeInputValue = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(inputDomElement), 'value');
if (nativeInputValue && nativeInputValue.set) {
nativeInputValue.set.call(inputDomElement, newValue);
}
let event;
if (typeof Event === 'function') {
// modern browsers
event = new Event('input', { bubbles: true });
}
else {
// IE 11
event = document.createEvent('Event');
event.initEvent('input', true, true);
}
inputDomElement.dispatchEvent(event);
}
}, [inputRef]);
(0, react_1.useEffect)(() => {
if (initialValue !== null && initialValue !== undefined) {
setInputValue(initialValue);
}
}, [initialValue, setInputValue]);
(0, react_1.useEffect)(() => {
const getInteractiveAutocompleteItems = () => {
if (autocompleteItems &&
autocompleteItems.length &&
autocompleteItems[0].hasOwnProperty('header')) {
return (0, lodash_flatten_1.default)(autocompleteItems.map(group => group.items));
}
return autocompleteItems || [];
};
setInteractiveAutocompleteItems(getInteractiveAutocompleteItems());
}, [autocompleteItems]);
const handleAutocompleteClick = (event, menuItemProps) => {
if (menuItemProps.isDisabled || menuItemProps['aria-disabled']) {
return;
}
setShowingAutocomplete(false);
setIsOpened(false);
setInputValue((0, exports.getTextFromElement)(menuItemProps.children));
if (menuItemProps.onClick) {
menuItemProps.onClick(event);
}
};
const focusInput = () => {
if (inputRef.current) {
inputRef.current.focus();
}
};
const handleClick = (event) => {
if (!showingAutocomplete) {
setShowingAutocomplete(true);
}
};
const handleFocus = (event) => {
setIsOpened(true);
if (onFocus) {
onFocus(event);
}
};
const handleBlur = (event) => {
if (comboboxRef.current) {
const target = event.relatedTarget;
if (target && comboboxRef.current.contains(target)) {
return;
}
}
setIsOpened(false);
if (onBlur) {
onBlur(event);
}
};
const resetSearchInput = () => {
setInputValue('');
focusInput();
};
const getGroupIndex = (itemIndex) => {
if (itemIndex != null &&
autocompleteItems &&
autocompleteItems.length &&
autocompleteItems[0].hasOwnProperty('header')) {
let count = 0;
return autocompleteItems.findIndex(groups => {
count += groups.items.length;
return count > itemIndex;
});
}
else {
return -1;
}
};
const handleKeyboardShortcuts = (event) => {
if (event.ctrlKey || event.altKey || event.metaKey || !interactiveAutocompleteItems.length) {
return;
}
const autoCompleteItemCount = interactiveAutocompleteItems.length;
const firstItem = 0;
const lastItem = autoCompleteItemCount - 1;
let nextIndex = null;
setIsOpened(true);
switch (event.key) {
case 'ArrowUp':
case 'Up': // IE/Edge specific value
const upIndex = selectedAutocompleteIndex !== null ? selectedAutocompleteIndex - 1 : lastItem;
nextIndex = upIndex < 0 ? lastItem : upIndex;
event.stopPropagation();
event.preventDefault();
break;
case 'ArrowDown':
case 'Down': // IE/Edge specific value
const downIndex = selectedAutocompleteIndex !== null ? selectedAutocompleteIndex + 1 : firstItem;
nextIndex = downIndex >= autoCompleteItemCount ? firstItem : downIndex;
event.stopPropagation();
event.preventDefault();
break;
case 'Escape':
case 'Esc': // IE/Edge specific value
resetSearchInput();
break;
case 'Enter':
if (selectedAutocompleteIndex != null) {
const item = interactiveAutocompleteItems[selectedAutocompleteIndex];
handleAutocompleteClick(event, item.props);
if (item.props.isDisabled || item.props['aria-disabled']) {
nextIndex = selectedAutocompleteIndex;
}
event.stopPropagation();
event.preventDefault();
}
break;
default:
}
const lastGroupIndex = getGroupIndex(selectedAutocompleteIndex);
const nextGroupIndex = getGroupIndex(nextIndex);
setShowGroupText(lastGroupIndex !== nextGroupIndex);
setSelectedAutocompleteIndex(nextIndex);
};
const handleSearchInputChange = (event) => {
if (onChange) {
onChange(event);
}
_setValue(event.target.value); // Calling set value directly only for on change event
};
const renderChildren = (inputElement) => {
let cssOverride = { ':focus': { zIndex: 2 } };
if (showClearButton) {
const paddingDirection = isRTL ? 'paddingLeft' : 'paddingRight';
cssOverride = {
...cssOverride,
[paddingDirection]: tokens_1.space.xl,
};
}
const newTextInputProps = {
type: 'text',
grow: grow,
value: value,
ref: elementRef,
'aria-autocomplete': 'list',
'aria-activedescendant': selectedAutocompleteIndex !== null
? (0, exports.getOptionId)(componentId, selectedAutocompleteIndex)
: undefined,
onClick: handleClick,
onChange: handleSearchInputChange,
onKeyDown: handleKeyboardShortcuts,
onFocus: handleFocus,
onBlur: handleBlur,
css: cssOverride,
role: 'combobox',
'aria-owns': showingAutocomplete ? `${componentId}-${exports.listBoxIdPart}` : undefined,
'aria-haspopup': true,
'aria-expanded': showingAutocomplete,
};
const cloneElement = (element, props) => (0, react_2.jsx)(element.type, {
...element.props,
...props,
});
return cloneElement(inputElement, { ...inputElement.props, ...newTextInputProps });
};
return (react_1.default.createElement(Container, { grow: grow, ...elemProps },
react_1.default.createElement(InputContainer, { ref: comboboxRef },
isValidSingleChild(children) && react_1.default.Children.map(children, renderChildren),
showClearButton && (react_1.default.createElement(ResetButton, { shouldShow: !!value, "aria-label": clearButtonAriaLabel, icon: canvas_system_icons_web_1.xSmallIcon, variant: clearButtonVariant, onClick: resetSearchInput, onBlur: handleBlur, size: "small", type: "button" })),
showingAutocomplete && autocompleteItems && (react_1.default.createElement(MenuContainer, { padding: tokens_1.space.zero, depth: 3 },
react_1.default.createElement(card_1.Card.Body, null,
react_1.default.createElement(AutocompleteList_1.AutocompleteList, { comboboxId: componentId, autocompleteItems: autocompleteItems, selectedIndex: selectedAutocompleteIndex, handleAutocompleteClick: handleAutocompleteClick, labelId: formLabelId, showGroupText: showGroupText }))))),
react_1.default.createElement(Status_1.Status, { announcementText: announcementText })));
};
exports.Combobox = Combobox;
;