Skip to content

Commit 9025b9f

Browse files
committed
Completed changes for v2.1.0, which includes the new 'async' mode; update README.md; add new story 'Async' to storybook and rebuild storybook files; bump some dev dependencies
1 parent 21c016b commit 9025b9f

File tree

59 files changed

+283
-255
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+283
-255
lines changed

.storybook/config/globalStyle.js

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,37 @@ export default createGlobalStyle`
2121
flex: 1;
2222
margin: 0;
2323
display: flex;
24-
color: #212428;
2524
font-size: 1rem;
2625
font-weight: 400;
2726
text-align: left;
2827
line-height: 1.5;
2928
min-height: 120vh;
3029
flex-direction: column;
3130
background-color: #fff;
32-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
31+
letter-spacing: 0.00938em;
32+
color: rgba(0, 0, 0, 0.87);
33+
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;
3334
}
3435
3536
em {
3637
font-weight: 600;
3738
}
3839
40+
strong {
41+
color: black;
42+
font-weight: 600;
43+
font-size: 1.025em;
44+
}
45+
3946
code {
40-
color: #191c20;
47+
line-height: 1.4;
4148
font-size: 0.96em;
42-
border-radius: 0.3em;
49+
border-radius: 2px;
4350
word-break: break-word;
51+
color: rgba(0, 0, 0, 0.87);
4452
padding: .15em .2em .05em;
4553
background-color: rgba(255,229,100,0.2);
46-
font-family: SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
54+
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
4755
4856
@media only screen and (max-width: 525px) {
4957
padding: .1em .25em .1em;

.storybook/config/reactToastifyCss.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ export default css`
7676
max-height: 800px;
7777
overflow: hidden;
7878
font-size: 1.075rem;
79-
font-weight: 600;
80-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
79+
font-weight: 400;
80+
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;
8181
cursor: pointer;
8282
direction: ltr;
8383
&--default {
@@ -86,7 +86,7 @@ export default css`
8686
}
8787
&--info {
8888
color: #fff;
89-
background: #149DF3;
89+
background: #149df3;
9090
}
9191
&-body {
9292
flex: 1;
@@ -139,7 +139,15 @@ export default css`
139139
transition: transform 0.2s;
140140
}
141141
&--default {
142-
background: linear-gradient(to right, #4cd964, #5ac8fa, #007aff, #34aadc, #5856d6, #ff2d55);
142+
background: linear-gradient(
143+
to right,
144+
#4cd964,
145+
#5ac8fa,
146+
#007aff,
147+
#34aadc,
148+
#5856d6,
149+
#ff2d55
150+
);
143151
}
144152
}
145153
}

