Skip to content

Commit d542ff4

Browse files
author
Michael Mitchell
committed
Create Selectable component
1 parent 1729104 commit d542ff4

23 files changed

+2856
-50
lines changed

Diff for: .changeset/few-bugs-promise.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@leafygreen-ui/select': major
3+
---
4+
5+
Initial release of Select component

Diff for: README.md

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ A set of CSS styles and React components built with design in mind.
3131
- [Portal](https://github.com/mongodb/leafygreen-ui/tree/master/packages/portal)
3232
- [Radio Box Group](https://github.com/mongodb/leafygreen-ui/tree/master/packages/radio-box-group)
3333
- [Radio Group](https://github.com/mongodb/leafygreen-ui/tree/master/packages/radio-group)
34+
- [Select](https://github.com/mongodb/leafygreen-ui/tree/master/packages/select)
3435
- [Side Nav](https://github.com/mongodb/leafygreen-ui/tree/master/packages/side-nav)
3536
- [Stepper](https://github.com/mongodb/leafygreen-ui/tree/master/packages/stepper)
3637
- [Syntax](https://github.com/mongodb/leafygreen-ui/tree/master/packages/syntax)

Diff for: babel.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ module.exports = function (api) {
2121
'@babel/plugin-proposal-export-default-from',
2222
'@babel/plugin-proposal-optional-chaining',
2323
'@babel/plugin-proposal-nullish-coalescing-operator',
24+
'@babel/plugin-proposal-logical-assignment-operators',
2425
'emotion',
2526
];
2627

Diff for: build.tsconfig.json

+3
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@
8080
{
8181
"path": "./packages/radio-group"
8282
},
83+
{
84+
"path": "./packages/select"
85+
},
8386
{
8487
"path": "./packages/side-nav"
8588
},

Diff for: package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@babel/plugin-proposal-class-properties": "7.8.3",
5353
"@babel/plugin-proposal-export-default-from": "7.8.3",
5454
"@babel/plugin-proposal-nullish-coalescing-operator": "7.8.3",
55+
"@babel/plugin-proposal-logical-assignment-operators": "7.12.1",
5556
"@babel/plugin-proposal-object-rest-spread": "7.9.5",
5657
"@babel/plugin-proposal-optional-chaining": "7.9.0",
5758
"@babel/plugin-transform-react-jsx": "^7.9.4",
@@ -63,7 +64,7 @@
6364
"@changesets/cli": "^2.6.2",
6465
"@emotion/core": "^10.0.28",
6566
"@emotion/styled": "^10.0.27",
66-
"@leafygreen-ui/lib": "^5.0.0",
67+
"@leafygreen-ui/lib": "^6.0.1",
6768
"@leafygreen-ui/testing-lib": "*",
6869
"@rollup/plugin-babel": "^5.2.0",
6970
"@rollup/plugin-commonjs": "^15.0.0",
@@ -108,7 +109,7 @@
108109
"eslint-config-prettier": "6.10.1",
109110
"eslint-plugin-import": "2.20.2",
110111
"eslint-plugin-jest": "23.8.2",
111-
"eslint-plugin-jsx-a11y": "^6.2.3",
112+
"eslint-plugin-jsx-a11y": "^6.3.1",
112113
"eslint-plugin-react": "7.19.0",
113114
"eslint-plugin-react-hooks": "^3.0.0",
114115
"file-loader": "^6.0.0",

Diff for: packages/select/README.md

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Select
2+
3+
![npm (scoped)](https://img.shields.io/npm/v/@leafygreen-ui/select.svg)
4+
5+
#### [View on Storybook](https://mongodb.github.io/leafygreen-ui/?path=/story/select--default)
6+
7+
## Installation
8+
9+
### Yarn
10+
11+
```shell
12+
yarn add @leafygreen-ui/select
13+
```
14+
15+
### NPM
16+
17+
```shell
18+
npm install @leafygreen-ui/select
19+
```
20+
21+
## Example
22+
23+
```js
24+
import Select, { Option, OptionGroup, Size } from '@leafygreen-ui/select';
25+
26+
<Select
27+
label="Label"
28+
description="Description"
29+
placeholder="Placeholder"
30+
name="Name"
31+
size={Size.Normal}
32+
defaultValue="cat"
33+
>
34+
<Option value="dog">Dog</Option>
35+
<Option value="cat">Cat</Option>
36+
<OptionGroup label="Less common">
37+
<Option value="hamster">Hamster</Option>
38+
<Option value="parrot">Parrot</Option>
39+
</OptionGroup>
40+
</Select>;
41+
```
42+
43+
**Output HTML**
44+
45+
```html
46+
<div>
47+
<label id="select-39-label" class="leafygreen-ui-xzhurf">Label</label>
48+
<div id="select-39-description" class="leafygreen-ui-1bf65pq">
49+
Description
50+
</div>
51+
<div
52+
role="combobox"
53+
aria-labelledby="select-39-label"
54+
aria-controls="select-39-menu"
55+
aria-expanded="false"
56+
aria-describedby="select-39-description"
57+
tabindex="0"
58+
class="leafygreen-ui-5kxwjh"
59+
>
60+
<div class="leafygreen-ui-cdhhty">
61+
<input type="hidden" name="Name" value="cat" /><span
62+
class="leafygreen-ui-1ks3bq2"
63+
>Cat</span
64+
><svg
65+
class="leafygreen-ui-msi0rg"
66+
height="16"
67+
width="16"
68+
viewBox="0 0 16 16"
69+
role="img"
70+
aria-labelledby="CaretDown-39"
71+
>
72+
<title id="CaretDown-39">Caret Down Icon</title>
73+
<g
74+
id="CaretDown-Copy"
75+
stroke="none"
76+
stroke-width="1"
77+
fill="none"
78+
fill-rule="evenodd"
79+
>
80+
<path
81+
d="M4.67285687,6 L11.3271431,6 C11.9254697,6 12.224633,6.775217 11.8024493,7.22717749 L8.47530616,10.7889853 C8.21248981,11.0703382 7.78751019,11.0703382 7.52748976,10.7889853 L4.19755071,7.22717749 C3.77536701,6.775217 4.07453029,6 4.67285687,6 Z"
82+
id="Path"
83+
fill="currentColor"
84+
></path>
85+
</g>
86+
</svg>
87+
</div>
88+
<div></div>
89+
</div>
90+
</div>
91+
```
92+
93+
## Select Properties
94+
95+
| Prop | Type | Description | Default |
96+
| -------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ---------- |
97+
| `children` | `node` | `<Option />` and `<OptionGroup />` elements. | |
98+
| `className` | `string` | Adds a className to the outermost element. | |
99+
| `darkMode` | `boolean` | Determines whether or not the component will appear in dark mode. | `false` |
100+
| `size` | `'xsmall'` \| `'small'` \| `'normal'` \| `'large'` | Sets the size of the component's elements. | `'normal'` |
101+
| `id` | `string` | id associated with the Select component. | |
102+
| `label` | `string` | Text shown in bold above the input element. | |
103+
| `description` | `string` | Text that gives more detail about the requirements for the input. | |
104+
| `placeholder` | `string` | The placeholder text shown in the input element when an option is not selected. | `'Select'` |
105+
| `disabled` | `boolean` | Disables the component from being edited. | `false` |
106+
| `value` | `string` | Sets the `<Option />` that will appear selected and makes the component a controlled component. | `''` |
107+
| `defaultValue` | `string` | Sets the `<Option />` that will appear selected on page load when the component is uncontrolled. | `''` |
108+
| `onChange` | `function` | A function that gets called when the selected value changes. Receives the value string as the first argument. | `() => {}` |
109+
| `readOnly` | `boolean` | Disables the console warning when the component is controlled and no `onChange` prop is provided. | `false` |
110+
111+
## Option
112+
113+
| Prop | Type | Description | Default |
114+
| ----------- | -------------------- | ----------------------------------------------------------------------------------------------------- | --------------------------- |
115+
| `children` | `node` | Content to appear inside of the component. | |
116+
| `className` | `string` | Adds a className to the outermost element. | |
117+
| `glyph` | `React.ReactElement` | Icon to display next to the option text. | |
118+
| `value` | `string` | Corresponds to the value passed into the `onChange` prop of `<Select />` when the option is selected. | text contents of `children` |
119+
| `disabled` | `boolean` | Prevents the option from being selectable. | `false` |
120+
121+
## OptionGroup
122+
123+
| Prop | Type | Description | Default |
124+
| ----------- | --------- | --------------------------------------------------------- | ------- |
125+
| `children` | `node` | `<Option />` elements | |
126+
| `className` | `string` | Adds a className to the outermost element. | |
127+
| `label` | `string` | Text shown above the group's options | |
128+
| `disabled` | `boolean` | Prevents all the contained options from being selectable. | `false` |

Diff for: packages/select/package.json

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "@leafygreen-ui/select",
3+
"version": "0.9.0",
4+
"description": "leafyGreen UI Kit Select",
5+
"main": "./dist/index.js",
6+
"module": "./dist/esm/index.js",
7+
"types": "./dist/index.d.ts",
8+
"typesVersions": {
9+
"<3.9": {
10+
"*": [
11+
"ts3.4/*"
12+
]
13+
}
14+
},
15+
"scripts": {
16+
"build": "../../node_modules/.bin/rollup --config ../../rollup.config.js"
17+
},
18+
"license": "Apache-2.0",
19+
"publishConfig": {
20+
"access": "public"
21+
},
22+
"dependencies": {
23+
"@leafygreen-ui/emotion": "^3.0.1",
24+
"@leafygreen-ui/hooks": "^5.0.1",
25+
"@leafygreen-ui/icon": "^7.0.1",
26+
"@leafygreen-ui/lib": "^6.0.1",
27+
"@leafygreen-ui/palette": "^3.0.1",
28+
"@leafygreen-ui/tokens": "0.5.0",
29+
"@types/react-is": "^16.7.1",
30+
"polished": "^4.0.3",
31+
"react-is": "^16.13.1"
32+
},
33+
"peerDependencies": {
34+
"@leafygreen-ui/leafygreen-provider": "^2.0.1"
35+
}
36+
}

Diff for: packages/select/src/ListMenu.tsx

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import React, { useCallback, useContext } from 'react';
2+
import { cx, css } from '@leafygreen-ui/emotion';
3+
import { useViewportSize } from '@leafygreen-ui/hooks';
4+
import { breakpoints } from '@leafygreen-ui/tokens';
5+
import SelectContext from './SelectContext';
6+
import { colorSets, mobileSizeSet, sizeSets } from './styleSets';
7+
8+
const menuStyle = css`
9+
position: relative;
10+
top: 5px;
11+
12+
width: 100%;
13+
border-radius: 3px;
14+
15+
line-height: 16px;
16+
list-style: none;
17+
margin: 0;
18+
padding: 0;
19+
overflow: auto scroll;
20+
`;
21+
22+
interface ListMenuProps {
23+
children: React.ReactNode;
24+
id: string;
25+
refElement: React.MutableRefObject<HTMLUListElement | null>;
26+
onClose: () => void;
27+
onSelectFocusedOption: () => void;
28+
onFocusPreviousOption: () => void;
29+
onFocusNextOption: () => void;
30+
}
31+
32+
export default function ListMenu({
33+
children,
34+
id,
35+
refElement,
36+
onClose,
37+
onFocusPreviousOption,
38+
onFocusNextOption,
39+
onSelectFocusedOption,
40+
}: ListMenuProps) {
41+
const { mode, size } = useContext(SelectContext);
42+
43+
const colorSet = colorSets[mode];
44+
const sizeSet = sizeSets[size];
45+
46+
const onKeyDown = useCallback(
47+
(event: React.KeyboardEvent) => {
48+
// No support for modifiers yet
49+
/* istanbul ignore if */
50+
if (event.ctrlKey || event.shiftKey || event.altKey) {
51+
return;
52+
}
53+
54+
let bubble = false;
55+
56+
switch (event.keyCode) {
57+
case 9: // Tab
58+
case 13: // Enter
59+
onSelectFocusedOption();
60+
break;
61+
case 27: // Escape
62+
onClose();
63+
break;
64+
case 38: // ArrowUp
65+
onFocusPreviousOption();
66+
break;
67+
case 40: // ArrowDown
68+
onFocusNextOption();
69+
break;
70+
/* istanbul ignore next */
71+
default:
72+
bubble = true;
73+
}
74+
75+
/* istanbul ignore else */
76+
if (!bubble) {
77+
event.preventDefault();
78+
event.stopPropagation();
79+
}
80+
},
81+
[onClose, onFocusNextOption, onFocusPreviousOption, onSelectFocusedOption],
82+
);
83+
84+
const viewportSize = useViewportSize();
85+
86+
const maxHeight =
87+
viewportSize === null || refElement.current === null
88+
? 0
89+
: viewportSize.height -
90+
refElement.current.getBoundingClientRect().top -
91+
10;
92+
93+
return (
94+
<ul
95+
role="listbox"
96+
ref={refElement}
97+
tabIndex={-1}
98+
onKeyDown={onKeyDown}
99+
className={cx(
100+
menuStyle,
101+
css`
102+
font-size: ${sizeSet.option.text}px;
103+
max-height: ${maxHeight}px;
104+
background-color: ${colorSet.option.background.base};
105+
box-shadow: 0 3px 7px 0 ${colorSet.menu.shadow};
106+
107+
@media only screen and (max-width: ${breakpoints.Desktop}px) {
108+
font-size: ${mobileSizeSet.option.text}px;
109+
}
110+
`,
111+
)}
112+
id={id}
113+
>
114+
{children}
115+
</ul>
116+
);
117+
}
118+
119+
ListMenu.displayName = 'ListMenu';

0 commit comments

Comments
 (0)