diff --git a/README.md b/README.md
index 4dc7ecc..8a0b291 100644
--- a/README.md
+++ b/README.md
@@ -193,7 +193,7 @@ The optionSnapshot is an object that contains the object state: `{ selected: boo
## Get options
-You can fetch options asynchronously with the `getOptions` property. You can either return options directly or through a `Promise`.
+You can fetch options asynchronously with the `getOptions` property. You can either return options directly or through a `Promise`. If a falsy value is returned, options will still be controlled by the `options` prop.
```jsx
function getOptions(query) {
diff --git a/__tests__/__snapshots__/0-Single.stories.storyshot b/__tests__/__snapshots__/0-Single.stories.storyshot
new file mode 100644
index 0000000..0586ac0
--- /dev/null
+++ b/__tests__/__snapshots__/0-Single.stories.storyshot
@@ -0,0 +1,427 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Storyshots Single select Always Open 1`] = `
+
+
+
+
+
+
+
+
+ Small
+
+
+
+
+ Medium
+
+
+
+
+ Large
+
+
+
+
+
+`;
+
+exports[`Storyshots Single select Default 1`] = `
+
+`;
+
+exports[`Storyshots Single select Group 1`] = `
+
+
+
+
+
+
+
+
+
+ Food
+
+
+
+
+
+ Hamburger
+
+
+
+
+ Pizza
+
+
+
+
+
+
+
+
+
+ Drinks
+
+
+
+
+
+ Soft drink
+
+
+
+
+ Beer
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Storyshots Single select Limited Options 1`] = `
+
+`;
+
+exports[`Storyshots Single select Search 1`] = `
+
+`;
+
+exports[`Storyshots Single select Search With Empty Message 1`] = `
+
+`;
+
+exports[`Storyshots Single select Search With Empty Message Renderer 1`] = `
+
+`;
+
+exports[`Storyshots Single select Stay On Select 1`] = `
+
+`;
+
+exports[`Storyshots Single select With Placeholder 1`] = `
+
+`;
diff --git a/__tests__/__snapshots__/storybook.test.js.snap b/__tests__/__snapshots__/1-Multiple.stories.storyshot
similarity index 77%
rename from __tests__/__snapshots__/storybook.test.js.snap
rename to __tests__/__snapshots__/1-Multiple.stories.storyshot
index 19a3414..6a7926f 100644
--- a/__tests__/__snapshots__/storybook.test.js.snap
+++ b/__tests__/__snapshots__/1-Multiple.stories.storyshot
@@ -1,94 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Storyshots Async Fetch 1`] = `
-
-`;
-
-exports[`Storyshots Async Fetch Multiple 1`] = `
-
-`;
-
-exports[`Storyshots Custom Avatar Example 1`] = `
+exports[`Storyshots Multiple select Default 1`] = `
-
-
-
-
-
-
- Annie Cruz
-
-
+ Hamburger
-
-
-
- Eli Shelton
-
-
+ Fries
-
-
-
-
- Loretta Rogers
-
-
-
-
-
-
-
-
-
- Lloyd Fisher
-
-
-
-
-
-
-
-
-
- Tiffany Gonzales
-
-
-
-
-
-
-
-
-
- Charles Hardy
-
-
-
-
-
-
-
-
-
- Rudolf Wilson
-
-
-
-
-
-
-
-
-
- Emerald Hensley
-
-
-
-
-
-
-
-
-
- Lorena McCoy
-
-
-
-
-
-
-
-
-
- Alicia Lamb
-
-
-
-
-
-
-
-
- Maria Waters
-
-
+ Milkshake
@@ -465,26 +65,26 @@ exports[`Storyshots Custom Avatar Example 1`] = `
`;
-exports[`Storyshots Custom CSS Modules 1`] = `
+exports[`Storyshots Multiple select Default Value 1`] = `
`;
-exports[`Storyshots Custom Controllable Display 1`] = `
-
-
-
- Display options
-
-
-`;
-
-exports[`Storyshots Custom Font Example 1`] = `
+exports[`Storyshots Multiple select Disabled 1`] = `
@@ -622,408 +158,14 @@ exports[`Storyshots Custom Font Example 1`] = `
`;
-exports[`Storyshots Events Controlled Value 1`] = `
-Array [
- ,
-
- You have selected:
-
,
-
- Click to reset
- ,
-
- Set medium
- ,
-]
-`;
-
-exports[`Storyshots Events On Change 1`] = `
-Array [
- ,
-
- Aa
- ,
-]
-`;
-
-exports[`Storyshots Hooks Custom Components 1`] = `
-
- Size:
-
-`;
-
-exports[`Storyshots Misc Form 1`] = `
-Array [
- ,
- ,
-]
-`;
-
-exports[`Storyshots Misc Numeric Values 1`] = `
+exports[`Storyshots Multiple select Group 1`] = `
-`;
-
-exports[`Storyshots Multiple select Default 1`] = `
-
-
-
-
-
- Hamburger
-
-
-
-
- Fries
-
-
-
-
- Milkshake
-
-
-
-
-
-`;
-
-exports[`Storyshots Multiple select Default Value 1`] = `
-
-
-
-
-
- Hamburger
-
-
-
-
- Fries
-
-
-
-
- Milkshake
-
-
-
-
-
-`;
-
-exports[`Storyshots Multiple select Disabled 1`] = `
-
-`;
-
-exports[`Storyshots Multiple select Group 1`] = `
-
@@ -5172,7 +4314,7 @@ exports[`Storyshots Multiple select Stay On Select With Single Initial Value 1`]
placeholder="Select your items"
readOnly={true}
tabIndex="0"
- value=""
+ value="Fries"
/>
@@ -5262,429 +4404,3 @@ exports[`Storyshots Multiple select With Placeholder 1`] = `
`;
-
-exports[`Storyshots Single select Always Open 1`] = `
-
-
-
-
-
-
-
-
- Small
-
-
-
-
- Medium
-
-
-
-
- Large
-
-
-
-
-
-`;
-
-exports[`Storyshots Single select Default 1`] = `
-
-`;
-
-exports[`Storyshots Single select Group 1`] = `
-
-
-
-
-
-
-
-
-
- Food
-
-
-
-
-
- Hamburger
-
-
-
-
- Pizza
-
-
-
-
-
-
-
-
-
- Drinks
-
-
-
-
-
- Soft drink
-
-
-
-
- Beer
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`Storyshots Single select Limited Options 1`] = `
-
-`;
-
-exports[`Storyshots Single select Search 1`] = `
-
-`;
-
-exports[`Storyshots Single select Search With Empty Message 1`] = `
-
-`;
-
-exports[`Storyshots Single select Search With Empty Message Renderer 1`] = `
-
-`;
-
-exports[`Storyshots Single select Stay On Select 1`] = `
-
-`;
-
-exports[`Storyshots Single select With Placeholder 1`] = `
-
-`;
diff --git a/__tests__/__snapshots__/2-Events.stories.storyshot b/__tests__/__snapshots__/2-Events.stories.storyshot
new file mode 100644
index 0000000..bf70d6b
--- /dev/null
+++ b/__tests__/__snapshots__/2-Events.stories.storyshot
@@ -0,0 +1,129 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Storyshots Events Controlled Value 1`] = `
+Array [
+ ,
+
+ You have selected:
+
,
+
+ Click to reset
+ ,
+
+ Set medium
+ ,
+]
+`;
+
+exports[`Storyshots Events On Change 1`] = `
+Array [
+ ,
+
+ Aa
+ ,
+]
+`;
diff --git a/__tests__/__snapshots__/3-Custom.stories.storyshot b/__tests__/__snapshots__/3-Custom.stories.storyshot
new file mode 100644
index 0000000..525ec89
--- /dev/null
+++ b/__tests__/__snapshots__/3-Custom.stories.storyshot
@@ -0,0 +1,559 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Storyshots Custom Avatar Example 1`] = `
+
+`;
+
+exports[`Storyshots Custom CSS Modules 1`] = `
+
+
+
+
+
+ Hamburger
+
+
+
+
+ Fries
+
+
+
+
+ Milkshake
+
+
+
+
+
+`;
+
+exports[`Storyshots Custom Controllable Display 1`] = `
+
+
+
+ Display options
+
+
+`;
+
+exports[`Storyshots Custom Font Example 1`] = `
+
+`;
diff --git a/__tests__/__snapshots__/4-Async.stories.storyshot b/__tests__/__snapshots__/4-Async.stories.storyshot
new file mode 100644
index 0000000..3b5f7f8
--- /dev/null
+++ b/__tests__/__snapshots__/4-Async.stories.storyshot
@@ -0,0 +1,129 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Storyshots Async Controlled Fetch 1`] = `
+
+`;
+
+exports[`Storyshots Async Controlled Fetch Multiple 1`] = `
+
+`;
+
+exports[`Storyshots Async Fetch 1`] = `
+
+`;
+
+exports[`Storyshots Async Fetch Multiple 1`] = `
+
+`;
diff --git a/__tests__/__snapshots__/5-Hooks.stories.storyshot b/__tests__/__snapshots__/5-Hooks.stories.storyshot
new file mode 100644
index 0000000..6c29669
--- /dev/null
+++ b/__tests__/__snapshots__/5-Hooks.stories.storyshot
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Storyshots Hooks Custom Components 1`] = `
+
+ Size: Small
+
+`;
diff --git a/__tests__/__snapshots__/6-Misc.stories.storyshot b/__tests__/__snapshots__/6-Misc.stories.storyshot
new file mode 100644
index 0000000..bb6803b
--- /dev/null
+++ b/__tests__/__snapshots__/6-Misc.stories.storyshot
@@ -0,0 +1,92 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Storyshots Misc Form 1`] = `
+Array [
+ ,
+ ,
+]
+`;
+
+exports[`Storyshots Misc Numeric Values 1`] = `
+
+`;
diff --git a/__tests__/equal.test.js b/__tests__/equal.test.js
new file mode 100644
index 0000000..013da06
--- /dev/null
+++ b/__tests__/equal.test.js
@@ -0,0 +1,759 @@
+import equal from '../src/lib/equal';
+import assert from 'assert';
+
+
+function func1() {}
+function func2() {}
+class MyMap extends Map {}
+class MySet extends Set {}
+var emptyObj = {};
+function map(obj, Class) {
+ var a = new (Class || Map);
+ for (var key in obj)
+ a.set(key, obj[key]);
+ return a;
+}
+
+function myMap(obj) {
+ return map(obj, MyMap);
+}
+
+function set(arr, Class) {
+ var a = new (Class || Set);
+ for (var value of arr)
+ a.add(value);
+ return a;
+}
+
+function mySet(arr) {
+ return set(arr, MySet);
+}
+
+var skipBigInt = typeof BigInt == 'undefined';
+var skipBigIntArray = typeof BigUint64Array == 'undefined';
+
+
+function testCases(equalFunc, suiteName, suiteTests) {
+ describe(suiteName, function() {
+ suiteTests.forEach(function (suite) {
+ describe(suite.description, function() {
+ suite.tests.forEach(function (test) {
+ (test.skip ? it.skip : it)(test.description, function() {
+ assert.strictEqual(equalFunc(test.value1, test.value2), test.equal);
+ });
+ (test.skip ? it.skip : it)(test.description + ' (reverse arguments)', function() {
+ assert.strictEqual(equalFunc(test.value2, test.value1), test.equal);
+ });
+ });
+ });
+ });
+ });
+}
+
+const standard_tests = [
+ {
+ description: 'scalars',
+ tests: [
+ {
+ description: 'equal numbers',
+ value1: 1,
+ value2: 1,
+ equal: true
+ },
+ {
+ description: 'not equal numbers',
+ value1: 1,
+ value2: 2,
+ equal: false
+ },
+ {
+ description: 'number and array are not equal',
+ value1: 1,
+ value2: [],
+ equal: false
+ },
+ {
+ description: '0 and null are not equal',
+ value1: 0,
+ value2: null,
+ equal: false
+ },
+ {
+ description: 'equal strings',
+ value1: 'a',
+ value2: 'a',
+ equal: true
+ },
+ {
+ description: 'not equal strings',
+ value1: 'a',
+ value2: 'b',
+ equal: false
+ },
+ {
+ description: 'empty string and null are not equal',
+ value1: '',
+ value2: null,
+ equal: false
+ },
+ {
+ description: 'null is equal to null',
+ value1: null,
+ value2: null,
+ equal: true
+ },
+ {
+ description: 'equal booleans (true)',
+ value1: true,
+ value2: true,
+ equal: true
+ },
+ {
+ description: 'equal booleans (false)',
+ value1: false,
+ value2: false,
+ equal: true
+ },
+ {
+ description: 'not equal booleans',
+ value1: true,
+ value2: false,
+ equal: false
+ },
+ {
+ description: '1 and true are not equal',
+ value1: 1,
+ value2: true,
+ equal: false
+ },
+ {
+ description: '0 and false are not equal',
+ value1: 0,
+ value2: false,
+ equal: false
+ },
+ {
+ description: 'NaN and NaN are equal',
+ value1: NaN,
+ value2: NaN,
+ equal: true
+ },
+ {
+ description: '0 and -0 are equal',
+ value1: 0,
+ value2: -0,
+ equal: true
+ },
+ {
+ description: 'Infinity and Infinity are equal',
+ value1: Infinity,
+ value2: Infinity,
+ equal: true
+ },
+ {
+ description: 'Infinity and -Infinity are not equal',
+ value1: Infinity,
+ value2: -Infinity,
+ equal: false
+ }
+ ]
+ },
+
+ {
+ description: 'objects',
+ tests: [
+ {
+ description: 'empty objects are equal',
+ value1: {},
+ value2: {},
+ equal: true
+ },
+ {
+ description: 'equal objects (same properties "order")',
+ value1: {a: 1, b: '2'},
+ value2: {a: 1, b: '2'},
+ equal: true
+ },
+ {
+ description: 'equal objects (different properties "order")',
+ value1: {a: 1, b: '2'},
+ value2: {b: '2', a: 1},
+ equal: true
+ },
+ {
+ description: 'not equal objects (extra property)',
+ value1: {a: 1, b: '2'},
+ value2: {a: 1, b: '2', c: []},
+ equal: false
+ },
+ {
+ description: 'not equal objects (different property values)',
+ value1: {a: 1, b: '2', c: 3},
+ value2: {a: 1, b: '2', c: 4},
+ equal: false
+ },
+ {
+ description: 'not equal objects (different properties)',
+ value1: {a: 1, b: '2', c: 3},
+ value2: {a: 1, b: '2', d: 3},
+ equal: false
+ },
+ {
+ description: 'equal objects (same sub-properties)',
+ value1: { a: [ { b: 'c' } ] },
+ value2: { a: [ { b: 'c' } ] },
+ equal: true
+ },
+ {
+ description: 'not equal objects (different sub-property value)',
+ value1: { a: [ { b: 'c' } ] },
+ value2: { a: [ { b: 'd' } ] },
+ equal: false
+ },
+ {
+ description: 'not equal objects (different sub-property)',
+ value1: { a: [ { b: 'c' } ] },
+ value2: { a: [ { c: 'c' } ] },
+ equal: false
+ },
+ {
+ description: 'empty array and empty object are not equal',
+ value1: {},
+ value2: [],
+ equal: false
+ },
+ {
+ description: 'object with extra undefined properties are not equal #1',
+ value1: {},
+ value2: {foo: undefined},
+ equal: false
+ },
+ {
+ description: 'object with extra undefined properties are not equal #2',
+ value1: {foo: undefined},
+ value2: {},
+ equal: false
+ },
+ {
+ description: 'object with extra undefined properties are not equal #3',
+ value1: {foo: undefined},
+ value2: {bar: undefined},
+ equal: false
+ },
+ {
+ description: 'nulls are equal',
+ value1: null,
+ value2: null,
+ equal: true
+ },
+ {
+ description: 'null and undefined are not equal',
+ value1: null,
+ value2: undefined,
+ equal: false
+ },
+ {
+ description: 'null and empty object are not equal',
+ value1: null,
+ value2: {},
+ equal: false
+ },
+ {
+ description: 'undefined and empty object are not equal',
+ value1: undefined,
+ value2: {},
+ equal: false
+ },
+ {
+ description: 'objects with different `toString` functions returning same values are equal',
+ value1: {toString: ()=>'Hello world!'},
+ value2: {toString: ()=>'Hello world!'},
+ equal: true
+ },
+ {
+ description: 'objects with `toString` functions returning different values are not equal',
+ value1: {toString: ()=>'Hello world!'},
+ value2: {toString: ()=>'Hi!'},
+ equal: false
+ }
+ ]
+ },
+
+ {
+ description: 'arrays',
+ tests: [
+ {
+ description: 'two empty arrays are equal',
+ value1: [],
+ value2: [],
+ equal: true
+ },
+ {
+ description: 'equal arrays',
+ value1: [1, 2, 3],
+ value2: [1, 2, 3],
+ equal: true
+ },
+ {
+ description: 'not equal arrays (different item)',
+ value1: [1, 2, 3],
+ value2: [1, 2, 4],
+ equal: false
+ },
+ {
+ description: 'not equal arrays (different length)',
+ value1: [1, 2, 3],
+ value2: [1, 2],
+ equal: false
+ },
+ {
+ description: 'equal arrays of objects',
+ value1: [{a: 'a'}, {b: 'b'}],
+ value2: [{a: 'a'}, {b: 'b'}],
+ equal: true
+ },
+ {
+ description: 'not equal arrays of objects',
+ value1: [{a: 'a'}, {b: 'b'}],
+ value2: [{a: 'a'}, {b: 'c'}],
+ equal: false
+ },
+ {
+ description: 'pseudo array and equivalent array are not equal',
+ value1: {'0': 0, '1': 1, length: 2},
+ value2: [0, 1],
+ equal: false
+ }
+ ]
+ },
+ {
+ description: 'Date objects',
+ tests: [
+ {
+ description: 'equal date objects',
+ value1: new Date('2017-06-16T21:36:48.362Z'),
+ value2: new Date('2017-06-16T21:36:48.362Z'),
+ equal: true
+ },
+ {
+ description: 'not equal date objects',
+ value1: new Date('2017-06-16T21:36:48.362Z'),
+ value2: new Date('2017-01-01T00:00:00.000Z'),
+ equal: false
+ },
+ {
+ description: 'date and string are not equal',
+ value1: new Date('2017-06-16T21:36:48.362Z'),
+ value2: '2017-06-16T21:36:48.362Z',
+ equal: false
+ },
+ {
+ description: 'date and object are not equal',
+ value1: new Date('2017-06-16T21:36:48.362Z'),
+ value2: {},
+ equal: false
+ }
+ ]
+ },
+ {
+ description: 'RegExp objects',
+ tests: [
+ {
+ description: 'equal RegExp objects',
+ value1: /foo/,
+ value2: /foo/,
+ equal: true
+ },
+ {
+ description: 'not equal RegExp objects (different pattern)',
+ value1: /foo/,
+ value2: /bar/,
+ equal: false
+ },
+ {
+ description: 'not equal RegExp objects (different flags)',
+ value1: /foo/,
+ value2: /foo/i,
+ equal: false
+ },
+ {
+ description: 'RegExp and string are not equal',
+ value1: /foo/,
+ value2: 'foo',
+ equal: false
+ },
+ {
+ description: 'RegExp and object are not equal',
+ value1: /foo/,
+ value2: {},
+ equal: false
+ }
+ ]
+ },
+ {
+ description: 'functions',
+ tests: [
+ {
+ description: 'same function is equal',
+ value1: func1,
+ value2: func1,
+ equal: true
+ },
+ {
+ description: 'different functions are not equal',
+ value1: func1,
+ value2: func2,
+ equal: false
+ }
+ ]
+ },
+ {
+ description: 'sample objects',
+ tests: [
+ {
+ description: 'big object',
+ value1: {
+ prop1: 'value1',
+ prop2: 'value2',
+ prop3: 'value3',
+ prop4: {
+ subProp1: 'sub value1',
+ subProp2: {
+ subSubProp1: 'sub sub value1',
+ subSubProp2: [1, 2, {prop2: 1, prop: 2}, 4, 5]
+ }
+ },
+ prop5: 1000,
+ prop6: new Date(2016, 2, 10)
+ },
+ value2: {
+ prop5: 1000,
+ prop3: 'value3',
+ prop1: 'value1',
+ prop2: 'value2',
+ prop6: new Date('2016/03/10'),
+ prop4: {
+ subProp2: {
+ subSubProp1: 'sub sub value1',
+ subSubProp2: [1, 2, {prop2: 1, prop: 2}, 4, 5]
+ },
+ subProp1: 'sub value1'
+ }
+ },
+ equal: true
+ }
+ ]
+ }
+];
+
+const es6_tests = [
+ {
+ description: 'bigint',
+ tests: [
+ {
+ description: 'equal bigints',
+ value1: skipBigInt || BigInt(1),
+ value2: skipBigInt || BigInt(1),
+ equal: true,
+ skip: skipBigInt
+ },
+ {
+ description: 'not equal bigints',
+ value1: skipBigInt || BigInt(1),
+ value2: skipBigInt || BigInt(2),
+ equal: false,
+ skip: skipBigInt
+ }
+ ]
+ },
+
+ {
+ description: 'Maps',
+ tests: [
+ {
+ description: 'empty maps are equal',
+ value1: new Map,
+ value2: new Map,
+ equal: true
+ },
+ {
+ description: 'empty maps of different class are not equal',
+ value1: new Map,
+ value2: new MyMap,
+ equal: false
+ },
+ {
+ description: 'equal maps (same key "order")',
+ value1: map({a: 1, b: '2'}),
+ value2: map({a: 1, b: '2'}),
+ equal: true
+ },
+ {
+ description: 'not equal maps (same key "order" - instances of different classes)',
+ value1: map({a: 1, b: '2'}),
+ value2: myMap({a: 1, b: '2'}),
+ equal: false
+ },
+ {
+ description: 'equal maps (different key "order")',
+ value1: map({a: 1, b: '2'}),
+ value2: map({b: '2', a: 1}),
+ equal: true
+ },
+ {
+ description: 'equal maps (different key "order" - instances of the same subclass)',
+ value1: myMap({a: 1, b: '2'}),
+ value2: myMap({b: '2', a: 1}),
+ equal: true
+ },
+ {
+ description: 'not equal maps (extra key)',
+ value1: map({a: 1, b: '2'}),
+ value2: map({a: 1, b: '2', c: []}),
+ equal: false
+ },
+ {
+ description: 'not equal maps (different key value)',
+ value1: map({a: 1, b: '2', c: 3}),
+ value2: map({a: 1, b: '2', c: 4}),
+ equal: false
+ },
+ {
+ description: 'not equal maps (different keys)',
+ value1: map({a: 1, b: '2', c: 3}),
+ value2: map({a: 1, b: '2', d: 3}),
+ equal: false
+ },
+ {
+ description: 'equal maps (same sub-keys)',
+ value1: map({ a: [ map({ b: 'c' }) ] }),
+ value2: map({ a: [ map({ b: 'c' }) ] }),
+ equal: true
+ },
+ {
+ description: 'not equal maps (different sub-key value)',
+ value1: map({ a: [ map({ b: 'c' }) ] }),
+ value2: map({ a: [ map({ b: 'd' }) ] }),
+ equal: false
+ },
+ {
+ description: 'not equal maps (different sub-key)',
+ value1: map({ a: [ map({ b: 'c' }) ] }),
+ value2: map({ a: [ map({ c: 'c' }) ] }),
+ equal: false
+ },
+ {
+ description: 'empty map and empty object are not equal',
+ value1: {},
+ value2: new Map,
+ equal: false
+ },
+ {
+ description: 'map with extra undefined key is not equal #1',
+ value1: map({}),
+ value2: map({foo: undefined}),
+ equal: false
+ },
+ {
+ description: 'map with extra undefined key is not equal #2',
+ value1: map({foo: undefined}),
+ value2: map({}),
+ equal: false
+ },
+ {
+ description: 'maps with extra undefined keys are not equal #3',
+ value1: map({foo: undefined}),
+ value2: map({bar: undefined}),
+ equal: false
+ },
+ {
+ description: 'null and empty map are not equal',
+ value1: null,
+ value2: new Map,
+ equal: false
+ },
+ {
+ description: 'undefined and empty map are not equal',
+ value1: undefined,
+ value2: new Map,
+ equal: false
+ },
+ {
+ description: 'map and a pseudo map are not equal',
+ value1: map({}),
+ value2: {
+ constructor: Map,
+ size: 0,
+ has: () => true,
+ get: () => 1,
+ },
+ equal: false
+ },
+ ]
+ },
+
+ {
+ description: 'Sets',
+ tests: [
+ {
+ description: 'empty sets are equal',
+ value1: new Set,
+ value2: new Set,
+ equal: true
+ },
+ {
+ description: 'empty sets of different class are not equal',
+ value1: new Set,
+ value2: new MySet,
+ equal: false
+ },
+ {
+ description: 'equal sets (same value "order")',
+ value1: set(['a', 'b']),
+ value2: set(['a', 'b']),
+ equal: true
+ },
+ {
+ description: 'not equal sets (same value "order" - instances of different classes)',
+ value1: set(['a', 'b']),
+ value2: mySet(['a', 'b']),
+ equal: false
+ },
+ {
+ description: 'equal sets (different value "order")',
+ value1: set(['a', 'b']),
+ value2: set(['b', 'a']),
+ equal: true
+ },
+ {
+ description: 'equal sets (different value "order" - instances of the same subclass)',
+ value1: mySet(['a', 'b']),
+ value2: mySet(['b', 'a']),
+ equal: true
+ },
+ {
+ description: 'not equal sets (extra value)',
+ value1: set(['a', 'b']),
+ value2: set(['a', 'b', 'c']),
+ equal: false
+ },
+ {
+ description: 'not equal sets (different values)',
+ value1: set(['a', 'b', 'c']),
+ value2: set(['a', 'b', 'd']),
+ equal: false
+ },
+ {
+ description: 'not equal sets (different instances of objects)',
+ value1: set([ 'a', {} ]),
+ value2: set([ 'a', {} ]),
+ equal: false
+ },
+ {
+ description: 'equal sets (same instances of objects)',
+ value1: set([ 'a', emptyObj ]),
+ value2: set([ 'a', emptyObj ]),
+ equal: true
+ },
+ {
+ description: 'empty set and empty object are not equal',
+ value1: {},
+ value2: new Set,
+ equal: false
+ },
+ {
+ description: 'empty set and empty array are not equal',
+ value1: [],
+ value2: new Set,
+ equal: false
+ },
+ {
+ description: 'set with extra undefined value is not equal #1',
+ value1: set([]),
+ value2: set([undefined]),
+ equal: false
+ },
+ {
+ description: 'set with extra undefined value is not equal #2',
+ value1: set([undefined]),
+ value2: set([]),
+ equal: false
+ },
+ {
+ description: 'set and pseudo set are not equal',
+ value1: new Set,
+ value2: {
+ constructor: Set,
+ size: 0,
+ has: () => true,
+ },
+ equal: false
+ },
+ ]
+ },
+
+ {
+ description: 'Typed arrays',
+ tests: [
+ {
+ description: 'two empty arrays of the same class are equal',
+ value1: new Int32Array([]),
+ value2: new Int32Array([]),
+ equal: true
+ },
+ {
+ description: 'two empty arrays of the different class are not equal',
+ value1: new Int32Array([]),
+ value2: new Int16Array([]),
+ equal: false
+ },
+ {
+ description: 'equal arrays',
+ value1: new Int32Array([1, 2, 3]),
+ value2: new Int32Array([1, 2, 3]),
+ equal: true
+ },
+ {
+ description: 'equal BigUint64Array arrays',
+ value1: skipBigIntArray || new BigUint64Array(['1', '2', '3']),
+ value2: skipBigIntArray || new BigUint64Array(['1', '2', '3']),
+ equal: true,
+ skip: skipBigIntArray
+ },
+ {
+ description: 'not equal BigUint64Array arrays',
+ value1: skipBigIntArray || new BigUint64Array(['1', '2', '3']),
+ value2: skipBigIntArray || new BigUint64Array(['1', '2', '4']),
+ equal: false,
+ skip: skipBigIntArray
+ },
+ {
+ description: 'not equal arrays (same items, different class)',
+ value1: new Int32Array([1, 2, 3]),
+ value2: new Int16Array([1, 2, 3]),
+ equal: false
+ },
+ {
+ description: 'not equal arrays (different item)',
+ value1: new Int32Array([1, 2, 3]),
+ value2: new Int32Array([1, 2, 4]),
+ equal: false
+ },
+ {
+ description: 'not equal arrays (different length)',
+ value1: new Int32Array([1, 2, 3]),
+ value2: new Int32Array([1, 2]),
+ equal: false
+ },
+ {
+ description: 'pseudo array and equivalent typed array are not equal',
+ value1: {'0': 1, '1': 2, length: 2, constructor: Int32Array},
+ value2: new Int32Array([1, 2]),
+ equal: false
+ }
+ ]
+ }
+];
+
+testCases(equal, 'equal - standard tests', standard_tests);
+testCases(equal, 'equal - es6 tests', es6_tests);
diff --git a/__tests__/storybook.test.js b/__tests__/storybook.test.js
index 80d295c..2450126 100644
--- a/__tests__/storybook.test.js
+++ b/__tests__/storybook.test.js
@@ -1,3 +1,35 @@
-import initStoryShots from '@storybook/addon-storyshots';
+import React from 'react'
+import initStoryShots, { Stories2SnapsConverter } from '@storybook/addon-storyshots';
+import { create, act } from 'react-test-renderer';
+import "regenerator-runtime/runtime";
+import fetchMock from "jest-fetch-mock"
-initStoryShots();
+fetchMock.enableMocks()
+fetch.mockResponse(JSON.stringify({ drinks: [] }))
+
+const wait = (amount = 0) => new Promise(resolve => setTimeout(resolve, amount));
+
+const converter = new Stories2SnapsConverter();
+
+initStoryShots({
+ asyncJest: true,
+ test: async ({ story, context, done }) => {
+ let renderer;
+ act(() => {
+ // React.createElement() is important because of hooks [shouldn't call story.render() directly]
+ renderer = create(React.createElement(story.render), {
+ // Fix Portal / MUI Dialog issues
+ createNodeMock: (node) => document.createElement(node.type),
+ });
+ });
+
+ // Let one render cycle pass before rendering snapshot
+ await act(() => wait(0));
+
+ // save each snapshot to a different file (similar to "multiSnapshotWithOptions")
+ const snapshotFileName = converter.getSnapshotFileName(context);
+ expect(renderer).toMatchSpecificSnapshot(snapshotFileName);
+
+ done();
+ },
+});
diff --git a/dist/cjs/useFetch.js b/dist/cjs/useFetch.js
index f8b9b09..42bf617 100644
--- a/dist/cjs/useFetch.js
+++ b/dist/cjs/useFetch.js
@@ -41,12 +41,15 @@ function useFetch(q, defaultOptions, _ref) {
return (0, _debounce["default"])(function (s) {
var optionsReq = getOptions(s, defaultOptions);
- setFetching(true);
- Promise.resolve(optionsReq).then(function (newOptions) {
- setOptions((0, _flattenOptions["default"])(filter(newOptions)(s)));
- })["finally"](function () {
- return setFetching(false);
- });
+
+ if (optionsReq) {
+ setFetching(true);
+ Promise.resolve(optionsReq).then(function (newOptions) {
+ setOptions((0, _flattenOptions["default"])(filter(newOptions)(s)));
+ })["finally"](function () {
+ return setFetching(false);
+ });
+ }
}, debounceTime);
}, [filterOptions, defaultOptions, getOptions, debounceTime]);
(0, _react.useEffect)(function () {
diff --git a/dist/esm/useFetch.js b/dist/esm/useFetch.js
index 98cd840..1ccc4a8 100644
--- a/dist/esm/useFetch.js
+++ b/dist/esm/useFetch.js
@@ -17,10 +17,13 @@ export default function useFetch(q, defaultOptions, {
return debounce(s => {
const optionsReq = getOptions(s, defaultOptions);
- setFetching(true);
- Promise.resolve(optionsReq).then(newOptions => {
- setOptions(flattenOptions(filter(newOptions)(s)));
- }).finally(() => setFetching(false));
+
+ if (optionsReq) {
+ setFetching(true);
+ Promise.resolve(optionsReq).then(newOptions => {
+ setOptions(flattenOptions(filter(newOptions)(s)));
+ }).finally(() => setFetching(false));
+ }
}, debounceTime);
}, [filterOptions, defaultOptions, getOptions, debounceTime]);
useEffect(() => setOptions(defaultOptions), [defaultOptions]);
diff --git a/package-lock.json b/package-lock.json
index 6975b5d..1f6b513 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,6 +24,7 @@
"@storybook/addon-storysource": "^6.2.9",
"@storybook/addons": "^6.2.9",
"@storybook/react": "^6.2.9",
+ "assert": "^1.5.0",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
@@ -38,6 +39,7 @@
"fuse.js": "^3.4.5",
"identity-obj-proxy": "^3.0.0",
"jest": "^26.6.3",
+ "jest-fetch-mock": "^3.0.3",
"pretty": "^2.0.0",
"prop-types": "^15.7.2",
"react": "^17.0.1",
@@ -9855,6 +9857,15 @@
"react": "^0.14.0 || ^15.0.0 || ^16.0.0"
}
},
+ "node_modules/cross-fetch": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz",
+ "integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==",
+ "dev": true,
+ "dependencies": {
+ "node-fetch": "2.6.1"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -12413,6 +12424,7 @@
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"dev": true,
+ "hasInstallScript": true,
"optional": true,
"os": [
"darwin"
@@ -12468,7 +12480,7 @@
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.6.1.tgz",
"integrity": "sha512-hT9yh/tiinkmirKrlv4KWOjztdoZo1mx9Qh4KvWqC7isoXwdUY3PNWUxceF4/qO9R6riA2C29jdTOeQOIROjgw==",
- "devOptional": true,
+ "dev": true,
"engines": {
"node": ">=6"
}
@@ -15157,6 +15169,16 @@
"node": ">= 10.14.2"
}
},
+ "node_modules/jest-fetch-mock": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz",
+ "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==",
+ "dev": true,
+ "dependencies": {
+ "cross-fetch": "^3.0.4",
+ "promise-polyfill": "^8.1.3"
+ }
+ },
"node_modules/jest-get-type": {
"version": "26.3.0",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz",
@@ -20423,6 +20445,12 @@
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
"dev": true
},
+ "node_modules/promise-polyfill": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.0.tgz",
+ "integrity": "sha512-k/TC0mIcPVF6yHhUvwAp7cvL6I2fFV7TzF1DuGPI8mBh4QQazf36xCKEHKTZKRysEoTQoQdKyP25J8MPJp7j5g==",
+ "dev": true
+ },
"node_modules/promise.allsettled": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.4.tgz",
@@ -28768,6 +28796,7 @@
"integrity": "sha512-qOtwgiqI3LMqT0eXYNV6ykp7qSu0LQGeXxy3wOBGuDDqAizfgnAjomYEWGFcyKp5ahV7HCRCjxbixAklFPUmyw==",
"dev": true,
"requires": {
+ "@babel/core": "^7.12.10",
"@babel/generator": "^7.12.11",
"@babel/parser": "^7.12.11",
"@babel/plugin-transform-react-jsx": "^7.12.12",
@@ -33351,6 +33380,15 @@
"warning": "^4.0.3"
}
},
+ "cross-fetch": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz",
+ "integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==",
+ "dev": true,
+ "requires": {
+ "node-fetch": "2.6.1"
+ }
+ },
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -35525,7 +35563,7 @@
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.6.1.tgz",
"integrity": "sha512-hT9yh/tiinkmirKrlv4KWOjztdoZo1mx9Qh4KvWqC7isoXwdUY3PNWUxceF4/qO9R6riA2C29jdTOeQOIROjgw==",
- "devOptional": true
+ "dev": true
},
"gauge": {
"version": "2.7.4",
@@ -37720,6 +37758,16 @@
"jest-util": "^26.6.2"
}
},
+ "jest-fetch-mock": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz",
+ "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==",
+ "dev": true,
+ "requires": {
+ "cross-fetch": "^3.0.4",
+ "promise-polyfill": "^8.1.3"
+ }
+ },
"jest-get-type": {
"version": "26.3.0",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz",
@@ -41819,6 +41867,12 @@
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
"dev": true
},
+ "promise-polyfill": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.0.tgz",
+ "integrity": "sha512-k/TC0mIcPVF6yHhUvwAp7cvL6I2fFV7TzF1DuGPI8mBh4QQazf36xCKEHKTZKRysEoTQoQdKyP25J8MPJp7j5g==",
+ "dev": true
+ },
"promise.allsettled": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.4.tgz",
diff --git a/package.json b/package.json
index a85db58..6fb0801 100644
--- a/package.json
+++ b/package.json
@@ -55,6 +55,7 @@
"@storybook/addon-storysource": "^6.2.9",
"@storybook/addons": "^6.2.9",
"@storybook/react": "^6.2.9",
+ "assert": "^1.5.0",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
@@ -69,6 +70,7 @@
"fuse.js": "^3.4.5",
"identity-obj-proxy": "^3.0.0",
"jest": "^26.6.3",
+ "jest-fetch-mock": "^3.0.3",
"pretty": "^2.0.0",
"prop-types": "^15.7.2",
"react": "^17.0.1",
diff --git a/src/Components/OptionsList.jsx b/src/Components/OptionsList.jsx
index 70c80ec..65de229 100644
--- a/src/Components/OptionsList.jsx
+++ b/src/Components/OptionsList.jsx
@@ -49,6 +49,11 @@ const OptionsList = ({
);
+OptionsList.defaultProps = {
+ renderOption: null,
+ renderGroupHeader: null,
+};
+
OptionsList.propTypes = {
options: PropTypes.arrayOf(optionType).isRequired,
optionProps: PropTypes.shape({}).isRequired,
diff --git a/src/lib/equal.js b/src/lib/equal.js
new file mode 100644
index 0000000..ad28f93
--- /dev/null
+++ b/src/lib/equal.js
@@ -0,0 +1,69 @@
+/* eslint-disable no-restricted-syntax */
+// eslint-disable-next-line no-unused-vars, no-var
+var envHasBigInt64Array = typeof BigInt64Array !== 'undefined';
+
+module.exports = function equal(a, b) {
+ if (a === b) return true;
+
+ if (a && b && typeof a === 'object' && typeof b === 'object') {
+ if (a.constructor !== b.constructor) return false;
+
+ let length; let i;
+ if (Array.isArray(a)) {
+ length = a.length;
+ // eslint-disable-next-line eqeqeq
+ if (length != b.length) return false;
+ // eslint-disable-next-line no-plusplus
+ for (i = length; i-- !== 0;) if (!equal(a[i], b[i])) return false;
+ return true;
+ }
+
+ if ((a instanceof Map) && (b instanceof Map)) {
+ if (a.size !== b.size) return false;
+ for (i of a.entries()) if (!b.has(i[0])) return false;
+ for (i of a.entries()) if (!equal(i[1], b.get(i[0]))) return false;
+ return true;
+ }
+
+ if ((a instanceof Set) && (b instanceof Set)) {
+ if (a.size !== b.size) return false;
+ for (i of a.entries()) if (!b.has(i[0])) return false;
+ return true;
+ }
+
+ if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
+ length = a.length;
+ // eslint-disable-next-line eqeqeq
+ if (length != b.length) return false;
+ // eslint-disable-next-line no-plusplus
+ for (i = length; i-- !== 0;) if (a[i] !== b[i]) return false;
+ return true;
+ }
+
+ if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags;
+ if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf();
+ if (a.toString !== Object.prototype.toString) return a.toString() === b.toString();
+
+ const keys = Object.keys(a);
+ length = keys.length;
+ if (length !== Object.keys(b).length) return false;
+
+ // eslint-disable-next-line no-plusplus
+ for (i = length; i-- !== 0;) {
+ if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false;
+ }
+
+ // eslint-disable-next-line no-plusplus
+ for (i = length; i-- !== 0;) {
+ const key = keys[i];
+
+ if (!equal(a[key], b[key])) return false;
+ }
+
+ return true;
+ }
+
+ // true if both NaN, false otherwise
+ // eslint-disable-next-line no-self-compare
+ return a !== a && b !== b;
+};
diff --git a/src/lib/getOptions.js b/src/lib/getOptions.js
index 97d5415..9ae1bb6 100644
--- a/src/lib/getOptions.js
+++ b/src/lib/getOptions.js
@@ -26,6 +26,7 @@ export default function getOptions(value, oldValue, options, multiple) {
}
newOptions.forEach((newOption) => {
+ // eslint-disable-next-line eqeqeq
const optionIndex = oldOptions.findIndex((o) => o.value == newOption.value);
if (optionIndex >= 0) {
diff --git a/src/useFetch.js b/src/useFetch.js
index 0cff3af..8fe0a87 100644
--- a/src/useFetch.js
+++ b/src/useFetch.js
@@ -18,14 +18,15 @@ export default function useFetch(q, defaultOptions, {
return debounce((s) => {
const optionsReq = getOptions(s, defaultOptions);
+ if (optionsReq) {
+ setFetching(true);
- setFetching(true);
-
- Promise.resolve(optionsReq)
- .then((newOptions) => {
- setOptions(flattenOptions(filter(newOptions)(s)));
- })
- .finally(() => setFetching(false));
+ Promise.resolve(optionsReq)
+ .then((newOptions) => {
+ setOptions(flattenOptions(filter(newOptions)(s)));
+ })
+ .finally(() => setFetching(false));
+ }
}, debounceTime);
}, [filterOptions, defaultOptions, getOptions, debounceTime]);
diff --git a/src/useSelect.js b/src/useSelect.js
index 28bcf6f..42015a3 100644
--- a/src/useSelect.js
+++ b/src/useSelect.js
@@ -10,6 +10,7 @@ import getDisplayValue from './lib/getDisplayValue';
import useFetch from './useFetch';
import getValues from './lib/getValues';
import useHighlight from './useHighlight';
+import equal from './lib/equal';
export default function useSelect({
value: defaultValue = null,
@@ -98,18 +99,19 @@ export default function useSelect({
}), [onMouseDown]);
useEffect(() => {
- if (valueRef.current === defaultValue) {
- return;
- }
-
- valueRef.current = defaultValue;
-
- setValue(getOptions(
+ const newValue = getOptions(
defaultValue,
null,
options,
multiple,
- ));
+ );
+
+ if (equal(valueRef.current, newValue)) {
+ return;
+ }
+
+ valueRef.current = newValue;
+ setValue(newValue);
}, [defaultValue, multiple, options]);
return [snapshot, valueProps, optionProps, setValue];
diff --git a/stories/4-Async.stories.js b/stories/4-Async.stories.js
index 1898524..6367d24 100644
--- a/stories/4-Async.stories.js
+++ b/stories/4-Async.stories.js
@@ -1,3 +1,4 @@
+import { useState, useEffect } from 'react';
import SelectSearch from '../src';
import '../style.css';
import { countries, fontStacks } from './data';
@@ -42,3 +43,70 @@ export const FetchMultiple = () => (
placeholder="Your favorite drink"
/>
);
+
+export const ControlledFetch = () => {
+ const [options, setOptions] = useState([])
+ const [value, setValue] = useState()
+ const [valueOption, setValueOption] = useState()
+ const [query, setQuery] = useState('')
+
+ useEffect(() => {
+ fetch(`https://www.thecocktaildb.com/api/json/v1/1/search.php?s=${query}`)
+ .then(response => response.json())
+ .then(({ drinks }) => {
+ const newOptions = drinks.map(({ idDrink, strDrink }) => ({ value: idDrink, name: strDrink }))
+ if (valueOption) newOptions.unshift(valueOption)
+ setOptions(newOptions)
+ })
+ .catch((e) => console.error(e));
+ }, [query])
+
+ return(
+ {
+ setValue(value)
+ setValueOption(options.find((o) => value === o.value))
+ }}
+ getOptions={(query) => {
+ setQuery(query)
+ }}
+ search
+ placeholder="Your favorite drink"
+ />
+ )
+};
+
+export const ControlledFetchMultiple = () => {
+ const [options, setOptions] = useState([])
+ const [value, setValue] = useState([])
+ const [valueOptions, setValueOptions] = useState([])
+ const [query, setQuery] = useState('')
+
+ useEffect(() => {
+ fetch(`https://www.thecocktaildb.com/api/json/v1/1/search.php?s=${query}`)
+ .then(response => response.json())
+ .then(({ drinks }) => {
+ setOptions([...valueOptions, ...drinks.map(({ idDrink, strDrink }) => ({ value: idDrink, name: strDrink }))])
+ })
+ .catch((e) => console.error(e));
+ }, [query])
+
+ return(
+ {
+ setValue(value)
+ setValueOptions(options.filter((o) => value.includes(o.value)))
+ }}
+ multiple
+ getOptions={(query) => {
+ setQuery(query)
+ }}
+ search
+ placeholder="Your favorite drink"
+ />
+ )
+};
\ No newline at end of file