README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- Extensible styling API with [`styled-components`](https://github.com/styled-components/styled-components)
1414
- Opt-in properties to make the component fully accessible
1515
- Effortlessly scroll, filter, and key through datasets numbering in the tens of thousands via [`react-window`](https://github.com/bvaughn/react-window) + performance conscious code
16+
- Async mode for fetching dynamic options from a remote server using the search input value (starting in `v2.1.0`)
1617

1718
<strong>Peer dependencies:</strong>
1819

@@ -21,14 +22,14 @@
2122

2223
## Overview
2324

24-
Essentially, this is a focused subset of [`react-select`](https://github.com/JedWatson/react-select)'s API that is engineered for ultimate performance and minimal bundle size. It is built entirely using `React Hooks` and `FunctionComponents`. The primary design principal revolves around weighing the cost/benefits of adding a feature against the impact to performance & # of lines of code its addition would have.
25+
Essentially, this is a focused subset of [`react-select`](https://github.com/JedWatson/react-select)'s API that is engineered for ultimate performance and minimal bundle size. It is built entirely using `React Hooks` and `FunctionComponents`. The primary design principal revolves around weighing the cost/benefits of adding a feature against the impact to performance & # of lines of code its addition would have.
2526

26-
I opted to exclude less "in-demand" features such as:
27+
I opted to exclude less "in-demand" features such as:
2728

2829
- Preventing scroll events on the app's body if the menu is open <strong><em>TODO: add code example</em></strong>
2930
- Closing an open menu if the app's body is scrolled <strong><em>TODO: add code example</em></strong>
3031

31-
These feature would have added significant overhead to the package. In addition, if we expose the right public methods and/or callback properties, this feature should be trivial to add to wrapping components - proper decoupling and abstraction of code is key to keeping such channels open for similar customizations that can be kept out of this package.
32+
These feature would have added significant overhead to the package. In addition, if we expose the right public methods and/or callback properties, this feature should be trivial to add to wrapping components - proper decoupling and abstraction of code is key to keeping such channels open for similar customizations that can be kept out of this package.
3233

3334
## Installation
3435

@@ -70,7 +71,7 @@ const SingleSelectDemo: React.FC = () => {
7071
const [isDisabled, setIsDisabled] = useState<boolean>(false);
7172
const [isClearable, setIsClearable] = useState<boolean>(true);
7273
const [selectedOption, setSelectedOption] = useState<CityOption | null>(null);
73-
74+
7475
const getOptionValue = useCallback((option: CityOption): number => option.id, []);
7576
const onOptionChange = useCallback((option: CityOption | null): void => setSelectedOption(option), []);
7677
const getOptionLabel = useCallback((option: CityOption): string => `${option.city}, ${option.state}`, []);
@@ -108,14 +109,15 @@ const SingleSelectDemo: React.FC = () => {
108109

109110
All properties are technically optional (with a few having default values). Very similar with [`react-select`](https://github.com/JedWatson/react-select)'s API.
110111

111-
> <strong><em>Note that the following non-primitive properties should be properly memoized if defined:</em></strong><br>`clearIcon`, `caretIcon`, `options`, `renderOptionLabel`, `onMenuOpen`, `onOptionChange`, `onKeyDown`, `getOptionLabel`, `getOptionLabel`, `getOptionValue`, `onInputBlur`, `onInputFocus`, `getIsOptionDisabled`, `getFilterOptionString`, `themeConfig`
112+
> <strong><em>Note that the following non-primitive properties should be properly memoized if defined:</em></strong><br>`clearIcon`, `caretIcon`, `options`, `renderOptionLabel`, `onMenuOpen`, `onOptionChange`, `onKeyDown`, `getOptionLabel`, `getOptionLabel`, `getOptionValue`, `onInputBlur`, `onInputFocus`, `onInputChange`, `onSearchChange`, `getIsOptionDisabled`, `getFilterOptionString`, `themeConfig`
112113
113114
| Property | Type | Default | Description
114115
:---|:---|:---|:---
115116
| `inputId`| string | `undefined` | The id of the autosize search input
116117
|`selectId`| string | `undefined` | The id of the parent div
117118
|`ariaLabel`| string | `undefined` | Aria label (for assistive tech)
118119
|`isMulti`| bool | `false` | Does the control allow for multiple selections (defaults to single-value mode)
120+
|`async`| bool | `false` | Is the component in 'async' mode - when in 'async' mode, updates to the input search value will NOT cause the effect `useMenuOptions` to execute (this effect parses `options` into stateful value `menuOptions`)
119121
|`autoFocus`| bool | `false` | Focus the control following initial mount of component
120122
|`isLoading`| bool | `false` | Is the select in a state of loading - shows loading dots animation
121123
|`isInvalid`| bool | `false` | Is the current value invalid - control recieves invalid styling
@@ -126,6 +128,7 @@ All properties are technically optional (with a few having default values). Very
126128
|`menuItemSize`| number | `35` | The height of each option in the menu (px)
127129
|`isClearable`| bool | `false` | Is the select value clearable
128130
|`noOptionsMsg`| string | `No options` | The text displayed in the menu when there are no options available
131+
|`loadingMsg`| string | `Loading...` | The text displayed in the menu when `isLoading` === `true`
129132
|`clearIcon`| ReactNode | `undefined` | Custom clear icon node
130133
|`caretIcon`| ReactNode | `undefined` | Custom caret icon node
131134
|`loadingNode`| ReactNode | `undefined` | Custom loading node
@@ -157,6 +160,8 @@ All properties are technically optional (with a few having default values). Very
157160
|`getOptionValue`| (data: any): ReactText | `undefined` | Resolves option data to React.ReactText to compare option values (by default will use option.value)
158161
|`onInputBlur`| (e: FocusEvent\<HTMLInputElement\>): void | `undefined` | Handle blur events on the search input
159162
|`onInputFocus`| (e: FocusEvent\<HTMLInputElement\>): void | `undefined` | Handle focus events on the search input
163+
|`onInputChange`| (value: string): void | `undefined` | Handle change events on the search input
164+
|`onSearchChange`| (value: string): void | `undefined` | Callback executed after the debounced search input value is persisted to the component's state - if no debounce is defined via the `inputDelay` property, it probably makes more sense to use `onInputChange` instead.
160165
|`renderOptionLabel`| (data: any): ReactNode | `undefined` | Formats option labels in the menu and control as JSX.Elements or React Components (by default will use `getOptionLabel`)
161166
|`getIsOptionDisabled`| (data: any): boolean | `undefined` | When defined will evaluate each option to determine whether it is disabled or not (if not specified, each option will be evaluated as to whether or not it contains a property of `isDisabled` with a value of `true`)
162167
|`getFilterOptionString`| (option: any): string | `undefined` | When defined will take each option and generate a string used in the filtering process (by default, will use option.label)

__stories__/2-Styling.story.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ storiesOf('React Functional Select', module).add('Styling', () => {
157157
/>
158158
</Column>
159159
</Columns>
160-
<SubTitle>Using classNames</SubTitle>
160+
<SubTitle>Using Classes</SubTitle>
161161
<Columns>
162162
<Column widthPercent={40}>
163163
<Content>
@@ -179,9 +179,7 @@ storiesOf('React Functional Select', module).add('Styling', () => {
179179
</List>
180180
</ListWrapper>
181181
</Column>
182-
<Column widthPercent={60}>
183-
{memoizedMarkupNode}
184-
</Column>
182+
<Column widthPercent={60}>{memoizedMarkupNode}</Column>
185183
</Columns>
186184
<SubTitle>Demo</SubTitle>
187185
<Hr />

__stories__/3-Events.story.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ storiesOf('React Functional Select', module).add('Events', () => {
5050
<TextHeader>onMenuClose(...args: any[]): void</TextHeader> -
5151
executed after the menu is closed
5252
</ListItem>
53+
<ListItem>
54+
<TextHeader>onInputChange(value: string): void</TextHeader> -
55+
executed after the input control's value changes
56+
</ListItem>
5357
<ListItem>
5458
<TextHeader>onKeyDown(e: KeyboardEvent&lt;HTMLDivElement&gt;): void</TextHeader> -
5559
executed after the onKeyDown event
@@ -62,24 +66,20 @@ storiesOf('React Functional Select', module).add('Events', () => {
6266
<TextHeader>onInputFocus(e: FocusEvent&lt;HTMLInputElement&gt;): void</TextHeader> -
6367
executed after the input control is focused
6468
</ListItem>
65-
<ListItem>
66-
<TextHeader>onInputChange(value: string): void</TextHeader> -
67-
executed directly following the input control's <code>onChange</code> event.
68-
</ListItem>
6969
<ListItem>
7070
<TextHeader>onSearchChange(value: string): void</TextHeader> -
71-
executed after the input value is persisted to state. This value also evaluates
71+
executed after the input value is persisted to state; this value also evaluates
7272
the <code>inputDelay</code> property for debouncing - this callback is really only useful
7373
when <code>inputDelay</code> is defined, and if not, it probably makes more sense to use
74-
the <code>onInputChange</code> callback.
74+
the <code>onInputChange</code> callback
7575
</ListItem>
7676
</List>
7777
</ListWrapper>
7878
<SubTitle>Demo</SubTitle>
7979
<Hr />
8080
<Card>
8181
<CardHeader>
82-
<LabelNote>*For demo purposes, events trigger a notification when executed.</LabelNote>
82+
<LabelNote>For demo purposes, events trigger a notification when executed.</LabelNote>
8383
<CheckboxGroup>
8484
<Checkbox
8585
label='onOptionChange'

__stories__/8-Async.story.tsx

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,30 @@ import React, { useState, useCallback } from 'react';
22
import { Select } from '../src';
33
import { Option } from './helpers/types';
44
import { storiesOf } from '@storybook/react';
5-
import { mockHttpRequest, createAsyncOptions } from './helpers/utils';
5+
import { getRandomInt, mockHttpRequest, createAsyncOptions } from './helpers/utils';
66
import { Hr, LabelNote, Title, SubTitle, List, ListItem, ListWrapper, Container, SelectContainer, TextHeader, Card, CardHeader, CardBody } from './helpers/styled';
77

88
storiesOf('React Functional Select', module).add('Async', () => {
99
const [isLoading, setIsLoading] = useState<boolean>(false);
10-
const [options, setOptions] = useState<Option[]>(() => createAsyncOptions(3, 'Initial'));
10+
const [options, setOptions] = useState<Option[]>(() => createAsyncOptions(5, 'Initial'));
1111

1212
const onInputChange = useCallback((): void => {
1313
setIsLoading(true);
1414
}, []);
1515

1616
const onSearchChange = useCallback((value: string): void => {
17-
mockHttpRequest().then(() => {
18-
const nextOptions = createAsyncOptions(3, `Search text: ${value || `''`}`);
19-
setOptions(nextOptions);
20-
setIsLoading(false);
21-
});
17+
mockHttpRequest(500)
18+
.then(() => {
19+
const count = getRandomInt(1, 5);
20+
const nextOptions = createAsyncOptions(count, `Search text: ${value || 'Initial'}`);
21+
setOptions(nextOptions);
22+
})
23+
.catch((e) => {
24+
console.error(e);
25+
})
26+
.then(() => {
27+
setIsLoading(false);
28+
});
2229
}, []);
2330

2431
return (
@@ -28,46 +35,45 @@ storiesOf('React Functional Select', module).add('Async', () => {
2835
<ListWrapper>
2936
Add the <code>async</code> property to enable async mode. There is one key difference
3037
in core functionality with async mode - changes to search input value will not cause
31-
the <code>useMenuOptions</code> effect to run (it will just wait for updates to
32-
the <code>options</code> property). The rest of hooking into async mode is achieved
33-
using some combination of the properties found below. <em>For callback function
34-
properties, strongly prefer memoization, as you could encounter some unintended behaviors.</em>
38+
the <code>useMenuOptions</code> effect to run. The rest of hooking into async mode is
39+
achieved using some combination of the properties found below
40+
. <em>Properties <code>onInputChange</code> and <code>onSearchChange</code> should be
41+
memoized.</em>
3542
<List>
3643
<ListItem>
3744
<TextHeader>onInputChange(value: string): void</TextHeader> -
3845
callback executed directly following the input control's <code>onChange</code> event.
39-
This callback is not in linked to the debounced value, so it fires immediately. This is a
46+
This callback is not debounced, so it fires immediately. This is a good
4047
place to set a stateful loading property in your parent component that is mapped to
4148
react-functional-select's <code>isLoading</code> property.
4249
</ListItem>
4350
<ListItem>
4451
<TextHeader>onSearchChange(value: string): void</TextHeader> -
45-
callback executed following component state updates for stateful
46-
value <code>debouncedInputValue</code>. The debounced delay controlled by
47-
the <code>inputDelay</code> property. This is most likely where your http fetch
48-
request and post-request logic (i.e. setting isLoading false) would go.
52+
callback executed following component state updates for
53+
the <code>debouncedInputValue</code>. The debounce is set using
54+
the <code>inputDelay</code> property. This callback is a good place for your
55+
http fetch request and post-request logic (i.e. setting isLoading false).
4956
</ListItem>
5057
<ListItem>
5158
<TextHeader>inputDelay?: number</TextHeader> - As mentioned above, this can be
52-
set to a positive integer in order to debounce updates to the search input value following
53-
input change events. This property directly maps to the <code>delay</code> in milliconds
54-
passed to the <code>setTimeout</code> method.
59+
set to a positive integer in order to debounce updates to the search input value
60+
following input change events. This property directly maps to the <code>delay</code> in
61+
milliconds passed to the <code>setTimeout</code> method.
5562
</ListItem>
5663
<ListItem>
57-
<TextHeader>isLoading?: boolean</TextHeader> - When true, a loading animation will appear
58-
in the far-right of the control and take the place of the clear icon (if shown). Additionally,
59-
it will hide options in the menu and instead, display a loading message. The loading message
60-
text defaults to 'Loading...', but can be overriden via the <code>loadingMsg</code> property.
64+
<TextHeader>isLoading?: boolean</TextHeader> - When true, a loading animation will
65+
appear in the far-right of the control and take the place of the clear icon (if shown).
66+
Additionally, it will hide options in the menu and instead, display a loading message.
67+
The loading message text defaults to 'Loading...', but can be overriden via
68+
the <code>loadingMsg</code> property.
6169
</ListItem>
6270
</List>
6371
</ListWrapper>
6472
<SubTitle>Demo</SubTitle>
6573
<Hr />
6674
<Card>
6775
<CardHeader>
68-
<LabelNote>
69-
*The search input change event is debounced using a delay of 375ms and the simulated http call resolves after 1000ms.
70-
</LabelNote>
76+
<LabelNote>Search input debounced 375ms and the mock http call resolves after 500ms</LabelNote>
7177
</CardHeader>
7278
<CardBody>
7379
<SelectContainer>

__stories__/helpers/components/Checkbox.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,20 @@ type CheckboxProps = {
1313
readonly onCheck: (checked: boolean) => void;
1414
};
1515

16-
const COLOR_LABEL = '#5E5E5E';
17-
const COLOR_BORDER = '#ced4da';
1816
const COLOR_CHECK_MARK = '#149DF3';
19-
const COLOR_BORDER_CHECKED = hexToRgba(COLOR_CHECK_MARK, 0.6);
17+
const COLOR_BORDER = 'rgba(0, 0, 0, 0.54)';
18+
const COLOR_BORDER_CHECKED = hexToRgba(COLOR_CHECK_MARK, 0.85);
2019

2120
const Label = styled.span`
2221
user-select: none;
23-
font-style: italic;
2422
margin-left: 1.6rem;
25-
color: ${COLOR_LABEL};
2623
`;
2724

2825
const Input = styled.input`
29-
top: 0.2em;
3026
z-index: 3;
3127
opacity: 0;
32-
width: 1rem;
33-
height: 1rem;
28+
width: 1em;
29+
height: 1em;
3430
cursor: pointer;
3531
position: absolute;
3632
@@ -58,7 +54,8 @@ const CheckboxWrapper = styled.label<CheckboxWrapperProps>`
5854
user-select: none;
5955
position: relative;
6056
margin-top: 0.5rem;
61-
display: inline-block;
57+
align-items: center;
58+
display: inline-flex;
6259
6360
${({ isReadOnly }) =>
6461
isReadOnly
@@ -72,14 +69,13 @@ const CheckboxWrapper = styled.label<CheckboxWrapperProps>`
7269
`;
7370

7471
const CheckIcon = styled.i`
75-
top: 0.2em;
7672
z-index: 0;
7773
width: 1rem;
7874
height: 1rem;
7975
position: absolute;
8076
border-style: solid;
77+
border-width: 1.5px;
8178
box-sizing: border-box;
82-
border-width: 0.125rem;
8379
border-radius: 0.0625rem;
8480
background-color: transparent;
8581
transition: border-color 0.365s ease;

0 commit comments

Comments
 (0)