Skip to content

Commit

Permalink
Merge 68ab3ef into 346c5b6
Browse files Browse the repository at this point in the history
  • Loading branch information
jonambas committed Apr 8, 2020
2 parents 346c5b6 + 68ab3ef commit 73e3423
Show file tree
Hide file tree
Showing 13 changed files with 401 additions and 445 deletions.
28 changes: 24 additions & 4 deletions packages/matchbox/src/components/ComboBox/ComboBox.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Box } from '../Box';

function ComboBox(props) {
const { children, style, rootRef, ...rest } = props;

function getChild(name, passedProps) {
return React.Children.map(children, child => {
if (!React.isValidElement(child)) {
return null;
}

if (child.type.name === name) {
return React.cloneElement(child, passedProps);
}
});
}

// Clones children in order to pass the menu into the textfield
// Textfield inserts the menu in the DOM directly after the input, before helptext and error
const menu = React.useMemo(() => getChild('ComboBoxMenu'), [children]);
const textfield = React.useMemo(() => getChild('ComboBoxTextField', { renderMenu: menu }), [
children,
]);

return (
<div ref={rootRef} style={{ position: 'relative', ...style }} {...rest}>
{children}
</div>
<Box position="relative" ref={rootRef} style={style} {...rest}>
{textfield}
</Box>
);
}

Expand All @@ -17,7 +37,7 @@ ComboBox.propTypes = {
/**
* Maps to Downshift's refKey set in getRootProps. refKey must be set to "rootRef"
*/
rootRef: PropTypes.func
rootRef: PropTypes.func,
};

export default ComboBox;
44 changes: 31 additions & 13 deletions packages/matchbox/src/components/ComboBox/ComboBoxMenu.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { tokens } from '@sparkpost/design-tokens';
import { maxHeight } from 'styled-system';
import { createPropTypes } from '@styled-system/prop-types';
import { ActionList } from '../ActionList';
import styles from './ComboBoxMenu.module.scss';
import { Box } from '../Box';
import PopoverContent from '../Popover/PopoverContent';

function ComboBoxMenu(props) {
const { items, menuRef, isOpen, maxHeight, ...rest } = props;

const listClasses = classnames(
styles.List,
isOpen && styles.open
);
const {
items,
menuRef,
isOpen,
maxHeight = '20rem',
emptyMessage = 'No Results',
...rest
} = props;

return (
<div {...rest} ref={menuRef} className={listClasses}>
<ActionList actions={items} maxHeight={maxHeight} />
</div>
<Box ref={menuRef} {...rest} position="relative" zIndex={tokens.zIndex_overlay}>
<PopoverContent open={isOpen} width="100%">
{items.length > 0 ? (
<ActionList actions={items} maxHeight={maxHeight} />
) : (
<Box p="200" color="gray.700">
{emptyMessage}
</Box>
)}
</PopoverContent>
</Box>
);
}

ComboBoxMenu.propTypes = {
/**
* String displayed when item length is 0. Defaults to "No Results"
*/
emptyMessage: PropTypes.string,
/**
* Array of items. see ActionList prop types.
*/
Expand All @@ -31,11 +48,12 @@ ComboBoxMenu.propTypes = {
/**
* Maps to Downshift's refKey set in getMenuProps. refKey must be set to "menuRef"
*/
menuRef: PropTypes.func
menuRef: PropTypes.func,
...createPropTypes(maxHeight.propNames),
};

ComboBoxMenu.defaultProps = {
items: []
items: [],
};

export default ComboBoxMenu;
31 changes: 0 additions & 31 deletions packages/matchbox/src/components/ComboBox/ComboBoxMenu.module.scss

This file was deleted.

152 changes: 99 additions & 53 deletions packages/matchbox/src/components/ComboBox/ComboBoxTextField.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,59 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Label } from '../Label';
import styled from 'styled-components';
import { margin } from 'styled-system';
import { createPropTypes } from '@styled-system/prop-types';
import { pick } from '@styled-system/props';
import { tokens } from '@sparkpost/design-tokens';
import { Box } from '../Box';
import { Error } from '../Error';
import { HelpText } from '../HelpText';
import { Inline } from '../Inline';
import { Label } from '../Label';
import { Tag } from '../Tag';
import { identity, noop } from '../../helpers/event';
import { onKey } from '../../helpers/keyEvents';
import classnames from 'classnames';
import styles from './ComboBoxTextField.module.scss';
import { omit } from '../../helpers/systemProps';
import useInputDescribedBy from '../../hooks/useInputDescribedBy';

const StyledWrapper = styled('div')`
${margin}
`;

const inputHeight = '2.5rem';

