Skip to content

Commit

Permalink
feat(component): add multiselect (#200)
Browse files Browse the repository at this point in the history
  • Loading branch information
jorgemoya authored Oct 3, 2019
1 parent bb07411 commit 02acf73
Show file tree
Hide file tree
Showing 22 changed files with 853 additions and 390 deletions.
4 changes: 2 additions & 2 deletions packages/big-design/src/components/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ class RawCheckbox extends React.PureComponent<CheckboxProps & PrivateProps> {

private renderLabel() {
const htmlFor = this.getInputId();
const { label, theme } = this.props;
const { disabled, label, theme } = this.props;

if (typeof label === 'string') {
return (
<StyledLabel htmlFor={htmlFor} id={this.labelUniqueId} theme={theme}>
<StyledLabel disabled={disabled} htmlFor={htmlFor} aria-hidden={disabled} id={this.labelUniqueId} theme={theme}>
{label}
</StyledLabel>
);
Expand Down
8 changes: 8 additions & 0 deletions packages/big-design/src/components/Checkbox/spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,11 @@ test('theme prop overrides default theme', () => {

expect(container.querySelector('label')).toHaveStyle(`background-color: red`);
});

test('displays text greyed out when disabled', () => {
const { container } = render(<Checkbox disabled label="Checked" className="test" />);

const labels = container.querySelectorAll('label');

expect(labels[1]).toHaveStyle('color: #B4BAD1');
});
16 changes: 13 additions & 3 deletions packages/big-design/src/components/Checkbox/styled.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { theme as defaultTheme } from '@bigcommerce/big-design-theme';
import { hideVisually } from 'polished';
import styled, { DefaultTheme, StyledComponent } from 'styled-components';
import styled, { css, DefaultTheme, StyledComponent } from 'styled-components';

import { StyleableText } from '../Typography/private';

Expand All @@ -9,6 +9,10 @@ interface StyledCheckboxProps {
isIndeterminate?: boolean;
}

export interface StyledLabelProps {
disabled?: boolean;
}

export const CheckboxContainer = styled.div`
align-items: center;
display: flex;
Expand Down Expand Up @@ -45,9 +49,15 @@ export const StyledCheckbox = styled.label<StyledCheckboxProps>`

export const StyledLabel = styled(StyleableText).attrs({
as: 'label',
})<React.LabelHTMLAttributes<HTMLLabelElement>>`
})<React.LabelHTMLAttributes<HTMLLabelElement> & StyledLabelProps>`
margin-left: ${({ theme }) => theme.spacing.medium};
` as StyledComponent<'label', DefaultTheme>;
${({ disabled, theme }) =>
disabled &&
css`
color: ${theme.colors.secondary40};
`}
` as StyledComponent<'label', DefaultTheme, StyledLabelProps>;

StyledCheckbox.defaultProps = { theme: defaultTheme };
StyledLabel.defaultProps = { theme: defaultTheme };
8 changes: 6 additions & 2 deletions packages/big-design/src/components/Chip/Chip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@ import { CloseIcon } from '@bigcommerce/big-design-icons';
import { ThemeInterface } from '@bigcommerce/big-design-theme';
import React, { memo } from 'react';

import { MarginProps } from '../../mixins';
import { Text } from '../Typography';

import { StyledChip, StyledCloseButton } from './styled';

export interface ChipProps {
export interface ChipProps extends MarginProps {
theme?: ThemeInterface;
onDelete?(): void;
}

export const Chip: React.FC<ChipProps> = memo(({ children, onDelete, theme }) => {
export const Chip: React.FC<ChipProps> = memo(({ children, onDelete, theme, ...rest }) => {
const label = typeof children === 'string' ? children : null;
const ariaLabel = label ? { 'aria-label': `Remove ${label}` } : {};

const handleOnDelete = (event: React.SyntheticEvent<HTMLButtonElement, MouseEvent>) => {
event.preventDefault();
Expand All @@ -25,6 +27,7 @@ export const Chip: React.FC<ChipProps> = memo(({ children, onDelete, theme }) =>
const renderDeleteButton = () =>
onDelete && (
<StyledCloseButton
{...ariaLabel}
variant="subtle"
onClick={handleOnDelete}
iconOnly={<CloseIcon size="medium" title="Delete" theme={theme} />}
Expand All @@ -40,6 +43,7 @@ export const Chip: React.FC<ChipProps> = memo(({ children, onDelete, theme }) =>
margin="xxSmall"
borderRadius="normal"
theme={theme}
{...rest}
>
<Text margin="none" marginRight="xxSmall" theme={theme}>
{label}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ exports[`renders with close button if onRemove is present 1`] = `
Test
</p>
<button
aria-label="Remove Test"
class="c3 c4"
role="button"
tabindex="0"
Expand Down
15 changes: 9 additions & 6 deletions packages/big-design/src/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Placement } from 'popper.js';
import React, { AllHTMLAttributes, RefObject } from 'react';
import React, { RefObject } from 'react';
import { Manager, Reference, RefHandler } from 'react-popper';
import scrollIntoView from 'scroll-into-view-if-needed';

Expand All @@ -16,7 +16,7 @@ interface Props {
maxHeight?: number;
placement?: Placement;
trigger: React.ReactElement;
onItemClick?(value: AllHTMLAttributes<HTMLElement>['value']): void;
onItemClick?(value: string | number | Array<string | number>): void;
}

export type DropdownProps = Props & React.HTMLAttributes<HTMLUListElement>;
Expand Down Expand Up @@ -56,7 +56,7 @@ export class Dropdown extends React.PureComponent<DropdownProps, DropdownState>
maxHeight={maxHeight}
onKeyDown={this.handleOnDropdownKeyDown}
placement={placement}
role="menu"
role="listbox"
{...aria}
{...rest}
>
Expand All @@ -80,7 +80,10 @@ export class Dropdown extends React.PureComponent<DropdownProps, DropdownState>
switch (child.type) {
case ListItem:
const id = this.getItemId(child, index);
this.listItemsRefs.push(ref);

if (!child.props.disabled) {
this.listItemsRefs.push(ref);
}

return React.cloneElement(child, {
'data-highlighted': highlightedItem && id === highlightedItem.id,
Expand All @@ -89,7 +92,7 @@ export class Dropdown extends React.PureComponent<DropdownProps, DropdownState>
onFocus: this.handleOnItemFocus,
onMouseOver: this.handleOnItemMouseOver,
ref,
role: 'menuitem',
role: 'option',
}) as React.LiHTMLAttributes<HTMLLIElement>;
default:
return;
Expand All @@ -100,7 +103,7 @@ export class Dropdown extends React.PureComponent<DropdownProps, DropdownState>
private renderTrigger(ref: RefHandler) {
const { trigger } = this.props;

const aria = this.state.isOpen ? { 'aria-expanded': true, 'aria-owns': this.getDropdownId() } : {};
const aria = this.state.isOpen ? { 'aria-expanded': true } : {};

return (
React.isValidElement(trigger) &&
Expand Down
57 changes: 28 additions & 29 deletions packages/big-design/src/components/Dropdown/spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,31 +36,30 @@ test('dropdown trigger has aria-haspopup', () => {
expect(trigger.getAttribute('aria-haspopup')).toBe('true');
});

test('dropdown trigger has aria-expanded and aria-owns when dropdown menu is open', () => {
test('dropdown trigger has aria-expanded when dropdown menu is open', () => {
const { getByRole } = render(DropdownMock);
const trigger = getByRole('button');

fireEvent.click(trigger);

expect(trigger.getAttribute('aria-expanded')).toBe('true');
expect(trigger.getAttribute('aria-owns')).toBe(getByRole('menu').id);
});

test('renders the dropdown menu closed', () => {
const { queryByRole } = render(DropdownMock);

expect(queryByRole('menu')).not.toBeVisible();
expect(queryByRole('listbox')).not.toBeVisible();
});

test('opens/closes dropdown menu when trigger is clicked', () => {
const { getByRole, queryByRole } = render(DropdownMock);
const trigger = getByRole('button');

fireEvent.click(trigger);
expect(queryByRole('menu')).not.toHaveStyle('height: 1px');
expect(queryByRole('listbox')).not.toHaveStyle('height: 1px');

fireEvent.click(trigger);
expect(queryByRole('menu')).toHaveStyle('height: 1px');
expect(queryByRole('listbox')).toHaveStyle('height: 1px');
});

test('dropdown menu has aria-labelledby', () => {
Expand All @@ -69,7 +68,7 @@ test('dropdown menu has aria-labelledby', () => {

fireEvent.click(trigger);

expect(getByRole('menu').getAttribute('aria-labelledby')).toBe(trigger.id);
expect(getByRole('listbox').getAttribute('aria-labelledby')).toBe(trigger.id);
});

test('dropdown menu has aria-activedescendant', () => {
Expand All @@ -78,22 +77,22 @@ test('dropdown menu has aria-activedescendant', () => {

fireEvent.click(trigger);

const options = getAllByRole('menuitem');
const options = getAllByRole('option');

expect(getByRole('menu').getAttribute('aria-activedescendant')).toBe(options[0].id);
expect(getByRole('listbox').getAttribute('aria-activedescendant')).toBe(options[0].id);
});

test('dropdown menu should have 4 dropdown items', () => {
const { getAllByRole } = render(DropdownMock);

const options = getAllByRole('menuitem');
const options = getAllByRole('option');
expect(options.length).toBe(4);
});

test('dropdown items should have values', () => {
const { getAllByRole } = render(DropdownMock);

const options = getAllByRole('menuitem');
const options = getAllByRole('option');
options.forEach((option, index) => expect(option.getAttribute('data-value')).toBe(`${index}`));
});

Expand All @@ -103,7 +102,7 @@ test('first dropdown item should be selected when dropdown is opened', () => {

fireEvent.click(trigger);

const option = getAllByRole('menuitem')[0];
const option = getAllByRole('option')[0];

expect(option.dataset.highlighted).toBe('true');
});
Expand All @@ -114,8 +113,8 @@ test('up/down arrows should change dropdown item selection', () => {

fireEvent.click(trigger);

const menu = getByRole('menu');
const options = getAllByRole('menuitem');
const menu = getByRole('listbox');
const options = getAllByRole('option');

fireEvent.keyDown(menu, { key: 'ArrowDown' });
expect(options[1].dataset.highlighted).toBe('true');
Expand All @@ -132,21 +131,21 @@ test('esc should close menu', () => {
const trigger = getByRole('button');

fireEvent.click(trigger);
expect(queryByRole('menu')).not.toHaveStyle('height: 1px');
expect(queryByRole('listbox')).not.toHaveStyle('height: 1px');

fireEvent.keyDown(getByRole('menu'), { key: 'Escape' });
expect(queryByRole('menu')).toHaveStyle('height: 1px');
fireEvent.keyDown(getByRole('listbox'), { key: 'Escape' });
expect(queryByRole('listbox')).toHaveStyle('height: 1px');
});

test('tab should close menu', () => {
const { getByRole, queryByRole } = render(DropdownMock);
const trigger = getByRole('button');

fireEvent.click(trigger);
expect(queryByRole('menu')).not.toHaveStyle('height: 1px');
expect(queryByRole('listbox')).not.toHaveStyle('height: 1px');

fireEvent.keyDown(getByRole('menu'), { key: 'Tab' });
expect(queryByRole('menu')).toHaveStyle('height: 1px');
fireEvent.keyDown(getByRole('listbox'), { key: 'Tab' });
expect(queryByRole('listbox')).toHaveStyle('height: 1px');
});

test('home should select first dropdown item', () => {
Expand All @@ -155,8 +154,8 @@ test('home should select first dropdown item', () => {

fireEvent.click(trigger);

const menu = getByRole('menu');
const options = getAllByRole('menuitem');
const menu = getByRole('listbox');
const options = getAllByRole('option');

fireEvent.keyDown(menu, { key: 'ArrowDown' });
fireEvent.keyDown(menu, { key: 'ArrowDown' });
Expand All @@ -174,8 +173,8 @@ test('end should select last dropdown item', () => {

fireEvent.click(trigger);

const menu = getByRole('menu');
const options = getAllByRole('menuitem');
const menu = getByRole('listbox');
const options = getAllByRole('option');

expect(options[0].dataset.highlighted).toBe('true');

Expand All @@ -196,8 +195,8 @@ test('enter should trigger onItemClick', () => {

fireEvent.click(trigger);

const menu = getByRole('menu');
const options = getAllByRole('menuitem');
const menu = getByRole('listbox');
const options = getAllByRole('option');

fireEvent.keyDown(menu, { key: 'ArrowDown' });
expect(options[1].dataset.highlighted).toBe('true');
Expand All @@ -218,8 +217,8 @@ test('space should trigger onItemClick', () => {

fireEvent.click(trigger);

const menu = getByRole('menu');
const options = getAllByRole('menuitem');
const menu = getByRole('listbox');
const options = getAllByRole('option');

fireEvent.keyDown(menu, { key: 'ArrowDown' });
expect(options[1].dataset.highlighted).toBe('true');
Expand All @@ -240,7 +239,7 @@ test('clicking on dropdown items should trigger onItemClick', () => {

fireEvent.click(trigger);

const options = getAllByRole('menuitem');
const options = getAllByRole('option');

fireEvent.mouseOver(options[1]);
fireEvent.click(options[1]);
Expand All @@ -253,7 +252,7 @@ test('dropdown items should be highlighted when moused over', () => {

fireEvent.click(trigger);

const option = getAllByRole('menuitem')[0];
const option = getAllByRole('option')[0];

fireEvent.mouseOver(option);
expect(option.dataset.highlighted).toBe('true');
Expand Down
Loading

0 comments on commit 02acf73

Please sign in to comment.