Skip to content

Commit

Permalink
Feature: EVER-13451: Add Select component (#325)
Browse files Browse the repository at this point in the history
* feat: EVER-13451: Initial commit of Select and Select.Option components

* WIP: Keyboard handler for moving selected option focus with up arrow.

* feat: EVER-13451: Up and down arrow keys, validity with hidden native select, break some code into separate hooks

* feat: EVER-13451: Add tests for , make  a child, export from index and import into example app

* feat: EVER-13451: Update jsdom env to sixteen

* feat: EVER-13451: Restore non-clicky style to plain Dropdown.Menu.Item content, style stories a bit

* feat: EVER-13451: Update Dropdown.Menu.Item README

* chore: EVER-13451: Update more tests and snapshots
  • Loading branch information
pixelbandito authored Aug 26, 2021
1 parent b692909 commit 7bb39f4
Show file tree
Hide file tree
Showing 39 changed files with 4,923 additions and 3,092 deletions.
22 changes: 22 additions & 0 deletions example/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import {
Modal,
Kite,
Loader,
Select,
Option,
// IMPORT_INJECTOR
} from '@cision/rover-ui';

Expand Down Expand Up @@ -484,6 +486,26 @@ const App = () => {
</div>
</Section>

<Section title="Select">
<Select.WithRef defaultValue="Option 6" placeholder="Pick one" required>
<Option value={new Date().toISOString()}>
Current time - {new Date().toString()}
</Option>
<Option>Option 1</Option>
<Option>Option 2</Option>
<Option>Option 3</Option>
<Option>Option 4</Option>
<Option>Option 5</Option>
<Option>Option 6</Option>
<Option disabled>Disabled option</Option>
<Option>Option 8</Option>
<Option>Option 9</Option>
<Option>Option 10</Option>
<Option>Option 11 is very wide, possibly too wide.</Option>
</Select.WithRef>{' '}
(required)
</Section>

{/** USAGE_INJECTOR */}
</div>
);
Expand Down
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@
"pre-commit:lint": "yarn lint-staged",
"pre-commit:types": "yarn tsc",
"pre-commit:test": "yarn test",
"test": "cross-env CI=1 react-scripts test --env=jsdom",
"test:watch": "react-scripts test --env=jsdom",
"test:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
"test": "cross-env CI=1 react-scripts test --env jest-environment-jsdom-sixteen",
"test:watch": "react-scripts test --env jest-environment-jsdom-sixteen",
"test:debug": "react-scripts --inspect-brk test --runInBand --no-cache --env jest-environment-jsdom-sixteen",
"compile": "microbundle-crl --raw --format modern,cjs --no-compress --tsconfig tsconfig.build.json --css-modules 'rvr-[local]__[hash:base64:3]' && mv dist/index.css dist/rover-ui.css",
"build": "yarn compile",
"start": "yarn compile --watch",
Expand Down Expand Up @@ -104,7 +104,7 @@
"@typescript-eslint/parser": "^2.26.0",
"auto-changelog": "^2.0.0",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"babel-loader": "8.1.0",
"babel-plugin-external-helpers": "^6.22.0",
"babel-plugin-transform-es2017-object-entries": "^0.0.5",
"cross-env": "^7.0.2",
Expand Down Expand Up @@ -134,6 +134,7 @@
"husky": "^1.3.1",
"hygen": "^4.0.2",
"in-publish": "^2.0.1",
"jest-environment-jsdom-sixteen": "2.0.0",
"lint-staged": "^8.1.4",
"microbundle-crl": "^0.13.10",
"npm-run-all": "^4.1.5",
Expand All @@ -156,6 +157,7 @@
"dependencies": {
"@cision/react-container-query": "1.0.0-alpha.3",
"classnames": "^2.2.6",
"lodash": "^4.17.19"
"lodash": "^4.17.19",
"nanoid": "^3.1.23"
}
}
14 changes: 12 additions & 2 deletions src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ interface BaseButtonProps {
circle?: boolean;
className?: string;
darkMode?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
forwardedRef?: React.Ref<any>;
hollow?: boolean;
level?: TButtonLevel;
size?: TButtonSize;
Expand All @@ -42,19 +44,21 @@ interface BaseButtonProps {
}

// Button props
type ButtonElementProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
export type ButtonElementProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
BaseButtonProps & {
href?: undefined;
};

// Anchor props
type AnchorElementProps = React.AnchorHTMLAttributes<HTMLAnchorElement> &
export type AnchorElementProps = React.AnchorHTMLAttributes<HTMLAnchorElement> &
BaseButtonProps & {
href?: string;
};

type ButtonType = React.FC<ButtonElementProps | AnchorElementProps> & {
Addon: React.FC<AddonProps>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
WithRef: any;
};

// Guard to check if href exists in props
Expand Down Expand Up @@ -94,6 +98,7 @@ const Button: ButtonType = ({
circle = undefined,
className: passedClassName = '',
darkMode = false,
forwardedRef,
hollow = false,
level = 'secondary',
size = 'lg',
Expand Down Expand Up @@ -138,6 +143,7 @@ const Button: ButtonType = ({
<Context.Provider value={{ size }}>
<Tag
className={className}
ref={forwardedRef}
type={Tag === 'button' ? 'button' : undefined}
{...passedProps}
>
Expand All @@ -149,4 +155,8 @@ const Button: ButtonType = ({

Button.Addon = Addon;

Button.WithRef = React.forwardRef((props, ref) => (
<Button {...props} forwardedRef={ref} />
));

export default Button;
32 changes: 9 additions & 23 deletions src/components/Dropdown/Menu/Item/Item.module.css
Original file line number Diff line number Diff line change
@@ -1,36 +1,22 @@
.Item {
background: transparent none;
padding: var(--rvr-space-sm) var(--rvr-space-lg);
-webkit-appearance: none;
border: 0 none transparent;
transition: 100ms var(--rvr-easeInOutQuad) background-color, 100ms var(--rvr-easeInOutQuad) color;
text-align: left;
display: block;
box-sizing: border-box;
width: 100%;
font-family: var(--rvr-base-font-family);
font-size: var(--rvr-font-size-md);
line-height: var(--rvr-line-height-sm);
color: var(--rvr-gray-90);
}

.button {
cursor: pointer;
color: var(--rvr-gray-90);
border-radius: 0;
}

.button:hover,
.button:active {
.button:hover:not(.disabled),
.button:active:not(.disabled) {
background-color: var(--rvr-gray-10);
}

.link {
cursor: pointer;
.content {
cursor: default;
color: var(--rvr-gray-90);
text-decoration: none;
}

.link:hover,
.link:active {
background-color: var(--rvr-gray-10);
.content:hover,
.content:active {
background: none;
color: var(--rvr-gray-90);
}
32 changes: 18 additions & 14 deletions src/components/Dropdown/Menu/Item/Item.test.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,55 @@
import React from 'react';
import { shallow } from 'enzyme';
import { render, screen } from '@testing-library/react';

import Item from '.';

describe('Item', () => {
it('renders', () => {
shallow(<Item>Boom</Item>);
const { baseElement } = render(<Item>Boom</Item>);
expect(baseElement).toMatchSnapshot();
});

it('renders content', () => {
const wrapper = shallow(<Item>Boom</Item>);
expect(wrapper.text()).toEqual('Boom');
render(<Item>Boom</Item>);
expect(screen.getByText('Boom')).toBeTruthy();
});

describe("when there's an href", () => {
it('renders a link', () => {
const wrapper = shallow(<Item href="foo">Hi</Item>);
expect(wrapper.find('a').length).toEqual(1);
render(<Item href="foo">Hi</Item>);
const menuItem = screen.getByRole('link', {
name: 'Hi',
}) as HTMLAnchorElement;
expect(menuItem.href).toEqual('http://localhost/foo');
});

describe('and an onClick', () => {
it('still renders a link', () => {
const wrapper = shallow(
render(
<Item href="foo" onClick={() => {}}>
Hi
</Item>
);
expect(wrapper.find('a').length).toEqual(1);
expect(wrapper.find('button').length).toEqual(0);

expect(screen.getByRole('link', { name: 'Hi' })).toBeTruthy();
});
});
});

describe("when there's an onClick", () => {
const wrapper = shallow(<Item onClick={() => {}}>Hi</Item>);
expect(wrapper.find('button').length).toEqual(1);
render(<Item onClick={() => {}}>Hi</Item>);
expect(screen.getByRole('button', { name: 'Hi' })).toBeTruthy();
});

describe('when children is a React node', () => {
it('still renders', () => {
const wrapper = shallow(
const { baseElement } = render(
<Item>
<span>Span!</span>
</Item>
);
expect(wrapper.find('span').length).toEqual(1);
expect(wrapper.text()).toEqual('Span!');
expect(baseElement).toMatchSnapshot();
expect(screen.getByText('Span!')).toBeTruthy();
});
});
});
70 changes: 42 additions & 28 deletions src/components/Dropdown/Menu/Item/Item.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,53 @@
import React from 'react';
import classNames from 'classnames';

import Button from '../../../Button';

import type {
AnchorElementProps,
ButtonElementProps,
} from '../../../Button/Button';

import styles from './Item.module.css';

export type ButtonElementProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
interface DivItemProps extends React.AllHTMLAttributes<HTMLDivElement> {
disabled?: boolean;
forwardedRef?: React.Ref<HTMLDivElement>;
}

type AnchorElementProps = React.AnchorHTMLAttributes<HTMLAnchorElement>;
interface AnchorItemProps extends AnchorElementProps {
forwardedRef?: React.Ref<HTMLAnchorElement>;
}

type DivElementProps = React.AllHTMLAttributes<HTMLDivElement>;
interface ButtonItemProps extends ButtonElementProps {
forwardedRef?: React.Ref<HTMLButtonElement>;
}

export type ItemProps =
| ButtonElementProps
| AnchorElementProps
| DivElementProps;
export type ItemProps = ButtonItemProps | AnchorItemProps | DivItemProps;

// Guard to check if href exists in props
const hasHref = (props: ItemProps): props is AnchorElementProps =>
'href' in props && props.href !== undefined;

const Item: React.FC<ItemProps> = ({ className = '', ...passedProps }) => {
const Item: React.FC<ItemProps> = ({
className = '',
forwardedRef,
...passedProps
}) => {
/**
* If an `href` prop is passed, the rendered element automatically renders
* as a bare <a> element with default styles
*/
if (hasHref(passedProps as AnchorElementProps)) {
if (hasHref(passedProps) || passedProps.onClick) {
return (
// eslint-disable-next-line jsx-a11y/anchor-has-content
<a
className={classNames(styles.Item, styles.link, className)}
{...(passedProps as AnchorElementProps)}
/>
);
}

/**
* If an `onClick` prop is passed, the rendered element automatically renders
* as a bare <button> element with some default styles
*/
if (!hasHref(passedProps as AnchorElementProps) && passedProps.onClick) {
return (
<button
className={classNames(styles.Item, styles.button, className)}
type="button"
<Button.WithRef
className={classNames(styles.Item, styles.button, className, {
[styles.disabled]: passedProps.disabled,
})}
level="text"
ref={forwardedRef}
size="md"
{...(passedProps as ButtonElementProps)}
/>
);
Expand All @@ -52,11 +58,19 @@ const Item: React.FC<ItemProps> = ({ className = '', ...passedProps }) => {
* as a <div> element with some default styles
*/
return (
<div
<Button.WithRef
className={classNames(styles.Item, styles.content, className)}
{...(passedProps as DivElementProps)}
level="text"
ref={forwardedRef}
size="md"
tag="div"
{...(passedProps as ButtonElementProps)}
/>
);
};

export default Item;
// Temporary
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default React.forwardRef<any, any>((props, ref) => (
<Item {...props} forwardedRef={ref} />
));
2 changes: 1 addition & 1 deletion src/components/Dropdown/Menu/Item/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

This dropdown menu item should _always_ be a direct child of a `Dropdown.Menu` component.

It adds consistent padding and wraps menu items in either a `<span>` or `<button>` depending on whether or not there's an `onClick` property provided.
It adds consistent padding and wraps menu items in either a `<button>` that has hover/active styles if an `onClick` or `href` property is present.
39 changes: 39 additions & 0 deletions src/components/Dropdown/Menu/Item/__snapshots__/Item.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Item renders 1`] = `
<body>
<div>
<button
class="Item button Button text md"
type="button"
>
<span>
Hi
</span>
</button>
</div>
<div>
<div
class="Item content Button text md"
>
<span>
Boom
</span>
</div>
</div>
</body>
`;

exports[`Item when children is a React node still renders 1`] = `
<body>
<div>
<div
class="Item content Button text md"
>
<span>
Span!
</span>
</div>
</div>
</body>
`;
Loading

0 comments on commit 7bb39f4

Please sign in to comment.