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; +`;