Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(component): add multiselect #200

Merged
merged 4 commits into from
Oct 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
jorgemoya marked this conversation as resolved.
Show resolved Hide resolved
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