react-downscreen
Advanced tools
Comparing version 0.0.9 to 0.0.10
@@ -84,2 +84,3 @@ 'use strict'; | ||
highlightedIndex: null, | ||
inputValue: '', | ||
lastKey: null | ||
@@ -97,2 +98,109 @@ }; | ||
const useEffectAfterMount = (cb, deps) => { | ||
const justMounted = React.useRef(true); | ||
React.useEffect(() => { | ||
if (!justMounted.current) { | ||
return cb(); | ||
} | ||
justMounted.current = false; | ||
}, deps); | ||
}; | ||
const getFirstPossibleIndex = (itemsLength, map) => { | ||
for (let i = 0; i < itemsLength; i++) { | ||
if (map[i]) { | ||
return i; | ||
} | ||
} | ||
return null; | ||
}; | ||
const getLastPossibleIndex = (itemsLength, map) => { | ||
for (let i = itemsLength - 1; i >= 0; i--) { | ||
if (map[i]) { | ||
return i; | ||
} | ||
} | ||
return null; | ||
}; | ||
function _extends() { | ||
_extends = Object.assign || function (target) { | ||
for (var i = 1; i < arguments.length; i++) { | ||
var source = arguments[i]; | ||
for (var key in source) { | ||
if (Object.prototype.hasOwnProperty.call(source, key)) { | ||
target[key] = source[key]; | ||
} | ||
} | ||
} | ||
return target; | ||
}; | ||
return _extends.apply(this, arguments); | ||
} | ||
// TODO: clean up state management (useReducer), memoize more for better performance (useMemo) | ||
const Downscreen = (_ref) => { | ||
let { | ||
children, | ||
onSelect = () => {}, | ||
initial = initialState, | ||
itemsLength, | ||
id | ||
} = _ref, | ||
props = _objectWithoutProperties(_ref, ["children", "onSelect", "initial", "itemsLength", "id"]); | ||
const menuItemsRef = React.useRef({}); | ||
const [state, setState] = React.useState(initial); | ||
const value = React.useMemo(() => ({ | ||
state, | ||
setState, | ||
itemsLength, | ||
id, | ||
getMenuItemsRef: () => menuItemsRef | ||
}), [state, itemsLength, id]); | ||
useEffectAfterMount(() => { | ||
if (state.isOpen) { | ||
switch (state.lastKey) { | ||
case 'ArrowUp': | ||
{ | ||
setState(s => _objectSpread({}, s, { | ||
highlightedIndex: getLastPossibleIndex(itemsLength, menuItemsRef.current) | ||
})); | ||
break; | ||
} | ||
case 'ArrowDown': | ||
{ | ||
setState(s => _objectSpread({}, s, { | ||
highlightedIndex: getFirstPossibleIndex(itemsLength, menuItemsRef.current), | ||
lastKey: 'ArrowDown' | ||
})); | ||
break; | ||
} | ||
} | ||
} | ||
}, [state.isOpen]); | ||
useEffectAfterMount(() => { | ||
onSelect(state.selectedIndex); | ||
}, [state.selectedIndex]); | ||
return React.createElement(DownscreenContext.Provider, { | ||
value: value | ||
}, React.createElement("div", _extends({ | ||
id: id, | ||
role: "combobox", | ||
"aria-haspopup": "listbox", | ||
"aria-expanded": state.isOpen, | ||
"aria-controls": state.isOpen ? `${id}-menu` : undefined, | ||
"aria-owns": state.isOpen ? `${id}-menu` : undefined, | ||
"aria-labelledby": `${id}-label` | ||
}, props), children)); | ||
}; | ||
const normalizeArrowKey = event => { | ||
@@ -111,12 +219,2 @@ const { | ||
const getFirstPossibleIndex = (itemsLength, map) => { | ||
for (let i = 0; i < itemsLength; i++) { | ||
if (map[i]) { | ||
return i; | ||
} | ||
} | ||
return null; | ||
}; | ||
const getNextIndex = (currentIndex, itemsLength, map) => { | ||
@@ -141,13 +239,7 @@ for (let i = currentIndex === null ? 0 : currentIndex + 1; i < itemsLength; i++) { | ||
for (let i = itemsLength - 1; i >= 0; i--) { | ||
if (map[i]) { | ||
return i; | ||
} | ||
} | ||
return getFirstPossibleIndex(itemsLength, map); | ||
return getLastPossibleIndex(itemsLength, map); | ||
}; | ||
function _extends() { | ||
_extends = Object.assign || function (target) { | ||
function _extends$1() { | ||
_extends$1 = Object.assign || function (target) { | ||
for (var i = 1; i < arguments.length; i++) { | ||
@@ -166,3 +258,3 @@ var source = arguments[i]; | ||
return _extends.apply(this, arguments); | ||
return _extends$1.apply(this, arguments); | ||
} | ||
@@ -187,3 +279,3 @@ | ||
})); | ||
}, [state.highlightedIndex, state.isOpen]); | ||
}, []); | ||
const onKeyUp = React.useCallback(event => { | ||
@@ -279,6 +371,4 @@ event.preventDefault(); | ||
}, []); | ||
const buttonChildren = React.useMemo(() => children({ | ||
selectedIndex: state.selectedIndex | ||
}), [state.selectedIndex]); | ||
return React.createElement("button", _extends({ | ||
const buttonChildren = React.useMemo(() => children(state.selectedIndex), [state.selectedIndex]); | ||
return React.createElement("button", _extends$1({ | ||
"aria-label": state.isOpen ? 'close menu' : 'open menu', | ||
@@ -297,15 +387,4 @@ "aria-haspopup": true, | ||
const useEffectAfterMount = (cb, deps) => { | ||
const justMounted = React.useRef(true); | ||
React.useEffect(() => { | ||
if (!justMounted.current) { | ||
return cb(); | ||
} | ||
justMounted.current = false; | ||
}, deps); | ||
}; | ||
function _extends$1() { | ||
_extends$1 = Object.assign || function (target) { | ||
function _extends$2() { | ||
_extends$2 = Object.assign || function (target) { | ||
for (var i = 1; i < arguments.length; i++) { | ||
@@ -324,19 +403,12 @@ var source = arguments[i]; | ||
return _extends$1.apply(this, arguments); | ||
return _extends$2.apply(this, arguments); | ||
} | ||
// TODO: clean up state management (useReducer), memoize more for better performance (useMemo) | ||
const Downscreen = (_ref) => { | ||
const Input = (_ref) => { | ||
let { | ||
children, | ||
onChange, | ||
initial = initialState, | ||
itemsLength, | ||
id | ||
children | ||
} = _ref, | ||
props = _objectWithoutProperties(_ref, ["children", "onChange", "initial", "itemsLength", "id"]); | ||
props = _objectWithoutProperties(_ref, ["children"]); | ||
const menuItemsRef = React.useRef({}); | ||
const [state, setState] = React.useState(initial); | ||
const value = React.useMemo(() => ({ | ||
const { | ||
state, | ||
@@ -346,51 +418,115 @@ setState, | ||
id, | ||
getMenuItemsRef: () => menuItemsRef | ||
}), [state, itemsLength, id]); | ||
useEffectAfterMount(() => { | ||
if (state.isOpen) { | ||
setState(s => _objectSpread({}, s, { | ||
highlightedIndex: null | ||
})); | ||
} | ||
}, [itemsLength]); | ||
useEffectAfterMount(() => { | ||
if (state.isOpen) { | ||
switch (state.lastKey) { | ||
case 'ArrowUp': | ||
{ | ||
getMenuItemsRef | ||
} = React.useContext(DownscreenContext); | ||
const onChange = React.useCallback(event => { | ||
const { | ||
target: { | ||
value: inputValue | ||
} | ||
} = event; | ||
setState(s => _objectSpread({}, s, { | ||
isOpen: true, | ||
highlightedIndex: null, | ||
inputValue | ||
})); | ||
}, []); | ||
const onBlur = React.useCallback(() => { | ||
setState(s => _objectSpread({}, s, { | ||
isOpen: false | ||
})); | ||
}, []); | ||
const onKeyDown = React.useCallback(event => { | ||
const key = normalizeArrowKey(event); | ||
switch (key) { | ||
case 'Enter': | ||
{ | ||
if (state.isOpen) { | ||
event.preventDefault(); | ||
setState(s => _objectSpread({}, s, { | ||
highlightedIndex: getPreviousIndex(getFirstPossibleIndex(itemsLength, menuItemsRef.current), itemsLength, menuItemsRef.current) | ||
isOpen: false, | ||
selectedIndex: s.highlightedIndex, | ||
highlightedIndex: null, | ||
lastKey: 'Enter' | ||
})); | ||
break; | ||
} | ||
case 'ArrowDown': | ||
{ | ||
break; | ||
} | ||
case 'Escape': | ||
{ | ||
event.preventDefault(); | ||
setState(s => _objectSpread({}, s, { | ||
isOpen: false, | ||
highlightedIndex: null, | ||
lastKey: 'Escape' | ||
})); | ||
break; | ||
} | ||
case 'End': | ||
case 'ArrowUp': | ||
{ | ||
event.preventDefault(); | ||
if (state.isOpen) { | ||
setState(s => _objectSpread({}, s, { | ||
highlightedIndex: getFirstPossibleIndex(itemsLength, menuItemsRef.current), | ||
highlightedIndex: getPreviousIndex(state.highlightedIndex, itemsLength, getMenuItemsRef().current), | ||
lastKey: 'ArrowUp' | ||
})); | ||
} else { | ||
setState(s => _objectSpread({}, s, { | ||
isOpen: true, | ||
lastKey: 'ArrowUp' | ||
})); | ||
} | ||
break; | ||
} | ||
case 'Home': | ||
case 'ArrowDown': | ||
{ | ||
event.preventDefault(); | ||
if (state.isOpen) { | ||
setState(s => _objectSpread({}, s, { | ||
highlightedIndex: getNextIndex(state.highlightedIndex, itemsLength, getMenuItemsRef().current), | ||
lastKey: 'ArrowDown' | ||
})); | ||
break; | ||
} else { | ||
setState(s => _objectSpread({}, s, { | ||
isOpen: true, | ||
lastKey: 'ArrowDown' | ||
})); | ||
} | ||
} | ||
break; | ||
} | ||
} | ||
}, [state.isOpen]); | ||
}, [itemsLength, state.highlightedIndex, state.isOpen]); | ||
const inputChildren = React.useMemo(() => children(state.selectedIndex), [state.selectedIndex]); | ||
useEffectAfterMount(() => { | ||
onChange(state.selectedIndex); | ||
}, [state.selectedIndex]); | ||
return React.createElement(DownscreenContext.Provider, { | ||
value: value | ||
}, React.createElement("div", _extends$1({ | ||
id: id, | ||
role: "combobox", | ||
"aria-haspopup": "listbox", | ||
"aria-expanded": state.isOpen, | ||
setState(s => _objectSpread({}, s, { | ||
inputValue: typeof inputChildren === 'string' ? inputChildren : s.inputValue | ||
})); | ||
}, [inputChildren]); | ||
return React.createElement("input", _extends$2({ | ||
"aria-autocomplete": "list", | ||
"aria-activedescendant": state.isOpen && state.highlightedIndex !== null ? `${id}-menu-item-${state.highlightedIndex}` : undefined, | ||
"aria-controls": state.isOpen ? `${id}-menu` : undefined, | ||
"aria-owns": state.isOpen ? `${id}-menu` : undefined, | ||
"aria-labelledby": `${id}-label` | ||
}, props), children)); | ||
"aria-labelledby": `${id}-label`, | ||
autoComplete: "off", | ||
id: `${id}-input`, | ||
type: "text", | ||
value: state.inputValue, | ||
onChange: onChange, | ||
onBlur: onBlur, | ||
onKeyDown: onKeyDown | ||
}, props)); | ||
}; | ||
function _extends$2() { | ||
_extends$2 = Object.assign || function (target) { | ||
function _extends$3() { | ||
_extends$3 = Object.assign || function (target) { | ||
for (var i = 1; i < arguments.length; i++) { | ||
@@ -409,5 +545,33 @@ var source = arguments[i]; | ||
return _extends$2.apply(this, arguments); | ||
return _extends$3.apply(this, arguments); | ||
} | ||
const Label = props => { | ||
const { | ||
id | ||
} = React.useContext(DownscreenContext); | ||
return React.createElement("label", _extends$3({ | ||
id: `${id}-label`, | ||
htmlFor: `${id}-input` | ||
}, props)); | ||
}; | ||
function _extends$4() { | ||
_extends$4 = Object.assign || function (target) { | ||
for (var i = 1; i < arguments.length; i++) { | ||
var source = arguments[i]; | ||
for (var key in source) { | ||
if (Object.prototype.hasOwnProperty.call(source, key)) { | ||
target[key] = source[key]; | ||
} | ||
} | ||
} | ||
return target; | ||
}; | ||
return _extends$4.apply(this, arguments); | ||
} | ||
const Menu = (_ref) => { | ||
@@ -429,5 +593,6 @@ let { | ||
selectedIndex: state.selectedIndex, | ||
highlightedIndex: state.highlightedIndex | ||
})), [state.selectedIndex, state.highlightedIndex]); | ||
return state.isOpen ? React.createElement("div", _extends$2({ | ||
highlightedIndex: state.highlightedIndex, | ||
inputValue: state.inputValue | ||
})), [state.selectedIndex, state.highlightedIndex, state.inputValue]); | ||
return state.isOpen ? React.createElement("div", _extends$4({ | ||
id: `${id}-menu`, | ||
@@ -438,4 +603,4 @@ role: "listbox" | ||
function _extends$3() { | ||
_extends$3 = Object.assign || function (target) { | ||
function _extends$5() { | ||
_extends$5 = Object.assign || function (target) { | ||
for (var i = 1; i < arguments.length; i++) { | ||
@@ -454,3 +619,3 @@ var source = arguments[i]; | ||
return _extends$3.apply(this, arguments); | ||
return _extends$5.apply(this, arguments); | ||
} | ||
@@ -513,3 +678,3 @@ | ||
}, []); | ||
return React.createElement("div", _extends$3({ | ||
return React.createElement("div", _extends$5({ | ||
ref: scrollRef, | ||
@@ -526,4 +691,6 @@ "aria-selected": state.highlightedIndex === index, | ||
exports.Button = Button; | ||
exports.Input = Input; | ||
exports.Label = Label; | ||
exports.Menu = Menu; | ||
exports.MenuItem = MenuItem; | ||
exports.default = Downscreen; |
@@ -24,3 +24,3 @@ function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } | ||
})); | ||
}, [state.highlightedIndex, state.isOpen]); | ||
}, []); | ||
const onKeyUp = React.useCallback(event => { | ||
@@ -116,5 +116,3 @@ event.preventDefault(); | ||
}, []); | ||
const buttonChildren = React.useMemo(() => children({ | ||
selectedIndex: state.selectedIndex | ||
}), [state.selectedIndex]); | ||
const buttonChildren = React.useMemo(() => children(state.selectedIndex), [state.selectedIndex]); | ||
return React.createElement("button", _extends({ | ||
@@ -121,0 +119,0 @@ "aria-label": state.isOpen ? 'close menu' : 'open menu', |
@@ -7,3 +7,3 @@ function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } | ||
import getFirstPossibleIndex from "./getFirstPossibleIndex.js"; | ||
import getPreviousIndex from "./getPreviousIndex.js"; // TODO: extract custom hooks, break them down by domain | ||
import getLastPossibleIndex from "./getLastPossibleIndex.js"; // TODO: extract custom hooks, break them down by domain | ||
// TODO: clean up state management (useReducer), memoize more for better performance (useMemo) | ||
@@ -13,3 +13,3 @@ | ||
children, | ||
onChange, | ||
onSelect = () => {}, | ||
initial = initialState, | ||
@@ -31,9 +31,2 @@ itemsLength, | ||
if (state.isOpen) { | ||
setState(s => ({ ...s, | ||
highlightedIndex: null | ||
})); | ||
} | ||
}, [itemsLength]); | ||
useEffectAfterMount(() => { | ||
if (state.isOpen) { | ||
switch (state.lastKey) { | ||
@@ -43,3 +36,3 @@ case 'ArrowUp': | ||
setState(s => ({ ...s, | ||
highlightedIndex: getPreviousIndex(getFirstPossibleIndex(itemsLength, menuItemsRef.current), itemsLength, menuItemsRef.current) | ||
highlightedIndex: getLastPossibleIndex(itemsLength, menuItemsRef.current) | ||
})); | ||
@@ -61,3 +54,3 @@ break; | ||
useEffectAfterMount(() => { | ||
onChange(state.selectedIndex); | ||
onSelect(state.selectedIndex); | ||
}, [state.selectedIndex]); | ||
@@ -64,0 +57,0 @@ return React.createElement(DownscreenContext.Provider, { |
@@ -6,2 +6,3 @@ import * as React from 'react'; | ||
highlightedIndex: null, | ||
inputValue: '', | ||
lastKey: null | ||
@@ -8,0 +9,0 @@ }; |
@@ -1,2 +0,2 @@ | ||
import getFirstPossibleIndex from "./getFirstPossibleIndex.js"; | ||
import getLastPossibleIndex from "./getLastPossibleIndex.js"; | ||
@@ -12,11 +12,5 @@ const getPreviousIndex = (currentIndex, itemsLength, map) => { | ||
for (let i = itemsLength - 1; i >= 0; i--) { | ||
if (map[i]) { | ||
return i; | ||
} | ||
} | ||
return getFirstPossibleIndex(itemsLength, map); | ||
return getLastPossibleIndex(itemsLength, map); | ||
}; | ||
export default getPreviousIndex; |
@@ -0,4 +1,6 @@ | ||
export { default } from "./Downscreen.js"; | ||
export { default as Button } from "./Button.js"; | ||
export { default } from "./Downscreen.js"; | ||
export { default as Input } from "./Input.js"; | ||
export { default as Label } from "./Label.js"; | ||
export { default as Menu } from "./Menu.js"; | ||
export { default as MenuItem } from "./MenuItem.js"; |
@@ -20,4 +20,5 @@ function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } | ||
selectedIndex: state.selectedIndex, | ||
highlightedIndex: state.highlightedIndex | ||
})), [state.selectedIndex, state.highlightedIndex]); | ||
highlightedIndex: state.highlightedIndex, | ||
inputValue: state.inputValue | ||
})), [state.selectedIndex, state.highlightedIndex, state.inputValue]); | ||
return state.isOpen ? React.createElement("div", _extends({ | ||
@@ -24,0 +25,0 @@ id: `${id}-menu`, |
import * as React from 'react'; | ||
import { State } from './DownscreenContext'; | ||
declare type Props = React.HTMLAttributes<HTMLButtonElement> & { | ||
children: (args: { | ||
selectedIndex: State['selectedIndex']; | ||
}) => React.ReactNode; | ||
children: (selectedIndex: State['selectedIndex']) => React.ReactNode; | ||
}; | ||
declare const Button: ({ children, ...props }: Props) => JSX.Element; | ||
export default Button; |
@@ -5,3 +5,3 @@ import * as React from 'react'; | ||
children: React.ReactNode; | ||
onChange: (selectedIndex: State['selectedIndex']) => void; | ||
onSelect: (selectedIndex: State['selectedIndex']) => void; | ||
initial?: State; | ||
@@ -11,3 +11,3 @@ itemsLength: number; | ||
}; | ||
declare const Downscreen: ({ children, onChange, initial, itemsLength, id, ...props }: Props) => JSX.Element; | ||
declare const Downscreen: ({ children, onSelect, initial, itemsLength, id, ...props }: Props) => JSX.Element; | ||
export default Downscreen; |
@@ -6,2 +6,3 @@ import * as React from 'react'; | ||
highlightedIndex: number | null; | ||
inputValue: string; | ||
lastKey: ' ' | 'Enter' | 'Escape' | 'ArrowUp' | 'ArrowDown' | null; | ||
@@ -16,2 +17,3 @@ } | ||
highlightedIndex: null; | ||
inputValue: string; | ||
lastKey: null; | ||
@@ -18,0 +20,0 @@ }; |
@@ -0,4 +1,6 @@ | ||
export { default } from './Downscreen'; | ||
export { default as Button } from './Button'; | ||
export { default } from './Downscreen'; | ||
export { default as Input } from './Input'; | ||
export { default as Label } from './Label'; | ||
export { default as Menu } from './Menu'; | ||
export { default as MenuItem } from './MenuItem'; |
@@ -8,2 +8,3 @@ import * as React from 'react'; | ||
highlightedIndex: State['highlightedIndex']; | ||
inputValue: State['inputValue']; | ||
}) => React.ReactNode; | ||
@@ -10,0 +11,0 @@ }; |
import * as React from 'react'; | ||
import { createContext, useContext, useCallback, useMemo, createElement, useRef, useEffect, useState } from 'react'; | ||
import { createContext, useRef, useEffect, useState, useMemo, createElement, useContext, useCallback } from 'react'; | ||
import scrollIntoViewIfNeeded from 'scroll-into-view-if-needed'; | ||
@@ -79,2 +79,3 @@ | ||
highlightedIndex: null, | ||
inputValue: '', | ||
lastKey: null | ||
@@ -92,2 +93,109 @@ }; | ||
const useEffectAfterMount = (cb, deps) => { | ||
const justMounted = useRef(true); | ||
useEffect(() => { | ||
if (!justMounted.current) { | ||
return cb(); | ||
} | ||
justMounted.current = false; | ||
}, deps); | ||
}; | ||
const getFirstPossibleIndex = (itemsLength, map) => { | ||
for (let i = 0; i < itemsLength; i++) { | ||
if (map[i]) { | ||
return i; | ||
} | ||
} | ||
return null; | ||
}; | ||
const getLastPossibleIndex = (itemsLength, map) => { | ||
for (let i = itemsLength - 1; i >= 0; i--) { | ||
if (map[i]) { | ||
return i; | ||
} | ||
} | ||
return null; | ||
}; | ||
function _extends() { | ||
_extends = Object.assign || function (target) { | ||
for (var i = 1; i < arguments.length; i++) { | ||
var source = arguments[i]; | ||
for (var key in source) { | ||
if (Object.prototype.hasOwnProperty.call(source, key)) { | ||
target[key] = source[key]; | ||
} | ||
} | ||
} | ||
return target; | ||
}; | ||
return _extends.apply(this, arguments); | ||
} | ||
// TODO: clean up state management (useReducer), memoize more for better performance (useMemo) | ||
const Downscreen = (_ref) => { | ||
let { | ||
children, | ||
onSelect = () => {}, | ||
initial = initialState, | ||
itemsLength, | ||
id | ||
} = _ref, | ||
props = _objectWithoutProperties(_ref, ["children", "onSelect", "initial", "itemsLength", "id"]); | ||
const menuItemsRef = useRef({}); | ||
const [state, setState] = useState(initial); | ||
const value = useMemo(() => ({ | ||
state, | ||
setState, | ||
itemsLength, | ||
id, | ||
getMenuItemsRef: () => menuItemsRef | ||
}), [state, itemsLength, id]); | ||
useEffectAfterMount(() => { | ||
if (state.isOpen) { | ||
switch (state.lastKey) { | ||
case 'ArrowUp': | ||
{ | ||
setState(s => _objectSpread({}, s, { | ||
highlightedIndex: getLastPossibleIndex(itemsLength, menuItemsRef.current) | ||
})); | ||
break; | ||
} | ||
case 'ArrowDown': | ||
{ | ||
setState(s => _objectSpread({}, s, { | ||
highlightedIndex: getFirstPossibleIndex(itemsLength, menuItemsRef.current), | ||
lastKey: 'ArrowDown' | ||
})); | ||
break; | ||
} | ||
} | ||
} | ||
}, [state.isOpen]); | ||
useEffectAfterMount(() => { | ||
onSelect(state.selectedIndex); | ||
}, [state.selectedIndex]); | ||
return createElement(DownscreenContext.Provider, { | ||
value: value | ||
}, createElement("div", _extends({ | ||
id: id, | ||
role: "combobox", | ||
"aria-haspopup": "listbox", | ||
"aria-expanded": state.isOpen, | ||
"aria-controls": state.isOpen ? "".concat(id, "-menu") : undefined, | ||
"aria-owns": state.isOpen ? "".concat(id, "-menu") : undefined, | ||
"aria-labelledby": "".concat(id, "-label") | ||
}, props), children)); | ||
}; | ||
const normalizeArrowKey = event => { | ||
@@ -106,12 +214,2 @@ const { | ||
const getFirstPossibleIndex = (itemsLength, map) => { | ||
for (let i = 0; i < itemsLength; i++) { | ||
if (map[i]) { | ||
return i; | ||
} | ||
} | ||
return null; | ||
}; | ||
const getNextIndex = (currentIndex, itemsLength, map) => { | ||
@@ -136,13 +234,7 @@ for (let i = currentIndex === null ? 0 : currentIndex + 1; i < itemsLength; i++) { | ||
for (let i = itemsLength - 1; i >= 0; i--) { | ||
if (map[i]) { | ||
return i; | ||
} | ||
} | ||
return getFirstPossibleIndex(itemsLength, map); | ||
return getLastPossibleIndex(itemsLength, map); | ||
}; | ||
function _extends() { | ||
_extends = Object.assign || function (target) { | ||
function _extends$1() { | ||
_extends$1 = Object.assign || function (target) { | ||
for (var i = 1; i < arguments.length; i++) { | ||
@@ -161,3 +253,3 @@ var source = arguments[i]; | ||
return _extends.apply(this, arguments); | ||
return _extends$1.apply(this, arguments); | ||
} | ||
@@ -182,3 +274,3 @@ | ||
})); | ||
}, [state.highlightedIndex, state.isOpen]); | ||
}, []); | ||
const onKeyUp = useCallback(event => { | ||
@@ -274,6 +366,4 @@ event.preventDefault(); | ||
}, []); | ||
const buttonChildren = useMemo(() => children({ | ||
selectedIndex: state.selectedIndex | ||
}), [state.selectedIndex]); | ||
return createElement("button", _extends({ | ||
const buttonChildren = useMemo(() => children(state.selectedIndex), [state.selectedIndex]); | ||
return createElement("button", _extends$1({ | ||
"aria-label": state.isOpen ? 'close menu' : 'open menu', | ||
@@ -292,15 +382,4 @@ "aria-haspopup": true, | ||
const useEffectAfterMount = (cb, deps) => { | ||
const justMounted = useRef(true); | ||
useEffect(() => { | ||
if (!justMounted.current) { | ||
return cb(); | ||
} | ||
justMounted.current = false; | ||
}, deps); | ||
}; | ||
function _extends$1() { | ||
_extends$1 = Object.assign || function (target) { | ||
function _extends$2() { | ||
_extends$2 = Object.assign || function (target) { | ||
for (var i = 1; i < arguments.length; i++) { | ||
@@ -319,19 +398,12 @@ var source = arguments[i]; | ||
return _extends$1.apply(this, arguments); | ||
return _extends$2.apply(this, arguments); | ||
} | ||
// TODO: clean up state management (useReducer), memoize more for better performance (useMemo) | ||
const Downscreen = (_ref) => { | ||
const Input = (_ref) => { | ||
let { | ||
children, | ||
onChange, | ||
initial = initialState, | ||
itemsLength, | ||
id | ||
children | ||
} = _ref, | ||
props = _objectWithoutProperties(_ref, ["children", "onChange", "initial", "itemsLength", "id"]); | ||
props = _objectWithoutProperties(_ref, ["children"]); | ||
const menuItemsRef = useRef({}); | ||
const [state, setState] = useState(initial); | ||
const value = useMemo(() => ({ | ||
const { | ||
state, | ||
@@ -341,51 +413,115 @@ setState, | ||
id, | ||
getMenuItemsRef: () => menuItemsRef | ||
}), [state, itemsLength, id]); | ||
useEffectAfterMount(() => { | ||
if (state.isOpen) { | ||
setState(s => _objectSpread({}, s, { | ||
highlightedIndex: null | ||
})); | ||
} | ||
}, [itemsLength]); | ||
useEffectAfterMount(() => { | ||
if (state.isOpen) { | ||
switch (state.lastKey) { | ||
case 'ArrowUp': | ||
{ | ||
getMenuItemsRef | ||
} = useContext(DownscreenContext); | ||
const onChange = useCallback(event => { | ||
const { | ||
target: { | ||
value: inputValue | ||
} | ||
} = event; | ||
setState(s => _objectSpread({}, s, { | ||
isOpen: true, | ||
highlightedIndex: null, | ||
inputValue | ||
})); | ||
}, []); | ||
const onBlur = useCallback(() => { | ||
setState(s => _objectSpread({}, s, { | ||
isOpen: false | ||
})); | ||
}, []); | ||
const onKeyDown = useCallback(event => { | ||
const key = normalizeArrowKey(event); | ||
switch (key) { | ||
case 'Enter': | ||
{ | ||
if (state.isOpen) { | ||
event.preventDefault(); | ||
setState(s => _objectSpread({}, s, { | ||
highlightedIndex: getPreviousIndex(getFirstPossibleIndex(itemsLength, menuItemsRef.current), itemsLength, menuItemsRef.current) | ||
isOpen: false, | ||
selectedIndex: s.highlightedIndex, | ||
highlightedIndex: null, | ||
lastKey: 'Enter' | ||
})); | ||
break; | ||
} | ||
case 'ArrowDown': | ||
{ | ||
break; | ||
} | ||
case 'Escape': | ||
{ | ||
event.preventDefault(); | ||
setState(s => _objectSpread({}, s, { | ||
isOpen: false, | ||
highlightedIndex: null, | ||
lastKey: 'Escape' | ||
})); | ||
break; | ||
} | ||
case 'End': | ||
case 'ArrowUp': | ||
{ | ||
event.preventDefault(); | ||
if (state.isOpen) { | ||
setState(s => _objectSpread({}, s, { | ||
highlightedIndex: getFirstPossibleIndex(itemsLength, menuItemsRef.current), | ||
highlightedIndex: getPreviousIndex(state.highlightedIndex, itemsLength, getMenuItemsRef().current), | ||
lastKey: 'ArrowUp' | ||
})); | ||
} else { | ||
setState(s => _objectSpread({}, s, { | ||
isOpen: true, | ||
lastKey: 'ArrowUp' | ||
})); | ||
} | ||
break; | ||
} | ||
case 'Home': | ||
case 'ArrowDown': | ||
{ | ||
event.preventDefault(); | ||
if (state.isOpen) { | ||
setState(s => _objectSpread({}, s, { | ||
highlightedIndex: getNextIndex(state.highlightedIndex, itemsLength, getMenuItemsRef().current), | ||
lastKey: 'ArrowDown' | ||
})); | ||
break; | ||
} else { | ||
setState(s => _objectSpread({}, s, { | ||
isOpen: true, | ||
lastKey: 'ArrowDown' | ||
})); | ||
} | ||
} | ||
break; | ||
} | ||
} | ||
}, [state.isOpen]); | ||
}, [itemsLength, state.highlightedIndex, state.isOpen]); | ||
const inputChildren = useMemo(() => children(state.selectedIndex), [state.selectedIndex]); | ||
useEffectAfterMount(() => { | ||
onChange(state.selectedIndex); | ||
}, [state.selectedIndex]); | ||
return createElement(DownscreenContext.Provider, { | ||
value: value | ||
}, createElement("div", _extends$1({ | ||
id: id, | ||
role: "combobox", | ||
"aria-haspopup": "listbox", | ||
"aria-expanded": state.isOpen, | ||
setState(s => _objectSpread({}, s, { | ||
inputValue: typeof inputChildren === 'string' ? inputChildren : s.inputValue | ||
})); | ||
}, [inputChildren]); | ||
return createElement("input", _extends$2({ | ||
"aria-autocomplete": "list", | ||
"aria-activedescendant": state.isOpen && state.highlightedIndex !== null ? "".concat(id, "-menu-item-").concat(state.highlightedIndex) : undefined, | ||
"aria-controls": state.isOpen ? "".concat(id, "-menu") : undefined, | ||
"aria-owns": state.isOpen ? "".concat(id, "-menu") : undefined, | ||
"aria-labelledby": "".concat(id, "-label") | ||
}, props), children)); | ||
"aria-labelledby": "".concat(id, "-label"), | ||
autoComplete: "off", | ||
id: "".concat(id, "-input"), | ||
type: "text", | ||
value: state.inputValue, | ||
onChange: onChange, | ||
onBlur: onBlur, | ||
onKeyDown: onKeyDown | ||
}, props)); | ||
}; | ||
function _extends$2() { | ||
_extends$2 = Object.assign || function (target) { | ||
function _extends$3() { | ||
_extends$3 = Object.assign || function (target) { | ||
for (var i = 1; i < arguments.length; i++) { | ||
@@ -404,5 +540,33 @@ var source = arguments[i]; | ||
return _extends$2.apply(this, arguments); | ||
return _extends$3.apply(this, arguments); | ||
} | ||
const Label = props => { | ||
const { | ||
id | ||
} = useContext(DownscreenContext); | ||
return createElement("label", _extends$3({ | ||
id: "".concat(id, "-label"), | ||
htmlFor: "".concat(id, "-input") | ||
}, props)); | ||
}; | ||
function _extends$4() { | ||
_extends$4 = Object.assign || function (target) { | ||
for (var i = 1; i < arguments.length; i++) { | ||
var source = arguments[i]; | ||
for (var key in source) { | ||
if (Object.prototype.hasOwnProperty.call(source, key)) { | ||
target[key] = source[key]; | ||
} | ||
} | ||
} | ||
return target; | ||
}; | ||
return _extends$4.apply(this, arguments); | ||
} | ||
const Menu = (_ref) => { | ||
@@ -424,5 +588,6 @@ let { | ||
selectedIndex: state.selectedIndex, | ||
highlightedIndex: state.highlightedIndex | ||
})), [state.selectedIndex, state.highlightedIndex]); | ||
return state.isOpen ? createElement("div", _extends$2({ | ||
highlightedIndex: state.highlightedIndex, | ||
inputValue: state.inputValue | ||
})), [state.selectedIndex, state.highlightedIndex, state.inputValue]); | ||
return state.isOpen ? createElement("div", _extends$4({ | ||
id: "".concat(id, "-menu"), | ||
@@ -433,4 +598,4 @@ role: "listbox" | ||
function _extends$3() { | ||
_extends$3 = Object.assign || function (target) { | ||
function _extends$5() { | ||
_extends$5 = Object.assign || function (target) { | ||
for (var i = 1; i < arguments.length; i++) { | ||
@@ -449,3 +614,3 @@ var source = arguments[i]; | ||
return _extends$3.apply(this, arguments); | ||
return _extends$5.apply(this, arguments); | ||
} | ||
@@ -508,3 +673,3 @@ | ||
}, []); | ||
return createElement("div", _extends$3({ | ||
return createElement("div", _extends$5({ | ||
ref: scrollRef, | ||
@@ -521,2 +686,2 @@ "aria-selected": state.highlightedIndex === index, | ||
export default Downscreen; | ||
export { Button, Menu, MenuItem }; | ||
export { Button, Input, Label, Menu, MenuItem }; |
{ | ||
"name": "react-downscreen", | ||
"description": "React Downscreen ⛹️", | ||
"version": "0.0.9", | ||
"version": "0.0.10", | ||
"license": "MIT", | ||
@@ -6,0 +6,0 @@ "files": [ |
57849
35
1866