@primer/components
Version:
Primer react components
209 lines (183 loc) • 6.77 kB
JavaScript
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import React, { useRef, useState } from 'react';
import { omit } from '@styled-system/props';
import styled from 'styled-components';
import { FocusKeys } from './behaviors/focusZone';
import { useCombinedRefs } from './hooks/useCombinedRefs';
import { useFocusZone } from './hooks/useFocusZone';
import Token from './Token/Token';
import TextInput from './TextInput';
import { useProvidedRefOrCreate } from './hooks';
import UnstyledTextInput from './_UnstyledTextInput';
const InputWrapper = styled.div.withConfig({
displayName: "TextInputWithTokens__InputWrapper",
componentId: "sc-8z94t5-0"
})(["order:1;flex-grow:1;"]);
// using forwardRef is important so that other components (ex. Autocomplete) can use the ref
const TextInputWithTokensComponent = /*#__PURE__*/React.forwardRef(({
icon: IconComponent,
contrast,
className,
block,
disabled,
theme,
sx: sxProp,
tokens,
onTokenRemove,
tokenComponent: TokenComponent,
preventTokenWrapping,
tokenSizeVariant,
hideTokenRemoveButtons,
selectedTokenIdx,
setSelectedTokenIdx,
...rest
}, externalRef) => {
const ref = useProvidedRefOrCreate(externalRef);
const {
onFocus,
onKeyDown,
...inputPropsRest
} = omit(rest);
const handleTokenFocus = tokenIdx => () => {
setSelectedTokenIdx(tokenIdx);
};
const handleTokenBlur = () => {
setSelectedTokenIdx(undefined);
};
const handleTokenKeyUp = e => {
if (e.key === 'Escape') {
var _ref$current;
ref === null || ref === void 0 ? void 0 : (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.focus();
}
};
const handleInputFocus = e => {
onFocus && onFocus(e);
setSelectedTokenIdx(undefined);
};
const handleInputKeyDown = e => {
var _ref$current2;
if (onKeyDown) {
onKeyDown(e);
}
if (ref !== null && ref !== void 0 && (_ref$current2 = ref.current) !== null && _ref$current2 !== void 0 && _ref$current2.value) {
return;
}
const lastToken = tokens[tokens.length - 1];
if (e.key === 'Backspace' && lastToken) {
onTokenRemove(lastToken.id);
if (ref !== null && ref !== void 0 && ref.current) {
// TODO: eliminate the first hack by making changes to the Autocomplete component
//
// HACKS:
// 1. Directly setting `ref.current.value` instead of updating state because the autocomplete
// highlight behavior doesn't work correctly if we update the value with a setState action in onChange
// 2. Adding an extra space so that when I backspace, it doesn't delete the last letter
ref.current.value = `${lastToken.text} `;
} // HACK: for some reason we need to wait a tick for `.select()` to work
setTimeout(() => {
var _ref$current3;
ref === null || ref === void 0 ? void 0 : (_ref$current3 = ref.current) === null || _ref$current3 === void 0 ? void 0 : _ref$current3.select();
}, 1);
}
};
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(InputWrapper, {
key: "inputWrapper"
}, /*#__PURE__*/React.createElement(UnstyledTextInput, _extends({
ref: ref,
disabled: disabled,
onFocus: handleInputFocus,
onKeyDown: handleInputKeyDown,
type: "text",
sx: {
height: '100%'
}
}, inputPropsRest))), tokens !== null && tokens !== void 0 && tokens.length && TokenComponent ? tokens.map(({
id,
...tokenRest
}, i) => /*#__PURE__*/React.createElement(TokenComponent, _extends({
key: id,
onFocus: handleTokenFocus(i),
onBlur: handleTokenBlur,
onKeyUp: handleTokenKeyUp,
isSelected: selectedTokenIdx === i,
handleRemove: () => {
onTokenRemove(id);
},
hideRemoveButton: hideTokenRemoveButtons,
variant: tokenSizeVariant,
tabIndex: 0
}, tokenRest))) : null);
});
const TextInputWithTokens = /*#__PURE__*/React.forwardRef(({
tokens,
onTokenRemove,
sx: sxProp,
...props
}, ref) => {
const localInputRef = useRef(null);
const combinedInputRef = useCombinedRefs(localInputRef, ref);
const [selectedTokenIdx, setSelectedTokenIdx] = useState();
const {
containerRef
} = useFocusZone({
focusOutBehavior: 'wrap',
bindKeys: FocusKeys.ArrowHorizontal | FocusKeys.HomeAndEnd,
focusableElementFilter: element => {
return !element.getAttributeNames().includes('aria-hidden');
},
getNextFocusable: direction => {
var _containerRef$current;
if (!selectedTokenIdx && selectedTokenIdx !== 0) {
return undefined;
}
let nextIndex = selectedTokenIdx + 1; // "+ 1" accounts for the first element: the text input
if (direction === 'next') {
nextIndex += 1;
}
if (direction === 'previous') {
nextIndex -= 1;
}
if (nextIndex > tokens.length || nextIndex < 1) {
return combinedInputRef.current || undefined;
}
return containerRef === null || containerRef === void 0 ? void 0 : (_containerRef$current = containerRef.current) === null || _containerRef$current === void 0 ? void 0 : _containerRef$current.children[nextIndex];
}
}, [selectedTokenIdx]);
const handleTokenRemove = tokenId => {
onTokenRemove(tokenId);
if (selectedTokenIdx) {
var _containerRef$current2;
const nextElementToFocus = containerRef === null || containerRef === void 0 ? void 0 : (_containerRef$current2 = containerRef.current) === null || _containerRef$current2 === void 0 ? void 0 : _containerRef$current2.children[selectedTokenIdx];
nextElementToFocus.focus();
}
};
return /*#__PURE__*/React.createElement(TextInput, _extends({
ref: combinedInputRef,
wrapperRef: containerRef,
as: TextInputWithTokensComponent,
selectedTokenIdx: selectedTokenIdx,
setSelectedTokenIdx: setSelectedTokenIdx,
tokens: tokens,
onTokenRemove: handleTokenRemove,
sx: {
'alignItems': 'center',
'flexWrap': props.preventTokenWrapping ? 'nowrap' : 'wrap',
'gap': '0.25rem',
'> *': {
'flexShrink': 0
},
...(props.block ? {
display: 'flex',
width: '100%'
} : {}),
...sxProp
}
}, props));
});
TextInputWithTokens.defaultProps = {
tokenComponent: Token,
tokenSizeVariant: "xl",
hideTokenRemoveButtons: false
};
TextInputWithTokens.displayName = 'TextInputWithTokens';
export default TextInputWithTokens;