const StyledInputWrapper = styled(Box)`
display: flex;
flex-wrap: wrap;
background: ${props => (props.isDisabled ? tokens.color_gray_200 : tokens.color_white)};
border: ${props =>
props.hasError
? `${tokens.borderWidth_100} solid ${tokens.color_red_700}`
: `${tokens.borderWidth_100} solid ${tokens.color_gray_400}`};
border-radius: ${tokens.borderRadius_100};
min-height: ${inputHeight};
&:focus-within {
border: ${tokens.borderWidth_100} solid ${tokens.color_blue_700};
}
`;

const StyledInput = styled('input')`
display: inline-block;
flex: 1;
background: none;
border: none;
outline: none;
padding-left: ${tokens.spacing_300};
padding-right: ${tokens.spacing_300};
line-height: calc(${inputHeight} - 2px);
height: calc(${inputHeight} - 2px);
font-weight: ${tokens.fontWeight_normal};
color: ${tokens.color_gray_900};
&:disabled {
cursor: not-allowed;
}
`;

function ComboBoxTextField(props) {
const {
Expand All @@ -27,48 +74,22 @@ function ComboBoxTextField(props) {
placeholder,
readOnly,
removeItem,
renderMenu,
required,
style,
selectedItems,
value,
...rest
} = props;

const inputRef = React.useRef();
const inputProps = omit(rest, margin.propNames);
const systemProps = pick(rest);

const setWrapperClasses = classnames(
styles.Wrapper,
error && styles.Error,
disabled && styles.Disabled
);

const labelMarkup = (
<Label id={id} label={label}>
{error && errorInLabel && <Error className={styles.InlineError} wrapper='span' error={error} />}
</Label>
);

const helpMarkup = helpText
? <div className={styles.HelpText}>{helpText}</div>
: null;

const selectedMarkup = selectedItems.length
? selectedItems.map((item, i) => (
<Tag
key={i}
onRemove={!disabled
? () => removeItem(item)
: null
}
>
{itemToString(item)}
</Tag>
))
: null;

// Auto focuses the input
function handleClick() {
inputRef.current.focus();
}
const { describedBy, errorId, helpTextId } = useInputDescribedBy({
id,
hasHelpText: !!helpText,
hasError: !!error,
});

// Removes last item with a backspace and and empty input value
function handleKeyDown(e) {
Expand All @@ -84,14 +105,32 @@ function ComboBoxTextField(props) {
}

return (
<fieldset className={styles.TextField}>
{label && !labelHidden && labelMarkup}
<div className={setWrapperClasses} onClick={handleClick}>
{selectedMarkup}
<input
{...rest}
<StyledWrapper {...systemProps}>
<Label id={id} label={label} labelHidden={labelHidden}>
{required && (
<Box as="span" pr="200" aria-hidden="true">
*
</Box>
)}
{error && errorInLabel && (
<Box as={Error} id={errorId} wrapper="span" error={error} fontWeight="400" />
)}
</Label>
<StyledInputWrapper hasError={!!error} isDisabled={disabled}>
{selectedItems.length > 0 && (
<Box display="flex" placeholder="200" pl="200" pt="0.375rem">
<Inline space="100">
{selectedItems.map((item, i) => (
<Tag key={i} onRemove={!disabled ? () => removeItem(item) : null}>
{itemToString(item)}
</Tag>
))}
</Inline>
</Box>
)}
<StyledInput
{...describedBy}
autoFocus={autoFocus}
className={styles.Input}
disabled={disabled}
id={id}
name={name}
Expand All @@ -101,14 +140,18 @@ function ComboBoxTextField(props) {
onKeyDown={handleKeyDown}
placeholder={placeholder}
readOnly={readOnly}
ref={inputRef}
required={required}
style={style}
type="text"
value={value}
{...inputProps}
/>
</div>
{error && !errorInLabel && <Error error={error} />}
{helpMarkup}
</fieldset>
</StyledInputWrapper>
{/* Menu is rendered here so it is positioned correctly before error and helptext */}
{renderMenu && renderMenu}
{error && !errorInLabel && <Error id={errorId} error={error} />}
{helpText && <HelpText id={helpTextId}>{helpText}</HelpText>}
</StyledWrapper>
);
}

Expand All @@ -118,7 +161,7 @@ ComboBoxTextField.propTypes = {
error: PropTypes.string,
errorInLabel: PropTypes.bool,
helpText: PropTypes.node,
id: PropTypes.string,
id: PropTypes.string.isRequired,
itemToString: PropTypes.func,
label: PropTypes.string,
labelHidden: PropTypes.bool,
Expand All @@ -129,13 +172,16 @@ ComboBoxTextField.propTypes = {
placeholder: PropTypes.string,
readOnly: PropTypes.bool,
removeItem: PropTypes.func,
selectedItems: PropTypes.array
required: PropTypes.bool,
renderMenu: PropTypes.node,
selectedItems: PropTypes.array,
...createPropTypes(margin.propNames),
};

ComboBoxTextField.defaultProps = {
selectedItems: [],
itemToString: identity,
removeItem: noop
removeItem: noop,
};

export default ComboBoxTextField;

0 comments on commit 73e3423

Please sign in to comment.