diff --git a/package-lock.json b/package-lock.json
index 3a61134d..bd0392f5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -28258,9 +28258,9 @@
"dev": true
},
"json-parse-even-better-errors": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.0.tgz",
- "integrity": "sha512-o3aP+RsWDJZayj1SbHNQAI8x0v3T3SKiGoZlNYfbUP1S3omJQ6i9CnqADqkSPaOAxwua4/1YWx5CM7oiChJt2Q==",
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true
},
"json-parse-helpfulerror": {
@@ -37303,9 +37303,9 @@
"dev": true
},
"react-docgen-typescript": {
- "version": "1.20.4",
- "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-1.20.4.tgz",
- "integrity": "sha512-gE2SeseJd6+o981qr9VQJRbvFJ5LjLSKQiwhHsuLN4flt+lheKtG1jp2BPzrv2MKR5gmbLwpmNtK4wbLCPSZAw==",
+ "version": "1.20.5",
+ "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-1.20.5.tgz",
+ "integrity": "sha512-AbLGMtn76bn7SYBJSSaKJrZ0lgNRRR3qL60PucM5M4v/AXyC8221cKBXW5Pyt9TfDRfe+LDnPNlg7TibxX0ovA==",
"dev": true
},
"react-dom": {
diff --git a/src/SelectBox/SelectBox.js b/src/SelectBox/SelectBox.js
new file mode 100644
index 00000000..8f82b66d
--- /dev/null
+++ b/src/SelectBox/SelectBox.js
@@ -0,0 +1,104 @@
+import React, { useRef, useEffect } from 'react';
+import propTypes from 'prop-types';
+import { StyledOptionsList, StyledOptionsListItem } from './SelectBox.styles';
+import { StyledCutout } from '../Cutout/Cutout';
+
+const SelectBox = React.forwardRef(function SelectBox(props, ref) {
+ const { options, value, onSelect, width, height } = props;
+ const selectedListItemRef = useRef(null);
+ const listRef = useRef(null);
+
+ const handleKeyDown = event => {
+ switch (event.key) {
+ case 'ArrowDown':
+ if (value < options.length - 1) {
+ event.preventDefault();
+ onSelect(value + 1);
+ listRef.current.childNodes[value + 1].scrollIntoView({
+ block: 'end'
+ });
+ }
+ break;
+ case 'ArrowUp':
+ if (value > 0) {
+ event.preventDefault();
+ onSelect(value - 1);
+ listRef.current.childNodes[value - 1].scrollIntoView({
+ block: 'nearest'
+ });
+ }
+ break;
+ case 'Home':
+ onSelect(0);
+ listRef.current.childNodes[0].scrollIntoView({
+ block: 'start'
+ });
+ break;
+ case 'End':
+ onSelect(options.length - 1);
+ listRef.current.childNodes[options.length - 1].scrollIntoView({
+ block: 'end'
+ });
+ break;
+ default:
+ break;
+ }
+ };
+
+ useEffect(() => {
+ selectedListItemRef.current.scrollIntoView({
+ block: 'start'
+ });
+ listRef.current.focus();
+ }, [selectedListItemRef]);
+
+ const handleClickOnItem = itemValue => {
+ onSelect(itemValue);
+ listRef.current.childNodes[itemValue].scrollIntoView({
+ block: 'nearest'
+ });
+ };
+
+ return (
+
+
+ {options.map(option => (
+ handleClickOnItem(option.value)}
+ type='button'
+ isSelected={option.value === value}
+ ref={option.value === value ? selectedListItemRef : null}
+ >
+ {option.label}
+
+ ))}
+
+
+ );
+});
+
+SelectBox.defaultProps = {
+ onSelect: () => {},
+ options: [],
+ value: 0,
+ width: '300px',
+ height: '150px'
+};
+
+SelectBox.propTypes = {
+ onSelect: propTypes.func,
+ options: propTypes.arrayOf(
+ propTypes.shape({ value: propTypes.number, label: propTypes.string })
+ ),
+ value: propTypes.number,
+ width: propTypes.string,
+ height: propTypes.string
+};
+
+export default SelectBox;
diff --git a/src/SelectBox/SelectBox.spec.js b/src/SelectBox/SelectBox.spec.js
new file mode 100644
index 00000000..8bdda908
--- /dev/null
+++ b/src/SelectBox/SelectBox.spec.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import { renderWithTheme } from '../../test/utils';
+import SelectBox from './SelectBox';
+
+const options = [
+ { label: 'ten', value: 0 },
+ { label: 'twenty', value: 1 },
+ { label: 'thirty', value: 2 }
+];
+
+describe('', () => {
+ beforeEach(() => {
+ window.HTMLElement.prototype.scrollIntoView = function() {};
+ });
+ it('should be able to mount the component', () => {
+ const { container } = renderWithTheme();
+ expect(container.querySelector('ul').querySelector('li').textContent).toBe(
+ 'ten'
+ );
+ });
+});
diff --git a/src/SelectBox/SelectBox.stories.js b/src/SelectBox/SelectBox.stories.js
new file mode 100644
index 00000000..25594940
--- /dev/null
+++ b/src/SelectBox/SelectBox.stories.js
@@ -0,0 +1,89 @@
+import React, { useState } from 'react';
+import styled from 'styled-components';
+
+import { Window, WindowContent, Cutout, Fieldset } from 'react95';
+import SelectBox from './SelectBox';
+
+const Wrapper = styled.div`
+ background: ${({ theme }) => theme.material};
+ padding: 5rem;
+ fieldset,
+ fieldset {
+ margin-bottom: 2rem;
+ }
+ legend + * {
+ margin-bottom: 1rem;
+ }
+ #default-selects {
+ width: 200px;
+ }
+ #cutout > div {
+ width: 250px;
+ padding: 1rem;
+ background: ${({ theme }) => theme.canvas};
+ & > p {
+ margin-bottom: 2rem;
+ }
+ }
+`;
+
+export default {
+ title: 'SelectBox',
+ component: SelectBox,
+ decorators: [story => {story()}]
+};
+
+const options = [
+ { value: 0, label: '[None]' },
+ { value: 1, label: 'Pikachu' },
+ { value: 2, label: 'Bulbasaur' },
+ { value: 3, label: 'Squirtle' },
+ { value: 4, label: 'Mega Charizard Y' },
+ { value: 5, label: 'Jigglypuff' },
+ { value: 6, label: 'Snorlax' },
+ { value: 7, label: 'Geodude' }
+];
+
+export const Default = () => {
+ const [selected, setSelected] = useState(0);
+ const onSelect = value => {
+ setSelected(value);
+ };
+ return (
+
+
+
+ );
+};
+
+Default.story = {
+ name: 'default'
+};
+
+export const Flat = () => (
+
+
+
+
+ When you want to use SelectBox on a light background (like scrollable
+ content), just use the flat variant:
+
+
+
+
+
+);
+
+Flat.story = {
+ name: 'flat'
+};
+
+export const CustomDisplayFormatting = () => ;
+
+CustomDisplayFormatting.story = {
+ name: 'custom display formatting'
+};
diff --git a/src/SelectBox/SelectBox.styles.js b/src/SelectBox/SelectBox.styles.js
new file mode 100644
index 00000000..29809dd2
--- /dev/null
+++ b/src/SelectBox/SelectBox.styles.js
@@ -0,0 +1,33 @@
+import styled from 'styled-components';
+
+import { createScrollbars } from '../common';
+import { blockSizes } from '../common/system';
+
+export const StyledOptionsList = styled.ul`
+ position: relative;
+ box-sizing: border-box;
+ background-color: #fff;
+ font-size: 1rem;
+ line-height: 1.5;
+ overflow-y: auto;
+ ${({ variant }) => createScrollbars(variant)}
+ outline: none;
+`;
+
+export const StyledOptionsListItem = styled.li`
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ height: 31px;
+ line-height: calc(${blockSizes.md} - 4px);
+ background: ${({ theme, isSelected }) =>
+ isSelected ? theme.hoverBackground : 'none'};
+ color: ${({ theme, isSelected }) =>
+ isSelected ? theme.canvasTextInvert : '#000'};
+ width: 100%;
+ padding-left: 8px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ user-select: none;
+`;