@manifoldco/react-select-zero
Advanced tools
Comparing version 0.0.1-0 to 0.0.1-1
@@ -10,36 +10,2 @@ 'use strict'; | ||
function _defineProperty(obj, key, value) { | ||
if (key in obj) { | ||
Object.defineProperty(obj, key, { | ||
value: value, | ||
enumerable: true, | ||
configurable: true, | ||
writable: true | ||
}); | ||
} else { | ||
obj[key] = value; | ||
} | ||
return obj; | ||
} | ||
function _objectSpread(target) { | ||
for (var i = 1; i < arguments.length; i++) { | ||
var source = arguments[i] != null ? arguments[i] : {}; | ||
var ownKeys = Object.keys(source); | ||
if (typeof Object.getOwnPropertySymbols === 'function') { | ||
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { | ||
return Object.getOwnPropertyDescriptor(source, sym).enumerable; | ||
})); | ||
} | ||
ownKeys.forEach(function (key) { | ||
_defineProperty(target, key, source[key]); | ||
}); | ||
} | ||
return target; | ||
} | ||
function _objectWithoutPropertiesLoose(source, excluded) { | ||
@@ -81,27 +47,2 @@ if (source == null) return {}; | ||
function elId(name, component) { | ||
return `rsz-${name}-${component}`; | ||
} | ||
function optionId(name, option) { | ||
return elId(name, `option-${option}`); | ||
} | ||
var Action; | ||
(function (Action) { | ||
Action["APPEND"] = "APPEND"; | ||
Action["ADD_MULTI_CLICK"] = "ADD_MULTI_CLICK"; | ||
Action["ADD_MULTI_KEYBOARD"] = "ADD_MULTI_KEYBOARD"; | ||
Action["ADD_SINGLE_CLICK"] = "ADD_SINGLE_CLICK"; | ||
Action["ADD_SINGLE_KEYBOARD"] = "ADD_SINGLE_KEYBOARD"; | ||
Action["DROPDOWN_CLOSE"] = "DROPDOWN_CLOSE"; | ||
Action["DROPDOWN_OPEN"] = "DROPDOWN_OPEN"; | ||
Action["DROPDOWN_TOGGLE"] = "DROPDOWN_TOGGLE"; | ||
Action["INIT"] = "INIT"; | ||
Action["MOVE_NEXT"] = "MOVE_NEXT"; | ||
Action["MOVE_PREV"] = "MOVE_PREV"; | ||
Action["MOVE_TO_END"] = "MOVE_TO_END"; | ||
Action["MOVE_TO_START"] = "MOVE_TO_START"; | ||
Action["SEARCH"] = "SEARCH"; | ||
})(Action || (Action = {})); | ||
var KEY; | ||
@@ -118,289 +59,2 @@ | ||
const initialState = { | ||
activeDescendant: '0', | ||
created: [], | ||
isOpen: false, | ||
search: '', | ||
selected: [], | ||
visibleOptions: [] | ||
}; | ||
function reducer(state, action) { | ||
switch (action.type) { | ||
// Add or remove an option from mouse click | ||
case Action.ADD_MULTI_CLICK: | ||
{ | ||
const isNew = action.options.indexOf(action.option) === -1; | ||
const total = state.selected.length + state.created.length; // if new | ||
if (isNew) { | ||
const created = [...state.created]; | ||
const index = state.created.indexOf(action.option); | ||
if (index !== -1) { | ||
created.splice(index, 1); // if existing, remove | ||
} else if (total < action.max) { | ||
// if new, add unless we’re at max | ||
created.push(action.option); | ||
} | ||
return _objectSpread({}, state, { | ||
created | ||
}); | ||
} // if existing | ||
const selected = [...state.selected]; | ||
const index = state.selected.indexOf(action.option); | ||
if (index !== -1) { | ||
selected.splice(index, 1); // if existing, remove | ||
} else if (total < action.max) { | ||
selected.push(action.option); // if new, add unless we’re at max | ||
} | ||
return _objectSpread({}, state, { | ||
selected: action.options.filter(order => selected.indexOf(order) !== -1) | ||
}); | ||
} | ||
// Add or remove an option from keyboard | ||
case Action.ADD_MULTI_KEYBOARD: | ||
{ | ||
const match = state.activeDescendant.match(/\d*$/); | ||
if (!match) { | ||
return state; | ||
} | ||
const total = state.selected.length + state.created.length; | ||
const number = parseInt(match[0], 10); // if new | ||
if (number > action.options.length - 1) { | ||
const created = [...state.created]; | ||
const option = state.search; | ||
const index = state.created.indexOf(option); | ||
if (index === -1) { | ||
created.splice(index, 1); // if existing, remove | ||
} else if (total < action.max) { | ||
// if new, add unless we’re at max | ||
created.push(option); | ||
} | ||
return _objectSpread({}, state, { | ||
created | ||
}); | ||
} // if existing | ||
const option = action.options[number]; | ||
const selected = [...state.selected]; | ||
const index = state.selected.indexOf(option); | ||
if (index !== -1) { | ||
selected.splice(index, 1); // if existing, remove | ||
} else if (total < action.max) { | ||
selected.push(option); // if new, add unless we’re at max | ||
} | ||
return _objectSpread({}, state, { | ||
selected: action.options.filter(order => selected.indexOf(order) !== -1) | ||
}); | ||
} | ||
// Set the option from mouse click | ||
case Action.ADD_SINGLE_CLICK: | ||
{ | ||
const newState = _objectSpread({}, state, { | ||
isOpen: false, | ||
search: '', | ||
visibleOptions: action.options | ||
}); | ||
if (action.options.indexOf(action.option) === -1) { | ||
return _objectSpread({}, newState, { | ||
created: [action.option] | ||
}); // if new | ||
} | ||
return _objectSpread({}, newState, { | ||
selected: [action.option] | ||
}); // if existing | ||
} | ||
// Set the option from keyboard | ||
case Action.ADD_SINGLE_KEYBOARD: | ||
{ | ||
const match = state.activeDescendant.match(/\d*$/); | ||
if (!match) { | ||
return state; | ||
} | ||
const number = parseInt(match[0], 10); | ||
const newState = _objectSpread({}, state, { | ||
isOpen: false, | ||
visibleOptions: action.options | ||
}); | ||
if (number > action.options.length - 1) { | ||
return _objectSpread({}, newState, { | ||
created: [state.search] | ||
}); // if new | ||
} | ||
return _objectSpread({}, newState, { | ||
selected: [action.options[number]] | ||
}); // if existing | ||
} | ||
// Close dropdown | ||
case Action.DROPDOWN_CLOSE: | ||
{ | ||
if (state.isOpen === false) { | ||
return state; // prevent other state from reseting | ||
} | ||
const activeDescendant = optionId(action.name, 0); | ||
return _objectSpread({}, state, { | ||
activeDescendant, | ||
isOpen: false, | ||
search: '', | ||
visibleOptions: action.options | ||
}); | ||
} | ||
// Open dropdown | ||
case Action.DROPDOWN_OPEN: | ||
{ | ||
if (state.isOpen === true) { | ||
return state; // prevent other state from reseting | ||
} | ||
const activeDescendant = optionId(action.name, 0); | ||
return _objectSpread({}, state, { | ||
activeDescendant, | ||
isOpen: true, | ||
search: '', | ||
visibleOptions: action.options | ||
}); | ||
} | ||
// Invert dropdown state | ||
case Action.DROPDOWN_TOGGLE: | ||
{ | ||
const nextOpen = !state.isOpen; | ||
const activeDescendant = optionId(action.name, 0); | ||
return _objectSpread({}, state, { | ||
activeDescendant, | ||
isOpen: nextOpen, | ||
search: '', | ||
visibleOptions: action.options | ||
}); | ||
} | ||
// Move to start / end | ||
case Action.MOVE_TO_END: | ||
case Action.MOVE_TO_START: | ||
{ | ||
const index = action.type === Action.MOVE_TO_START ? action.options.indexOf(state.visibleOptions[0]) : action.options.indexOf(state.visibleOptions[state.visibleOptions.length - 1]); | ||
const activeDescendant = action.allowCreate === true ? optionId(action.name, action.options.length) : optionId(action.name, index); | ||
if (action.wrapper) { | ||
const el = action.wrapper.querySelector(`#${activeDescendant}`); | ||
if (el) { | ||
el.scrollIntoView(false); | ||
} | ||
} | ||
return _objectSpread({}, state, { | ||
activeDescendant | ||
}); | ||
} | ||
// Initialize dropdown & set default state (while still letting props be mutable) | ||
case Action.INIT: | ||
{ | ||
// Users may specify created options within defaults, if they’re missing from the options array | ||
const defaults = action.defaults || []; | ||
const selected = action.options.filter(option => defaults.indexOf(option) !== -1); | ||
const created = defaults.filter(option => action.options.indexOf(option) === -1); | ||
return _objectSpread({}, state, { | ||
created, | ||
selected, | ||
visibleOptions: action.options | ||
}); | ||
} | ||
// Move up or down with keyboard | ||
case Action.MOVE_NEXT: | ||
case Action.MOVE_PREV: | ||
{ | ||
const match = state.activeDescendant.match(/\d*$/); // get number at end of ID | ||
if (!match) { | ||
return state; | ||
} | ||
const index = parseInt(match[0], 10); | ||
const selected = action.options[index]; | ||
const visibleIndex = state.visibleOptions.indexOf(selected); | ||
const options = [...action.options]; | ||
if (action.allowCreate === true) { | ||
options.push(state.search); // if allowCreate, allow keyboard to highlight newly-created item | ||
} | ||
let next; | ||
if (action.type === Action.MOVE_PREV) { | ||
const prevVisible = options.indexOf(state.visibleOptions[visibleIndex - 1]); | ||
const lastVisible = options.indexOf(state.visibleOptions[state.visibleOptions.length - 1]); | ||
next = prevVisible !== -1 ? prevVisible : lastVisible; | ||
} else { | ||
const nextVisible = options.indexOf(state.visibleOptions[visibleIndex + 1]); | ||
const firstVisible = options.indexOf(state.visibleOptions[0]); | ||
next = nextVisible !== -1 ? nextVisible : firstVisible; | ||
} | ||
const activeDescendant = optionId(action.name, next); | ||
if (action.wrapper) { | ||
const el = action.wrapper.querySelector(`#${activeDescendant}`); | ||
if (el) { | ||
el.scrollIntoView(false); | ||
} | ||
} | ||
return _objectSpread({}, state, { | ||
activeDescendant | ||
}); | ||
} | ||
// Filter results | ||
case Action.SEARCH: | ||
{ | ||
const visibleOptions = action.options.filter(option => new RegExp(action.search, 'i').test(option)); | ||
const options = [...action.options]; | ||
if (action.allowCreate === true) { | ||
options.push(action.search); | ||
} | ||
return _objectSpread({}, state, { | ||
activeDescendant: optionId(action.name, options.indexOf(visibleOptions[0] || state.search)), | ||
search: action.search, | ||
visibleOptions | ||
}); | ||
} | ||
default: | ||
return state; | ||
} | ||
} | ||
const SelectZero = (_ref) => { | ||
@@ -410,5 +64,5 @@ let { | ||
className = 'rsz', | ||
defaultValue = [], | ||
max = Infinity, | ||
multi = false, | ||
name, | ||
noSearch = false, | ||
@@ -418,176 +72,140 @@ onChange, | ||
placeholder = 'Select', | ||
name | ||
value = [] | ||
} = _ref, | ||
rest = _objectWithoutProperties(_ref, ["allowCreate", "className", "defaultValue", "max", "multi", "noSearch", "onChange", "options", "placeholder", "name"]); | ||
rest = _objectWithoutProperties(_ref, ["allowCreate", "className", "max", "multi", "name", "noSearch", "onChange", "options", "placeholder", "value"]); | ||
const [isInitialized, setIsInitialized] = React.useState(false); | ||
const [searchListener, setSearchListener] = React.useState(); | ||
const [triggerListener, setTriggerListener] = React.useState(); | ||
// state | ||
const listRef = React.useRef(null); | ||
const searchRef = React.useRef(null); | ||
const triggerRef = React.useRef(null); | ||
const [state, dispatch] = React.useReducer(reducer, initialState); | ||
const [activeDescendantIndex, setActiveDescendantIndex] = React.useState(0); // Active descendant. Numbers are easier to manipulate than element IDs. | ||
function onClick(option) { | ||
if (multi === true) { | ||
dispatch({ | ||
max, | ||
name, | ||
option, | ||
options, | ||
type: Action.ADD_MULTI_CLICK | ||
}); | ||
} else { | ||
dispatch({ | ||
name, | ||
option, | ||
options, | ||
type: Action.ADD_SINGLE_CLICK | ||
}); | ||
const [search, setSearch] = React.useState(''); | ||
const [isOpen, setIsOpen] = React.useState(false); // computed | ||
const visibleIndices = []; | ||
const visibleOptions = []; | ||
options.forEach((option, index) => { | ||
if (new RegExp(search, 'i').test(option)) { | ||
// filter results in one pass | ||
visibleOptions.push(option); | ||
visibleIndices.push(index); | ||
} | ||
} // effect 1: user callback | ||
}); | ||
const shouldDisplaySearch = noSearch !== true && options.length > 4; | ||
const shouldDisplayCreate = allowCreate === true && search.length > 0 && options.findIndex(option => option === search) === -1; | ||
const isMaxed = value.length === max; // methods | ||
function elId(component) { | ||
return `rsz-${name}-${component}`; | ||
} | ||
React.useEffect(() => { | ||
if (typeof onChange === 'function') { | ||
onChange(state.selected, state.created); | ||
} | ||
}, [state.selected, state.created]); // eslint-disable-line react-hooks/exhaustive-deps | ||
// effect 2: initialization | ||
function optionId(option) { | ||
return elId(`option-${option}`); | ||
} | ||
React.useEffect(() => { | ||
if (!isInitialized) { | ||
dispatch({ | ||
defaults: defaultValue, | ||
options, | ||
type: Action.INIT | ||
}); | ||
setIsInitialized(true); | ||
} | ||
}, [defaultValue, options, isInitialized]); // effect 3: maintain focus | ||
function scrollTo(wrapper, selector) { | ||
if (wrapper) { | ||
const el = wrapper.querySelector(selector); | ||
React.useEffect(() => { | ||
if (state.isOpen) { | ||
if (searchRef.current) { | ||
searchRef.current.focus(); | ||
} else if (triggerRef.current) { | ||
triggerRef.current.focus(); | ||
if (el) { | ||
el.scrollIntoView(false); | ||
} | ||
} else if (!state.isOpen && triggerRef.current) { | ||
triggerRef.current.focus(); | ||
} | ||
}, [state.activeDescendant, state.isOpen]); // effect 4: navigation listener | ||
} | ||
React.useEffect(() => { | ||
function onKeydown(evt) { | ||
// eslint-disable-next-line default-case | ||
switch (evt.key) { | ||
case KEY.ENTER: | ||
{ | ||
evt.preventDefault(); | ||
dispatch({ | ||
max, | ||
options, | ||
type: multi === true ? Action.ADD_MULTI_KEYBOARD : Action.ADD_SINGLE_KEYBOARD | ||
}); | ||
break; | ||
} | ||
// move down | ||
function addItem(option) { | ||
// multi | ||
if (multi === true) { | ||
let selected = [...value]; | ||
const index = selected.indexOf(option); | ||
case KEY.DOWN: | ||
{ | ||
evt.preventDefault(); | ||
dispatch({ | ||
allowCreate, | ||
name, | ||
options, | ||
type: Action.MOVE_NEXT, | ||
wrapper: listRef.current | ||
}); | ||
break; | ||
} | ||
// move to end | ||
if (index !== -1) { | ||
selected.splice(index, 1); // if existing, remove | ||
} else if (selected.length < max) { | ||
selected.push(option); // if new, add unless we’re at max | ||
} | ||
case KEY.END: | ||
dispatch({ | ||
allowCreate, | ||
name, | ||
options, | ||
type: Action.MOVE_TO_END, | ||
wrapper: listRef.current | ||
}); | ||
if (options.indexOf(option) !== -1) { | ||
selected = options.filter(order => selected.indexOf(order) !== -1); // if existing option, keep original order | ||
} | ||
onChange(selected); | ||
return; | ||
} // single | ||
onChange([option]); | ||
setIsOpen(false); | ||
} | ||
function onKeyDown(evt) { | ||
const first = visibleIndices[0]; | ||
const last = visibleIndices[visibleIndices.length - 1]; | ||
switch (evt.key) { | ||
// select active item | ||
case KEY.ENTER: | ||
{ | ||
evt.preventDefault(); | ||
addItem(options[activeDescendantIndex]); | ||
break; | ||
// move to start | ||
} | ||
// move down / up | ||
case KEY.HOME: | ||
dispatch({ | ||
allowCreate, | ||
name, | ||
options, | ||
type: Action.MOVE_TO_START, | ||
wrapper: listRef.current | ||
}); | ||
case KEY.DOWN: | ||
case KEY.UP: | ||
{ | ||
evt.preventDefault(); | ||
const sum = evt.key === KEY.UP ? -1 : 1; | ||
const fallback = evt.key === KEY.UP ? last : first; // if at beginning, loop around to end, and vice-versa | ||
const nextIndex = visibleIndices.indexOf(activeDescendantIndex) + sum; | ||
const next = visibleIndices[nextIndex] !== undefined ? visibleIndices[nextIndex] : fallback; | ||
setActiveDescendantIndex(next); // set to last index if at beginning of list | ||
scrollTo(listRef.current, `#${optionId(next)}`); | ||
break; | ||
// move up | ||
} | ||
// move to start / end | ||
case KEY.ESC: | ||
dispatch({ | ||
name, | ||
options, | ||
type: Action.DROPDOWN_CLOSE | ||
}); | ||
case KEY.END: | ||
case KEY.HOME: | ||
{ | ||
const next = evt.key === KEY.HOME ? first : last; | ||
setActiveDescendantIndex(next); | ||
scrollTo(listRef.current, `#${optionId(next)}`); | ||
break; | ||
// close | ||
} | ||
// close | ||
case KEY.UP: | ||
{ | ||
evt.preventDefault(); | ||
dispatch({ | ||
allowCreate, | ||
name, | ||
options, | ||
type: Action.MOVE_PREV, | ||
wrapper: listRef.current | ||
}); | ||
break; | ||
} | ||
} | ||
} | ||
case KEY.ESC: | ||
setIsOpen(false); | ||
break; | ||
if (!searchListener && searchRef.current) { | ||
searchRef.current.addEventListener('keydown', onKeydown); | ||
setSearchListener(true); | ||
default: | ||
if (noSearch) { | ||
evt.preventDefault(); | ||
} | ||
break; | ||
} | ||
}, [searchListener]); // eslint-disable-line react-hooks/exhaustive-deps | ||
// effect 5: trigger listener | ||
} // effect 1. maintain focus | ||
React.useEffect(() => { | ||
function onKeydown(e) { | ||
if (e.key === KEY.DOWN) { | ||
e.preventDefault(); | ||
dispatch({ | ||
name, | ||
options, | ||
type: Action.DROPDOWN_OPEN | ||
}); | ||
} | ||
if (searchRef.current && isOpen) { | ||
searchRef.current.focus(); | ||
} else if (triggerRef.current && !isOpen) { | ||
triggerRef.current.focus(); | ||
} | ||
}); // effect 2. active descendant | ||
if (triggerRef.current && !triggerListener) { | ||
triggerRef.current.addEventListener('keyup', onKeydown); | ||
setTriggerListener(true); | ||
React.useEffect(() => { | ||
if (visibleIndices.indexOf(activeDescendantIndex) === -1) { | ||
setActiveDescendantIndex(visibleIndices[0]); | ||
} | ||
}, [name, options, triggerListener]); | ||
const shouldDisplaySearch = noSearch !== true && options.length > 4; | ||
const shouldDisplayCreate = allowCreate !== false && state.search.length > 0 && state.visibleOptions.findIndex(option => option === state.search) === -1; // maintain focus on re-render | ||
if (searchRef.current && state.isOpen) { | ||
searchRef.current.focus(); | ||
} else if (triggerRef.current && !state.isOpen) { | ||
triggerRef.current.focus(); | ||
} | ||
const selection = [...state.selected, ...state.created]; | ||
}, [activeDescendantIndex, visibleIndices]); | ||
return React__default.createElement("div", Object.assign({ | ||
"aria-expanded": state.isOpen || undefined, | ||
"aria-expanded": isOpen === true, | ||
"aria-multiselectable": multi === true || undefined, | ||
@@ -599,18 +217,19 @@ className: className, | ||
}, React__default.createElement("button", { | ||
"aria-controls": elId(name, 'menu'), | ||
"aria-controls": elId('menu'), | ||
"aria-haspopup": "listbox", | ||
className: "rsz__trigger-button", | ||
onClick: () => setIsOpen(true), | ||
onKeyDown: e => { | ||
if (e.key === KEY.DOWN) { | ||
setIsOpen(true); | ||
} | ||
}, | ||
ref: triggerRef, | ||
type: "button", | ||
onClick: () => dispatch({ | ||
name, | ||
options, | ||
type: Action.DROPDOWN_TOGGLE | ||
}) | ||
type: "button" | ||
}, multi === true ? `Select options for ${name}` : `Select an option for ${name}`, React__default.createElement("span", { | ||
"aria-hidden": true, | ||
className: "rsz__arrow" | ||
}, "\u2193")), selection.length > 0 ? React__default.createElement("ul", { | ||
}, "\u2193")), value.length > 0 ? React__default.createElement("ul", { | ||
className: "rsz__selection" | ||
}, selection.map(option => React__default.createElement("li", { | ||
}, value.map(option => React__default.createElement("li", { | ||
key: option, | ||
@@ -621,12 +240,6 @@ className: "rsz__selected" | ||
}, option), React__default.createElement("button", { | ||
"aria-label": `remove ${option}`, | ||
className: "rsz__selected-action", | ||
"aria-hidden": true, | ||
type: "button", | ||
onClick: () => dispatch({ | ||
max, | ||
name, | ||
option, | ||
options, | ||
type: Action.ADD_MULTI_CLICK | ||
}) | ||
onClick: () => addItem(option), | ||
type: "button" | ||
}, "\u2715")))) : React__default.createElement("div", { | ||
@@ -637,8 +250,5 @@ className: "rsz__selection rsz__placeholder" | ||
className: "rsz__overlay", | ||
onClick: () => dispatch({ | ||
name, | ||
options, | ||
type: Action.DROPDOWN_CLOSE | ||
}) | ||
onClick: () => setIsOpen(false) | ||
}), React__default.createElement("menu", { | ||
id: elId('menu'), | ||
className: "rsz__dropdown-wrapper" | ||
@@ -648,16 +258,11 @@ }, React__default.createElement("div", { | ||
}, React__default.createElement("input", { | ||
"aria-activedescendant": state.activeDescendant, | ||
"aria-activedescendant": optionId(activeDescendantIndex), | ||
"aria-hidden": shouldDisplaySearch === false || undefined, | ||
"aria-label": "Filter options", | ||
className: "rsz__search", | ||
onChange: e => noSearch !== true && dispatch({ | ||
allowCreate, | ||
name, | ||
options, | ||
search: e.target.value, | ||
type: Action.SEARCH | ||
}), | ||
onChange: e => setSearch(e.target.value), | ||
onKeyDown: onKeyDown, | ||
ref: searchRef, | ||
type: "search", | ||
value: state.search | ||
value: search | ||
}), React__default.createElement("div", { | ||
@@ -668,9 +273,7 @@ className: "rsz__search-icon" | ||
ref: listRef | ||
}, state.visibleOptions.length > 0 ? state.visibleOptions.map(option => { | ||
const id = optionId(name, options.indexOf(option)); | ||
const isSelected = selection.indexOf(option) !== -1; | ||
const isMaxxed = selection.length === max; | ||
}, visibleOptions.map((option, index) => { | ||
const isSelected = value.indexOf(option) !== -1; | ||
let ariaSelected = isSelected; | ||
if (isMaxxed) { | ||
if (ariaSelected === false && isMaxed === true) { | ||
ariaSelected = undefined; | ||
@@ -684,21 +287,23 @@ } | ||
"aria-selected": ariaSelected, | ||
"data-highlighted": state.activeDescendant === id || undefined, | ||
disabled: isMaxxed && !isSelected, | ||
id: id, | ||
onClick: () => onClick(option), | ||
"data-highlighted": activeDescendantIndex === visibleIndices[index] || undefined, | ||
disabled: isMaxed && !isSelected, | ||
id: optionId(visibleIndices[index]), | ||
onClick: () => addItem(option), | ||
role: "option", | ||
type: "button" | ||
}, option)); | ||
}) : React__default.createElement("span", { | ||
}), options.length > 0 && visibleOptions.length === 0 && React__default.createElement("span", { | ||
className: "rsz__no-results" | ||
}, "No results for \u201C", state.search, "\u201D"), shouldDisplayCreate && React__default.createElement("li", { | ||
}, "No results for \u201C", search, "\u201D"), shouldDisplayCreate && React__default.createElement("li", { | ||
className: "rsz__option" | ||
}, React__default.createElement("button", { | ||
className: "rsz__create", | ||
id: optionId(name, options.length), | ||
type: "button", | ||
onClick: () => onClick(state.search) | ||
"data-highlighted": activeDescendantIndex === options.length || undefined, | ||
disabled: isMaxed, | ||
id: optionId(options.length), | ||
onClick: () => addItem(search), | ||
type: "button" | ||
}, "Create ", React__default.createElement("span", { | ||
className: "rsz__search-term" | ||
}, state.search))), allowCreate === true && state.search === '' && React__default.createElement("li", { | ||
}, search))), allowCreate === true && search === '' && React__default.createElement("li", { | ||
className: "rsz__option" | ||
@@ -710,3 +315,3 @@ }, React__default.createElement("span", { | ||
name: name, | ||
value: selection.join(',') | ||
value: value.join(',') | ||
})); | ||
@@ -716,3 +321,1 @@ }; | ||
exports.default = SelectZero; | ||
exports.elId = elId; | ||
exports.optionId = optionId; |
@@ -1,1 +0,1 @@ | ||
"use strict";function _interopDefault(e){return e&&"object"==typeof e&&"default"in e?e.default:e}Object.defineProperty(exports,"__esModule",{value:!0});var Action,KEY,React=require("react"),React__default=_interopDefault(React);function _defineProperty(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function _objectSpread(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{},a=Object.keys(n);"function"==typeof Object.getOwnPropertySymbols&&(a=a.concat(Object.getOwnPropertySymbols(n).filter(function(e){return Object.getOwnPropertyDescriptor(n,e).enumerable}))),a.forEach(function(t){_defineProperty(e,t,n[t])})}return e}function _objectWithoutPropertiesLoose(e,t){if(null==e)return{};var n,a,o={},c=Object.keys(e);for(a=0;a<c.length;a++)n=c[a],t.indexOf(n)>=0||(o[n]=e[n]);return o}function _objectWithoutProperties(e,t){if(null==e)return{};var n,a,o=_objectWithoutPropertiesLoose(e,t);if(Object.getOwnPropertySymbols){var c=Object.getOwnPropertySymbols(e);for(a=0;a<c.length;a++)n=c[a],t.indexOf(n)>=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function elId(e,t){return`rsz-${e}-${t}`}function optionId(e,t){return elId(e,`option-${t}`)}!function(e){e.APPEND="APPEND",e.ADD_MULTI_CLICK="ADD_MULTI_CLICK",e.ADD_MULTI_KEYBOARD="ADD_MULTI_KEYBOARD",e.ADD_SINGLE_CLICK="ADD_SINGLE_CLICK",e.ADD_SINGLE_KEYBOARD="ADD_SINGLE_KEYBOARD",e.DROPDOWN_CLOSE="DROPDOWN_CLOSE",e.DROPDOWN_OPEN="DROPDOWN_OPEN",e.DROPDOWN_TOGGLE="DROPDOWN_TOGGLE",e.INIT="INIT",e.MOVE_NEXT="MOVE_NEXT",e.MOVE_PREV="MOVE_PREV",e.MOVE_TO_END="MOVE_TO_END",e.MOVE_TO_START="MOVE_TO_START",e.SEARCH="SEARCH"}(Action||(Action={})),function(e){e.DOWN="ArrowDown",e.END="End",e.ENTER="Enter",e.ESC="Escape",e.HOME="Home",e.UP="ArrowUp"}(KEY||(KEY={}));const initialState={activeDescendant:"0",created:[],isOpen:!1,search:"",selected:[],visibleOptions:[]};function reducer(e,t){switch(t.type){case Action.ADD_MULTI_CLICK:{const n=-1===t.options.indexOf(t.option),a=e.selected.length+e.created.length;if(n){const n=[...e.created],o=e.created.indexOf(t.option);return-1!==o?n.splice(o,1):a<t.max&&n.push(t.option),_objectSpread({},e,{created:n})}const o=[...e.selected],c=e.selected.indexOf(t.option);return-1!==c?o.splice(c,1):a<t.max&&o.push(t.option),_objectSpread({},e,{selected:t.options.filter(e=>-1!==o.indexOf(e))})}case Action.ADD_MULTI_KEYBOARD:{const n=e.activeDescendant.match(/\d*$/);if(!n)return e;const a=e.selected.length+e.created.length,o=parseInt(n[0],10);if(o>t.options.length-1){const n=[...e.created],o=e.search,c=e.created.indexOf(o);return-1===c?n.splice(c,1):a<t.max&&n.push(o),_objectSpread({},e,{created:n})}const c=t.options[o],r=[...e.selected],s=e.selected.indexOf(c);return-1!==s?r.splice(s,1):a<t.max&&r.push(c),_objectSpread({},e,{selected:t.options.filter(e=>-1!==r.indexOf(e))})}case Action.ADD_SINGLE_CLICK:{const n=_objectSpread({},e,{isOpen:!1,search:"",visibleOptions:t.options});return-1===t.options.indexOf(t.option)?_objectSpread({},n,{created:[t.option]}):_objectSpread({},n,{selected:[t.option]})}case Action.ADD_SINGLE_KEYBOARD:{const n=e.activeDescendant.match(/\d*$/);if(!n)return e;const a=parseInt(n[0],10),o=_objectSpread({},e,{isOpen:!1,visibleOptions:t.options});return a>t.options.length-1?_objectSpread({},o,{created:[e.search]}):_objectSpread({},o,{selected:[t.options[a]]})}case Action.DROPDOWN_CLOSE:if(!1===e.isOpen)return e;return _objectSpread({},e,{activeDescendant:optionId(t.name,0),isOpen:!1,search:"",visibleOptions:t.options});case Action.DROPDOWN_OPEN:if(!0===e.isOpen)return e;return _objectSpread({},e,{activeDescendant:optionId(t.name,0),isOpen:!0,search:"",visibleOptions:t.options});case Action.DROPDOWN_TOGGLE:{const n=!e.isOpen;return _objectSpread({},e,{activeDescendant:optionId(t.name,0),isOpen:n,search:"",visibleOptions:t.options})}case Action.MOVE_TO_END:case Action.MOVE_TO_START:{const n=t.type===Action.MOVE_TO_START?t.options.indexOf(e.visibleOptions[0]):t.options.indexOf(e.visibleOptions[e.visibleOptions.length-1]),a=!0===t.allowCreate?optionId(t.name,t.options.length):optionId(t.name,n);if(t.wrapper){const e=t.wrapper.querySelector(`#${a}`);e&&e.scrollIntoView(!1)}return _objectSpread({},e,{activeDescendant:a})}case Action.INIT:{const n=t.defaults||[],a=t.options.filter(e=>-1!==n.indexOf(e));return _objectSpread({},e,{created:n.filter(e=>-1===t.options.indexOf(e)),selected:a,visibleOptions:t.options})}case Action.MOVE_NEXT:case Action.MOVE_PREV:{const n=e.activeDescendant.match(/\d*$/);if(!n)return e;const a=parseInt(n[0],10),o=t.options[a],c=e.visibleOptions.indexOf(o),r=[...t.options];let s;if(!0===t.allowCreate&&r.push(e.search),t.type===Action.MOVE_PREV){const t=r.indexOf(e.visibleOptions[c-1]),n=r.indexOf(e.visibleOptions[e.visibleOptions.length-1]);s=-1!==t?t:n}else{const t=r.indexOf(e.visibleOptions[c+1]),n=r.indexOf(e.visibleOptions[0]);s=-1!==t?t:n}const i=optionId(t.name,s);if(t.wrapper){const e=t.wrapper.querySelector(`#${i}`);e&&e.scrollIntoView(!1)}return _objectSpread({},e,{activeDescendant:i})}case Action.SEARCH:{const n=t.options.filter(e=>new RegExp(t.search,"i").test(e)),a=[...t.options];return!0===t.allowCreate&&a.push(t.search),_objectSpread({},e,{activeDescendant:optionId(t.name,a.indexOf(n[0]||e.search)),search:t.search,visibleOptions:n})}default:return e}}const SelectZero=e=>{let{allowCreate:t=!1,className:n="rsz",defaultValue:a=[],max:o=1/0,multi:c=!1,noSearch:r=!1,onChange:s,options:i,placeholder:l="Select",name:p}=e,_=_objectWithoutProperties(e,["allowCreate","className","defaultValue","max","multi","noSearch","onChange","options","placeholder","name"]);const[d,u]=React.useState(!1),[O,f]=React.useState(),[E,D]=React.useState(),m=React.useRef(null),b=React.useRef(null),R=React.useRef(null),[h,A]=React.useReducer(reducer,initialState);function N(e){A(!0===c?{max:o,name:p,option:e,options:i,type:Action.ADD_MULTI_CLICK}:{name:p,option:e,options:i,type:Action.ADD_SINGLE_CLICK})}React.useEffect(()=>{"function"==typeof s&&s(h.selected,h.created)},[h.selected,h.created]),React.useEffect(()=>{d||(A({defaults:a,options:i,type:Action.INIT}),u(!0))},[a,i,d]),React.useEffect(()=>{h.isOpen?b.current?b.current.focus():R.current&&R.current.focus():!h.isOpen&&R.current&&R.current.focus()},[h.activeDescendant,h.isOpen]),React.useEffect(()=>{!O&&b.current&&(b.current.addEventListener("keydown",function(e){switch(e.key){case KEY.ENTER:e.preventDefault(),A({max:o,options:i,type:!0===c?Action.ADD_MULTI_KEYBOARD:Action.ADD_SINGLE_KEYBOARD});break;case KEY.DOWN:e.preventDefault(),A({allowCreate:t,name:p,options:i,type:Action.MOVE_NEXT,wrapper:m.current});break;case KEY.END:A({allowCreate:t,name:p,options:i,type:Action.MOVE_TO_END,wrapper:m.current});break;case KEY.HOME:A({allowCreate:t,name:p,options:i,type:Action.MOVE_TO_START,wrapper:m.current});break;case KEY.ESC:A({name:p,options:i,type:Action.DROPDOWN_CLOSE});break;case KEY.UP:e.preventDefault(),A({allowCreate:t,name:p,options:i,type:Action.MOVE_PREV,wrapper:m.current})}}),f(!0))},[O]),React.useEffect(()=>{R.current&&!E&&(R.current.addEventListener("keyup",function(e){e.key===KEY.DOWN&&(e.preventDefault(),A({name:p,options:i,type:Action.DROPDOWN_OPEN}))}),D(!0))},[p,i,E]);const v=!0!==r&&i.length>4,S=!1!==t&&h.search.length>0&&-1===h.visibleOptions.findIndex(e=>e===h.search);b.current&&h.isOpen?b.current.focus():R.current&&!h.isOpen&&R.current.focus();const I=[...h.selected,...h.created];return React__default.createElement("div",Object.assign({"aria-expanded":h.isOpen||void 0,"aria-multiselectable":!0===c||void 0,className:n,role:"listbox"},_),React__default.createElement("div",{className:"rsz__trigger"},React__default.createElement("button",{"aria-controls":elId(p,"menu"),"aria-haspopup":"listbox",className:"rsz__trigger-button",ref:R,type:"button",onClick:()=>A({name:p,options:i,type:Action.DROPDOWN_TOGGLE})},!0===c?`Select options for ${p}`:`Select an option for ${p}`,React__default.createElement("span",{"aria-hidden":!0,className:"rsz__arrow"},"↓")),I.length>0?React__default.createElement("ul",{className:"rsz__selection"},I.map(e=>React__default.createElement("li",{key:e,className:"rsz__selected"},React__default.createElement("span",{className:"rsz__selected-text"},e),React__default.createElement("button",{className:"rsz__selected-action","aria-hidden":!0,type:"button",onClick:()=>A({max:o,name:p,option:e,options:i,type:Action.ADD_MULTI_CLICK})},"✕")))):React__default.createElement("div",{className:"rsz__selection rsz__placeholder"},l)),React__default.createElement("div",{"aria-label":"Close dropdown",className:"rsz__overlay",onClick:()=>A({name:p,options:i,type:Action.DROPDOWN_CLOSE})}),React__default.createElement("menu",{className:"rsz__dropdown-wrapper"},React__default.createElement("div",{className:"rsz__dropdown"},React__default.createElement("input",{"aria-activedescendant":h.activeDescendant,"aria-hidden":!1===v||void 0,"aria-label":"Filter options",className:"rsz__search",onChange:e=>!0!==r&&A({allowCreate:t,name:p,options:i,search:e.target.value,type:Action.SEARCH}),ref:b,type:"search",value:h.search}),React__default.createElement("div",{className:"rsz__search-icon"}),React__default.createElement("ul",{className:"rsz__option-list",ref:m},h.visibleOptions.length>0?h.visibleOptions.map(e=>{const t=optionId(p,i.indexOf(e)),n=-1!==I.indexOf(e),a=I.length===o;let c=n;return a&&(c=void 0),React__default.createElement("li",{className:"rsz__option",key:e},React__default.createElement("button",{"aria-selected":c,"data-highlighted":h.activeDescendant===t||void 0,disabled:a&&!n,id:t,onClick:()=>N(e),role:"option",type:"button"},e))}):React__default.createElement("span",{className:"rsz__no-results"},"No results for “",h.search,"”"),S&&React__default.createElement("li",{className:"rsz__option"},React__default.createElement("button",{className:"rsz__create",id:optionId(p,i.length),type:"button",onClick:()=>N(h.search)},"Create ",React__default.createElement("span",{className:"rsz__search-term"},h.search))),!0===t&&""===h.search&&React__default.createElement("li",{className:"rsz__option"},React__default.createElement("span",{className:"rsz__create"},"Start typing to create an item"))))),React__default.createElement("input",{type:"hidden",name:p,value:I.join(",")}))};exports.default=SelectZero,exports.elId=elId,exports.optionId=optionId; | ||
"use strict";function _interopDefault(e){return e&&"object"==typeof e&&"default"in e?e.default:e}Object.defineProperty(exports,"__esModule",{value:!0});var KEY,React=require("react"),React__default=_interopDefault(React);function _objectWithoutPropertiesLoose(e,t){if(null==e)return{};var a,r,l={},n=Object.keys(e);for(r=0;r<n.length;r++)a=n[r],t.indexOf(a)>=0||(l[a]=e[a]);return l}function _objectWithoutProperties(e,t){if(null==e)return{};var a,r,l=_objectWithoutPropertiesLoose(e,t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);for(r=0;r<n.length;r++)a=n[r],t.indexOf(a)>=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(l[a]=e[a])}return l}!function(e){e.DOWN="ArrowDown",e.END="End",e.ENTER="Enter",e.ESC="Escape",e.HOME="Home",e.UP="ArrowUp"}(KEY||(KEY={}));const SelectZero=e=>{let{allowCreate:t=!1,className:a="rsz",max:r=1/0,multi:l=!1,name:n,noSearch:c=!1,onChange:s,options:o,placeholder:i="Select",value:u=[]}=e,_=_objectWithoutProperties(e,["allowCreate","className","max","multi","name","noSearch","onChange","options","placeholder","value"]);const d=React.useRef(null),f=React.useRef(null),m=React.useRef(null),[p,E]=React.useState(0),[h,R]=React.useState(""),[b,N]=React.useState(!1),g=[],v=[];o.forEach((e,t)=>{new RegExp(h,"i").test(e)&&(v.push(e),g.push(t))});const y=!0!==c&&o.length>4,z=!0===t&&h.length>0&&-1===o.findIndex(e=>e===h),O=u.length===r;function x(e){return`rsz-${n}-${e}`}function k(e){return x(`option-${e}`)}function w(e,t){if(e){const a=e.querySelector(t);a&&a.scrollIntoView(!1)}}function S(e){if(!0===l){let t=[...u];const a=t.indexOf(e);return-1!==a?t.splice(a,1):t.length<r&&t.push(e),-1!==o.indexOf(e)&&(t=o.filter(e=>-1!==t.indexOf(e))),void s(t)}s([e]),N(!1)}return React.useEffect(()=>{f.current&&b?f.current.focus():m.current&&!b&&m.current.focus()}),React.useEffect(()=>{-1===g.indexOf(p)&&E(g[0])},[p,g]),React__default.createElement("div",Object.assign({"aria-expanded":!0===b,"aria-multiselectable":!0===l||void 0,className:a,role:"listbox"},_),React__default.createElement("div",{className:"rsz__trigger"},React__default.createElement("button",{"aria-controls":x("menu"),"aria-haspopup":"listbox",className:"rsz__trigger-button",onClick:()=>N(!0),onKeyDown:e=>{e.key===KEY.DOWN&&N(!0)},ref:m,type:"button"},!0===l?`Select options for ${n}`:`Select an option for ${n}`,React__default.createElement("span",{"aria-hidden":!0,className:"rsz__arrow"},"↓")),u.length>0?React__default.createElement("ul",{className:"rsz__selection"},u.map(e=>React__default.createElement("li",{key:e,className:"rsz__selected"},React__default.createElement("span",{className:"rsz__selected-text"},e),React__default.createElement("button",{"aria-label":`remove ${e}`,className:"rsz__selected-action",onClick:()=>S(e),type:"button"},"✕")))):React__default.createElement("div",{className:"rsz__selection rsz__placeholder"},i)),React__default.createElement("div",{"aria-label":"Close dropdown",className:"rsz__overlay",onClick:()=>N(!1)}),React__default.createElement("menu",{id:x("menu"),className:"rsz__dropdown-wrapper"},React__default.createElement("div",{className:"rsz__dropdown"},React__default.createElement("input",{"aria-activedescendant":k(p),"aria-hidden":!1===y||void 0,"aria-label":"Filter options",className:"rsz__search",onChange:e=>R(e.target.value),onKeyDown:function(e){const t=g[0],a=g[g.length-1];switch(e.key){case KEY.ENTER:e.preventDefault(),S(o[p]);break;case KEY.DOWN:case KEY.UP:{e.preventDefault();const r=e.key===KEY.UP?-1:1,l=e.key===KEY.UP?a:t,n=g.indexOf(p)+r,c=void 0!==g[n]?g[n]:l;E(c),w(d.current,`#${k(c)}`);break}case KEY.END:case KEY.HOME:{const r=e.key===KEY.HOME?t:a;E(r),w(d.current,`#${k(r)}`);break}case KEY.ESC:N(!1);break;default:c&&e.preventDefault()}},ref:f,type:"search",value:h}),React__default.createElement("div",{className:"rsz__search-icon"}),React__default.createElement("ul",{className:"rsz__option-list",ref:d},v.map((e,t)=>{const a=-1!==u.indexOf(e);let r=a;return!1===r&&!0===O&&(r=void 0),React__default.createElement("li",{className:"rsz__option",key:e},React__default.createElement("button",{"aria-selected":r,"data-highlighted":p===g[t]||void 0,disabled:O&&!a,id:k(g[t]),onClick:()=>S(e),role:"option",type:"button"},e))}),o.length>0&&0===v.length&&React__default.createElement("span",{className:"rsz__no-results"},"No results for “",h,"”"),z&&React__default.createElement("li",{className:"rsz__option"},React__default.createElement("button",{className:"rsz__create","data-highlighted":p===o.length||void 0,disabled:O,id:k(o.length),onClick:()=>S(h),type:"button"},"Create ",React__default.createElement("span",{className:"rsz__search-term"},h))),!0===t&&""===h&&React__default.createElement("li",{className:"rsz__option"},React__default.createElement("span",{className:"rsz__create"},"Start typing to create an item"))))),React__default.createElement("input",{type:"hidden",name:n,value:u.join(",")}))};exports.default=SelectZero; |
@@ -1,26 +0,2 @@ | ||
// TODO: when searching, up/down doesn’t work | ||
import React, { useState, useEffect, useRef, useReducer } from 'react'; | ||
export function elId(name, component) { | ||
return `rsz-${name}-${component}`; | ||
} | ||
export function optionId(name, option) { | ||
return elId(name, `option-${option}`); | ||
} | ||
var Action; | ||
(function (Action) { | ||
Action["APPEND"] = "APPEND"; | ||
Action["ADD_MULTI_CLICK"] = "ADD_MULTI_CLICK"; | ||
Action["ADD_MULTI_KEYBOARD"] = "ADD_MULTI_KEYBOARD"; | ||
Action["ADD_SINGLE_CLICK"] = "ADD_SINGLE_CLICK"; | ||
Action["ADD_SINGLE_KEYBOARD"] = "ADD_SINGLE_KEYBOARD"; | ||
Action["DROPDOWN_CLOSE"] = "DROPDOWN_CLOSE"; | ||
Action["DROPDOWN_OPEN"] = "DROPDOWN_OPEN"; | ||
Action["DROPDOWN_TOGGLE"] = "DROPDOWN_TOGGLE"; | ||
Action["INIT"] = "INIT"; | ||
Action["MOVE_NEXT"] = "MOVE_NEXT"; | ||
Action["MOVE_PREV"] = "MOVE_PREV"; | ||
Action["MOVE_TO_END"] = "MOVE_TO_END"; | ||
Action["MOVE_TO_START"] = "MOVE_TO_START"; | ||
Action["SEARCH"] = "SEARCH"; | ||
})(Action || (Action = {})); | ||
import React, { useState, useRef, useEffect } from 'react'; | ||
var KEY; | ||
@@ -35,392 +11,156 @@ (function (KEY) { | ||
})(KEY || (KEY = {})); | ||
const initialState = { | ||
activeDescendant: '0', | ||
created: [], | ||
isOpen: false, | ||
search: '', | ||
selected: [], | ||
visibleOptions: [], | ||
}; | ||
function reducer(state, action) { | ||
switch (action.type) { | ||
// Add or remove an option from mouse click | ||
case Action.ADD_MULTI_CLICK: { | ||
const isNew = action.options.indexOf(action.option) === -1; | ||
const total = state.selected.length + state.created.length; | ||
// if new | ||
if (isNew) { | ||
const created = [...state.created]; | ||
const index = state.created.indexOf(action.option); | ||
if (index !== -1) { | ||
created.splice(index, 1); // if existing, remove | ||
} | ||
else if (total < action.max) { | ||
// if new, add unless we’re at max | ||
created.push(action.option); | ||
} | ||
return { ...state, created }; | ||
const SelectZero = ({ allowCreate = false, className = 'rsz', max = Infinity, multi = false, name, noSearch = false, onChange, options, placeholder = 'Select', value = [], ...rest }) => { | ||
// state | ||
const listRef = useRef(null); | ||
const searchRef = useRef(null); | ||
const triggerRef = useRef(null); | ||
const [activeDescendantIndex, setActiveDescendantIndex] = useState(0); // Active descendant. Numbers are easier to manipulate than element IDs. | ||
const [search, setSearch] = useState(''); | ||
const [isOpen, setIsOpen] = useState(false); | ||
// computed | ||
const visibleIndices = []; | ||
const visibleOptions = []; | ||
options.forEach((option, index) => { | ||
if (new RegExp(search, 'i').test(option)) { | ||
// filter results in one pass | ||
visibleOptions.push(option); | ||
visibleIndices.push(index); | ||
} | ||
}); | ||
const shouldDisplaySearch = noSearch !== true && options.length > 4; | ||
const shouldDisplayCreate = allowCreate === true && | ||
search.length > 0 && | ||
options.findIndex(option => option === search) === -1; | ||
const isMaxed = value.length === max; | ||
// methods | ||
function elId(component) { | ||
return `rsz-${name}-${component}`; | ||
} | ||
function optionId(option) { | ||
return elId(`option-${option}`); | ||
} | ||
function scrollTo(wrapper, selector) { | ||
if (wrapper) { | ||
const el = wrapper.querySelector(selector); | ||
if (el) { | ||
el.scrollIntoView(false); | ||
} | ||
// if existing | ||
const selected = [...state.selected]; | ||
const index = state.selected.indexOf(action.option); | ||
if (index !== -1) { | ||
selected.splice(index, 1); // if existing, remove | ||
} | ||
else if (total < action.max) { | ||
selected.push(action.option); // if new, add unless we’re at max | ||
} | ||
return { | ||
...state, | ||
selected: action.options.filter(order => selected.indexOf(order) !== -1), | ||
}; | ||
} | ||
// Add or remove an option from keyboard | ||
case Action.ADD_MULTI_KEYBOARD: { | ||
const match = state.activeDescendant.match(/\d*$/); | ||
if (!match) { | ||
return state; | ||
} | ||
const total = state.selected.length + state.created.length; | ||
const number = parseInt(match[0], 10); | ||
// if new | ||
if (number > action.options.length - 1) { | ||
const created = [...state.created]; | ||
const option = state.search; | ||
const index = state.created.indexOf(option); | ||
if (index === -1) { | ||
created.splice(index, 1); // if existing, remove | ||
} | ||
else if (total < action.max) { | ||
// if new, add unless we’re at max | ||
created.push(option); | ||
} | ||
return { ...state, created }; | ||
} | ||
// if existing | ||
const option = action.options[number]; | ||
const selected = [...state.selected]; | ||
const index = state.selected.indexOf(option); | ||
} | ||
function addItem(option) { | ||
// multi | ||
if (multi === true) { | ||
let selected = [...value]; | ||
const index = selected.indexOf(option); | ||
if (index !== -1) { | ||
selected.splice(index, 1); // if existing, remove | ||
} | ||
else if (total < action.max) { | ||
else if (selected.length < max) { | ||
selected.push(option); // if new, add unless we’re at max | ||
} | ||
return { | ||
...state, | ||
selected: action.options.filter(order => selected.indexOf(order) !== -1), | ||
}; | ||
} | ||
// Set the option from mouse click | ||
case Action.ADD_SINGLE_CLICK: { | ||
const newState = { ...state, isOpen: false, search: '', visibleOptions: action.options }; | ||
if (action.options.indexOf(action.option) === -1) { | ||
return { ...newState, created: [action.option] }; // if new | ||
if (options.indexOf(option) !== -1) { | ||
selected = options.filter(order => selected.indexOf(order) !== -1); // if existing option, keep original order | ||
} | ||
return { ...newState, selected: [action.option] }; // if existing | ||
onChange(selected); | ||
return; | ||
} | ||
// Set the option from keyboard | ||
case Action.ADD_SINGLE_KEYBOARD: { | ||
const match = state.activeDescendant.match(/\d*$/); | ||
if (!match) { | ||
return state; | ||
// single | ||
onChange([option]); | ||
setIsOpen(false); | ||
} | ||
function onKeyDown(evt) { | ||
const first = visibleIndices[0]; | ||
const last = visibleIndices[visibleIndices.length - 1]; | ||
switch (evt.key) { | ||
// select active item | ||
case KEY.ENTER: { | ||
evt.preventDefault(); | ||
addItem(options[activeDescendantIndex]); | ||
break; | ||
} | ||
const number = parseInt(match[0], 10); | ||
const newState = { ...state, isOpen: false, visibleOptions: action.options }; | ||
if (number > action.options.length - 1) { | ||
return { ...newState, created: [state.search] }; // if new | ||
// move down / up | ||
case KEY.DOWN: | ||
case KEY.UP: { | ||
evt.preventDefault(); | ||
const sum = evt.key === KEY.UP ? -1 : 1; | ||
const fallback = evt.key === KEY.UP ? last : first; // if at beginning, loop around to end, and vice-versa | ||
const nextIndex = visibleIndices.indexOf(activeDescendantIndex) + sum; | ||
const next = visibleIndices[nextIndex] !== undefined ? visibleIndices[nextIndex] : fallback; | ||
setActiveDescendantIndex(next); // set to last index if at beginning of list | ||
scrollTo(listRef.current, `#${optionId(next)}`); | ||
break; | ||
} | ||
return { ...newState, selected: [action.options[number]] }; // if existing | ||
} | ||
// Close dropdown | ||
case Action.DROPDOWN_CLOSE: { | ||
if (state.isOpen === false) { | ||
return state; // prevent other state from reseting | ||
// move to start / end | ||
case KEY.END: | ||
case KEY.HOME: { | ||
const next = evt.key === KEY.HOME ? first : last; | ||
setActiveDescendantIndex(next); | ||
scrollTo(listRef.current, `#${optionId(next)}`); | ||
break; | ||
} | ||
const activeDescendant = optionId(action.name, 0); | ||
return { | ||
...state, | ||
activeDescendant, | ||
isOpen: false, | ||
search: '', | ||
visibleOptions: action.options, | ||
}; | ||
} | ||
// Open dropdown | ||
case Action.DROPDOWN_OPEN: { | ||
if (state.isOpen === true) { | ||
return state; // prevent other state from reseting | ||
} | ||
const activeDescendant = optionId(action.name, 0); | ||
return { | ||
...state, | ||
activeDescendant, | ||
isOpen: true, | ||
search: '', | ||
visibleOptions: action.options, | ||
}; | ||
} | ||
// Invert dropdown state | ||
case Action.DROPDOWN_TOGGLE: { | ||
const nextOpen = !state.isOpen; | ||
const activeDescendant = optionId(action.name, 0); | ||
return { | ||
...state, | ||
activeDescendant, | ||
isOpen: nextOpen, | ||
search: '', | ||
visibleOptions: action.options, | ||
}; | ||
} | ||
// Move to start / end | ||
case Action.MOVE_TO_END: | ||
case Action.MOVE_TO_START: { | ||
const index = action.type === Action.MOVE_TO_START | ||
? action.options.indexOf(state.visibleOptions[0]) | ||
: action.options.indexOf(state.visibleOptions[state.visibleOptions.length - 1]); | ||
const activeDescendant = action.allowCreate === true | ||
? optionId(action.name, action.options.length) | ||
: optionId(action.name, index); | ||
if (action.wrapper) { | ||
const el = action.wrapper.querySelector(`#${activeDescendant}`); | ||
if (el) { | ||
el.scrollIntoView(false); | ||
// close | ||
case KEY.ESC: | ||
setIsOpen(false); | ||
break; | ||
default: | ||
if (noSearch) { | ||
evt.preventDefault(); | ||
} | ||
} | ||
return { ...state, activeDescendant }; | ||
break; | ||
} | ||
// Initialize dropdown & set default state (while still letting props be mutable) | ||
case Action.INIT: { | ||
// Users may specify created options within defaults, if they’re missing from the options array | ||
const defaults = action.defaults || []; | ||
const selected = action.options.filter(option => defaults.indexOf(option) !== -1); | ||
const created = defaults.filter(option => action.options.indexOf(option) === -1); | ||
return { ...state, created, selected, visibleOptions: action.options }; | ||
} | ||
// Move up or down with keyboard | ||
case Action.MOVE_NEXT: | ||
case Action.MOVE_PREV: { | ||
const match = state.activeDescendant.match(/\d*$/); // get number at end of ID | ||
if (!match) { | ||
return state; | ||
} | ||
const index = parseInt(match[0], 10); | ||
const selected = action.options[index]; | ||
const visibleIndex = state.visibleOptions.indexOf(selected); | ||
const options = [...action.options]; | ||
if (action.allowCreate === true) { | ||
options.push(state.search); // if allowCreate, allow keyboard to highlight newly-created item | ||
} | ||
let next; | ||
if (action.type === Action.MOVE_PREV) { | ||
const prevVisible = options.indexOf(state.visibleOptions[visibleIndex - 1]); | ||
const lastVisible = options.indexOf(state.visibleOptions[state.visibleOptions.length - 1]); | ||
next = prevVisible !== -1 ? prevVisible : lastVisible; | ||
} | ||
else { | ||
const nextVisible = options.indexOf(state.visibleOptions[visibleIndex + 1]); | ||
const firstVisible = options.indexOf(state.visibleOptions[0]); | ||
next = nextVisible !== -1 ? nextVisible : firstVisible; | ||
} | ||
const activeDescendant = optionId(action.name, next); | ||
if (action.wrapper) { | ||
const el = action.wrapper.querySelector(`#${activeDescendant}`); | ||
if (el) { | ||
el.scrollIntoView(false); | ||
} | ||
} | ||
return { ...state, activeDescendant }; | ||
} | ||
// Filter results | ||
case Action.SEARCH: { | ||
const visibleOptions = action.options.filter(option => new RegExp(action.search, 'i').test(option)); | ||
const options = [...action.options]; | ||
if (action.allowCreate === true) { | ||
options.push(action.search); | ||
} | ||
return { | ||
...state, | ||
activeDescendant: optionId(action.name, options.indexOf(visibleOptions[0] || state.search)), | ||
search: action.search, | ||
visibleOptions, | ||
}; | ||
} | ||
default: | ||
return state; | ||
} | ||
} | ||
const SelectZero = ({ allowCreate = false, className = 'rsz', defaultValue = [], max = Infinity, multi = false, noSearch = false, onChange, options, placeholder = 'Select', name, ...rest }) => { | ||
const [isInitialized, setIsInitialized] = useState(false); | ||
const [searchListener, setSearchListener] = useState(); | ||
const [triggerListener, setTriggerListener] = useState(); | ||
const listRef = useRef(null); | ||
const searchRef = useRef(null); | ||
const triggerRef = useRef(null); | ||
const [state, dispatch] = useReducer(reducer, initialState); | ||
function onClick(option) { | ||
if (multi === true) { | ||
dispatch({ max, name, option, options, type: Action.ADD_MULTI_CLICK }); | ||
} | ||
else { | ||
dispatch({ name, option, options, type: Action.ADD_SINGLE_CLICK }); | ||
} | ||
} | ||
// effect 1: user callback | ||
// effect 1. maintain focus | ||
useEffect(() => { | ||
if (typeof onChange === 'function') { | ||
onChange(state.selected, state.created); | ||
if (searchRef.current && isOpen) { | ||
searchRef.current.focus(); | ||
} | ||
}, [state.selected, state.created]); // eslint-disable-line react-hooks/exhaustive-deps | ||
// effect 2: initialization | ||
useEffect(() => { | ||
if (!isInitialized) { | ||
dispatch({ defaults: defaultValue, options, type: Action.INIT }); | ||
setIsInitialized(true); | ||
} | ||
}, [defaultValue, options, isInitialized]); | ||
// effect 3: maintain focus | ||
useEffect(() => { | ||
if (state.isOpen) { | ||
if (searchRef.current) { | ||
searchRef.current.focus(); | ||
} | ||
else if (triggerRef.current) { | ||
triggerRef.current.focus(); | ||
} | ||
} | ||
else if (!state.isOpen && triggerRef.current) { | ||
else if (triggerRef.current && !isOpen) { | ||
triggerRef.current.focus(); | ||
} | ||
}, [state.activeDescendant, state.isOpen]); | ||
// effect 4: navigation listener | ||
}); | ||
// effect 2. active descendant | ||
useEffect(() => { | ||
function onKeydown(evt) { | ||
// eslint-disable-next-line default-case | ||
switch (evt.key) { | ||
case KEY.ENTER: { | ||
evt.preventDefault(); | ||
dispatch({ | ||
max, | ||
options, | ||
type: multi === true ? Action.ADD_MULTI_KEYBOARD : Action.ADD_SINGLE_KEYBOARD, | ||
}); | ||
break; | ||
} | ||
// move down | ||
case KEY.DOWN: { | ||
evt.preventDefault(); | ||
dispatch({ | ||
allowCreate, | ||
name, | ||
options, | ||
type: Action.MOVE_NEXT, | ||
wrapper: listRef.current, | ||
}); | ||
break; | ||
} | ||
// move to end | ||
case KEY.END: | ||
dispatch({ | ||
allowCreate, | ||
name, | ||
options, | ||
type: Action.MOVE_TO_END, | ||
wrapper: listRef.current, | ||
}); | ||
break; | ||
// move to start | ||
case KEY.HOME: | ||
dispatch({ | ||
allowCreate, | ||
name, | ||
options, | ||
type: Action.MOVE_TO_START, | ||
wrapper: listRef.current, | ||
}); | ||
break; | ||
// move up | ||
case KEY.ESC: | ||
dispatch({ name, options, type: Action.DROPDOWN_CLOSE }); | ||
break; | ||
// close | ||
case KEY.UP: { | ||
evt.preventDefault(); | ||
dispatch({ | ||
allowCreate, | ||
name, | ||
options, | ||
type: Action.MOVE_PREV, | ||
wrapper: listRef.current, | ||
}); | ||
break; | ||
} | ||
} | ||
if (visibleIndices.indexOf(activeDescendantIndex) === -1) { | ||
setActiveDescendantIndex(visibleIndices[0]); | ||
} | ||
if (!searchListener && searchRef.current) { | ||
searchRef.current.addEventListener('keydown', onKeydown); | ||
setSearchListener(true); | ||
} | ||
}, [searchListener]); // eslint-disable-line react-hooks/exhaustive-deps | ||
// effect 5: trigger listener | ||
useEffect(() => { | ||
function onKeydown(e) { | ||
if (e.key === KEY.DOWN) { | ||
e.preventDefault(); | ||
dispatch({ name, options, type: Action.DROPDOWN_OPEN }); | ||
} | ||
} | ||
if (triggerRef.current && !triggerListener) { | ||
triggerRef.current.addEventListener('keyup', onKeydown); | ||
setTriggerListener(true); | ||
} | ||
}, [name, options, triggerListener]); | ||
const shouldDisplaySearch = noSearch !== true && options.length > 4; | ||
const shouldDisplayCreate = allowCreate !== false && | ||
state.search.length > 0 && | ||
state.visibleOptions.findIndex(option => option === state.search) === -1; | ||
// maintain focus on re-render | ||
if (searchRef.current && state.isOpen) { | ||
searchRef.current.focus(); | ||
} | ||
else if (triggerRef.current && !state.isOpen) { | ||
triggerRef.current.focus(); | ||
} | ||
const selection = [...state.selected, ...state.created]; | ||
return (React.createElement("div", Object.assign({ "aria-expanded": state.isOpen || undefined, "aria-multiselectable": multi === true || undefined, className: className, role: "listbox" }, rest), | ||
}, [activeDescendantIndex, visibleIndices]); | ||
return (React.createElement("div", Object.assign({ "aria-expanded": isOpen === true, "aria-multiselectable": multi === true || undefined, className: className, role: "listbox" }, rest), | ||
React.createElement("div", { className: "rsz__trigger" }, | ||
React.createElement("button", { "aria-controls": elId(name, 'menu'), "aria-haspopup": "listbox", className: "rsz__trigger-button", ref: triggerRef, type: "button", onClick: () => dispatch({ name, options, type: Action.DROPDOWN_TOGGLE }) }, | ||
React.createElement("button", { "aria-controls": elId('menu'), "aria-haspopup": "listbox", className: "rsz__trigger-button", onClick: () => setIsOpen(true), onKeyDown: e => { | ||
if (e.key === KEY.DOWN) { | ||
setIsOpen(true); | ||
} | ||
}, ref: triggerRef, type: "button" }, | ||
multi === true ? `Select options for ${name}` : `Select an option for ${name}`, | ||
React.createElement("span", { "aria-hidden": true, className: "rsz__arrow" }, "\u2193")), | ||
selection.length > 0 ? (React.createElement("ul", { className: "rsz__selection" }, selection.map(option => (React.createElement("li", { key: option, className: "rsz__selected" }, | ||
value.length > 0 ? (React.createElement("ul", { className: "rsz__selection" }, value.map(option => (React.createElement("li", { key: option, className: "rsz__selected" }, | ||
React.createElement("span", { className: "rsz__selected-text" }, option), | ||
React.createElement("button", { className: "rsz__selected-action", "aria-hidden": true, type: "button", onClick: () => dispatch({ max, name, option, options, type: Action.ADD_MULTI_CLICK }) }, "\u2715")))))) : (React.createElement("div", { className: "rsz__selection rsz__placeholder" }, placeholder))), | ||
React.createElement("div", { "aria-label": "Close dropdown", className: "rsz__overlay", onClick: () => dispatch({ name, options, type: Action.DROPDOWN_CLOSE }) }), | ||
React.createElement("menu", { className: "rsz__dropdown-wrapper" }, | ||
React.createElement("button", { "aria-label": `remove ${option}`, className: "rsz__selected-action", onClick: () => addItem(option), type: "button" }, "\u2715")))))) : (React.createElement("div", { className: "rsz__selection rsz__placeholder" }, placeholder))), | ||
React.createElement("div", { "aria-label": "Close dropdown", className: "rsz__overlay", onClick: () => setIsOpen(false) }), | ||
React.createElement("menu", { id: elId('menu'), className: "rsz__dropdown-wrapper" }, | ||
React.createElement("div", { className: "rsz__dropdown" }, | ||
React.createElement("input", { "aria-activedescendant": state.activeDescendant, "aria-hidden": shouldDisplaySearch === false || undefined, "aria-label": "Filter options", className: "rsz__search", onChange: e => noSearch !== true && | ||
dispatch({ allowCreate, name, options, search: e.target.value, type: Action.SEARCH }), ref: searchRef, type: "search", value: state.search }), | ||
React.createElement("input", { "aria-activedescendant": optionId(activeDescendantIndex), "aria-hidden": shouldDisplaySearch === false || undefined, "aria-label": "Filter options", className: "rsz__search", onChange: e => setSearch(e.target.value), onKeyDown: onKeyDown, ref: searchRef, type: "search", value: search }), | ||
React.createElement("div", { className: "rsz__search-icon" }), | ||
React.createElement("ul", { className: "rsz__option-list", ref: listRef }, | ||
state.visibleOptions.length > 0 ? (state.visibleOptions.map(option => { | ||
const id = optionId(name, options.indexOf(option)); | ||
const isSelected = selection.indexOf(option) !== -1; | ||
const isMaxxed = selection.length === max; | ||
visibleOptions.map((option, index) => { | ||
const isSelected = value.indexOf(option) !== -1; | ||
let ariaSelected = isSelected; | ||
if (isMaxxed) { | ||
if (ariaSelected === false && isMaxed === true) { | ||
ariaSelected = undefined; | ||
} | ||
return (React.createElement("li", { className: "rsz__option", key: option }, | ||
React.createElement("button", { "aria-selected": ariaSelected, "data-highlighted": state.activeDescendant === id || undefined, disabled: isMaxxed && !isSelected, id: id, onClick: () => onClick(option), role: "option", type: "button" }, option))); | ||
})) : (React.createElement("span", { className: "rsz__no-results" }, | ||
React.createElement("button", { "aria-selected": ariaSelected, "data-highlighted": activeDescendantIndex === visibleIndices[index] || undefined, disabled: isMaxed && !isSelected, id: optionId(visibleIndices[index]), onClick: () => addItem(option), role: "option", type: "button" }, option))); | ||
}), | ||
options.length > 0 && visibleOptions.length === 0 && (React.createElement("span", { className: "rsz__no-results" }, | ||
"No results for \u201C", | ||
state.search, | ||
search, | ||
"\u201D")), | ||
shouldDisplayCreate && (React.createElement("li", { className: "rsz__option" }, | ||
React.createElement("button", { className: "rsz__create", id: optionId(name, options.length), type: "button", onClick: () => onClick(state.search) }, | ||
React.createElement("button", { className: "rsz__create", "data-highlighted": activeDescendantIndex === options.length || undefined, disabled: isMaxed, id: optionId(options.length), onClick: () => addItem(search), type: "button" }, | ||
"Create ", | ||
React.createElement("span", { className: "rsz__search-term" }, state.search)))), | ||
allowCreate === true && state.search === '' && (React.createElement("li", { className: "rsz__option" }, | ||
React.createElement("span", { className: "rsz__search-term" }, search)))), | ||
allowCreate === true && search === '' && (React.createElement("li", { className: "rsz__option" }, | ||
React.createElement("span", { className: "rsz__create" }, "Start typing to create an item")))))), | ||
React.createElement("input", { type: "hidden", name: name, value: selection.join(',') }))); | ||
React.createElement("input", { type: "hidden", name: name, value: value.join(',') }))); | ||
}; | ||
export default SelectZero; |
@@ -1,7 +0,7 @@ | ||
import React, { ReactNode } from 'react'; | ||
import React, { ReactNode, Dispatch, SetStateAction } from 'react'; | ||
declare type Selection = string[]; | ||
declare type OnChangeCallback = (selected: Selection) => void | Dispatch<SetStateAction<Selection>>; | ||
interface SelectProps { | ||
allowCreate?: boolean; | ||
className?: string; | ||
defaultValue?: Selection; | ||
max?: number; | ||
@@ -11,9 +11,8 @@ multi?: boolean; | ||
noSearch?: boolean; | ||
onChange?: (selected: Selection, created: Selection) => void; | ||
onChange: OnChangeCallback; | ||
options: Selection; | ||
placeholder?: ReactNode; | ||
value?: Selection; | ||
} | ||
export declare function elId(name: string, component: string): string; | ||
export declare function optionId(name: string, option: number): string; | ||
declare const SelectZero: React.FunctionComponent<SelectProps>; | ||
export default SelectZero; |
@@ -1,37 +0,3 @@ | ||
import React, { useState, useRef, useReducer, useEffect } from 'react'; | ||
import React, { useRef, useState, useEffect } from 'react'; | ||
function _defineProperty(obj, key, value) { | ||
if (key in obj) { | ||
Object.defineProperty(obj, key, { | ||
value: value, | ||
enumerable: true, | ||
configurable: true, | ||
writable: true | ||
}); | ||
} else { | ||
obj[key] = value; | ||
} | ||
return obj; | ||
} | ||
function _objectSpread(target) { | ||
for (var i = 1; i < arguments.length; i++) { | ||
var source = arguments[i] != null ? arguments[i] : {}; | ||
var ownKeys = Object.keys(source); | ||
if (typeof Object.getOwnPropertySymbols === 'function') { | ||
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { | ||
return Object.getOwnPropertyDescriptor(source, sym).enumerable; | ||
})); | ||
} | ||
ownKeys.forEach(function (key) { | ||
_defineProperty(target, key, source[key]); | ||
}); | ||
} | ||
return target; | ||
} | ||
function _objectWithoutPropertiesLoose(source, excluded) { | ||
@@ -73,27 +39,2 @@ if (source == null) return {}; | ||
function elId(name, component) { | ||
return "rsz-".concat(name, "-").concat(component); | ||
} | ||
function optionId(name, option) { | ||
return elId(name, "option-".concat(option)); | ||
} | ||
var Action; | ||
(function (Action) { | ||
Action["APPEND"] = "APPEND"; | ||
Action["ADD_MULTI_CLICK"] = "ADD_MULTI_CLICK"; | ||
Action["ADD_MULTI_KEYBOARD"] = "ADD_MULTI_KEYBOARD"; | ||
Action["ADD_SINGLE_CLICK"] = "ADD_SINGLE_CLICK"; | ||
Action["ADD_SINGLE_KEYBOARD"] = "ADD_SINGLE_KEYBOARD"; | ||
Action["DROPDOWN_CLOSE"] = "DROPDOWN_CLOSE"; | ||
Action["DROPDOWN_OPEN"] = "DROPDOWN_OPEN"; | ||
Action["DROPDOWN_TOGGLE"] = "DROPDOWN_TOGGLE"; | ||
Action["INIT"] = "INIT"; | ||
Action["MOVE_NEXT"] = "MOVE_NEXT"; | ||
Action["MOVE_PREV"] = "MOVE_PREV"; | ||
Action["MOVE_TO_END"] = "MOVE_TO_END"; | ||
Action["MOVE_TO_START"] = "MOVE_TO_START"; | ||
Action["SEARCH"] = "SEARCH"; | ||
})(Action || (Action = {})); | ||
var KEY; | ||
@@ -110,289 +51,2 @@ | ||
const initialState = { | ||
activeDescendant: '0', | ||
created: [], | ||
isOpen: false, | ||
search: '', | ||
selected: [], | ||
visibleOptions: [] | ||
}; | ||
function reducer(state, action) { | ||
switch (action.type) { | ||
// Add or remove an option from mouse click | ||
case Action.ADD_MULTI_CLICK: | ||
{ | ||
const isNew = action.options.indexOf(action.option) === -1; | ||
const total = state.selected.length + state.created.length; // if new | ||
if (isNew) { | ||
const created = [...state.created]; | ||
const index = state.created.indexOf(action.option); | ||
if (index !== -1) { | ||
created.splice(index, 1); // if existing, remove | ||
} else if (total < action.max) { | ||
// if new, add unless we’re at max | ||
created.push(action.option); | ||
} | ||
return _objectSpread({}, state, { | ||
created | ||
}); | ||
} // if existing | ||
const selected = [...state.selected]; | ||
const index = state.selected.indexOf(action.option); | ||
if (index !== -1) { | ||
selected.splice(index, 1); // if existing, remove | ||
} else if (total < action.max) { | ||
selected.push(action.option); // if new, add unless we’re at max | ||
} | ||
return _objectSpread({}, state, { | ||
selected: action.options.filter(order => selected.indexOf(order) !== -1) | ||
}); | ||
} | ||
// Add or remove an option from keyboard | ||
case Action.ADD_MULTI_KEYBOARD: | ||
{ | ||
const match = state.activeDescendant.match(/\d*$/); | ||
if (!match) { | ||
return state; | ||
} | ||
const total = state.selected.length + state.created.length; | ||
const number = parseInt(match[0], 10); // if new | ||
if (number > action.options.length - 1) { | ||
const created = [...state.created]; | ||
const option = state.search; | ||
const index = state.created.indexOf(option); | ||
if (index === -1) { | ||
created.splice(index, 1); // if existing, remove | ||
} else if (total < action.max) { | ||
// if new, add unless we’re at max | ||
created.push(option); | ||
} | ||
return _objectSpread({}, state, { | ||
created | ||
}); | ||
} // if existing | ||
const option = action.options[number]; | ||
const selected = [...state.selected]; | ||
const index = state.selected.indexOf(option); | ||
if (index !== -1) { | ||
selected.splice(index, 1); // if existing, remove | ||
} else if (total < action.max) { | ||
selected.push(option); // if new, add unless we’re at max | ||
} | ||
return _objectSpread({}, state, { | ||
selected: action.options.filter(order => selected.indexOf(order) !== -1) | ||
}); | ||
} | ||
// Set the option from mouse click | ||
case Action.ADD_SINGLE_CLICK: | ||
{ | ||
const newState = _objectSpread({}, state, { | ||
isOpen: false, | ||
search: '', | ||
visibleOptions: action.options | ||
}); | ||
if (action.options.indexOf(action.option) === -1) { | ||
return _objectSpread({}, newState, { | ||
created: [action.option] | ||
}); // if new | ||
} | ||
return _objectSpread({}, newState, { | ||
selected: [action.option] | ||
}); // if existing | ||
} | ||
// Set the option from keyboard | ||
case Action.ADD_SINGLE_KEYBOARD: | ||
{ | ||
const match = state.activeDescendant.match(/\d*$/); | ||
if (!match) { | ||
return state; | ||
} | ||
const number = parseInt(match[0], 10); | ||
const newState = _objectSpread({}, state, { | ||
isOpen: false, | ||
visibleOptions: action.options | ||
}); | ||
if (number > action.options.length - 1) { | ||
return _objectSpread({}, newState, { | ||
created: [state.search] | ||
}); // if new | ||
} | ||
return _objectSpread({}, newState, { | ||
selected: [action.options[number]] | ||
}); // if existing | ||
} | ||
// Close dropdown | ||
case Action.DROPDOWN_CLOSE: | ||
{ | ||
if (state.isOpen === false) { | ||
return state; // prevent other state from reseting | ||
} | ||
const activeDescendant = optionId(action.name, 0); | ||
return _objectSpread({}, state, { | ||
activeDescendant, | ||
isOpen: false, | ||
search: '', | ||
visibleOptions: action.options | ||
}); | ||
} | ||
// Open dropdown | ||
case Action.DROPDOWN_OPEN: | ||
{ | ||
if (state.isOpen === true) { | ||
return state; // prevent other state from reseting | ||
} | ||
const activeDescendant = optionId(action.name, 0); | ||
return _objectSpread({}, state, { | ||
activeDescendant, | ||
isOpen: true, | ||
search: '', | ||
visibleOptions: action.options | ||
}); | ||
} | ||
// Invert dropdown state | ||
case Action.DROPDOWN_TOGGLE: | ||
{ | ||
const nextOpen = !state.isOpen; | ||
const activeDescendant = optionId(action.name, 0); | ||
return _objectSpread({}, state, { | ||
activeDescendant, | ||
isOpen: nextOpen, | ||
search: '', | ||
visibleOptions: action.options | ||
}); | ||
} | ||
// Move to start / end | ||
case Action.MOVE_TO_END: | ||
case Action.MOVE_TO_START: | ||
{ | ||
const index = action.type === Action.MOVE_TO_START ? action.options.indexOf(state.visibleOptions[0]) : action.options.indexOf(state.visibleOptions[state.visibleOptions.length - 1]); | ||
const activeDescendant = action.allowCreate === true ? optionId(action.name, action.options.length) : optionId(action.name, index); | ||
if (action.wrapper) { | ||
const el = action.wrapper.querySelector("#".concat(activeDescendant)); | ||
if (el) { | ||
el.scrollIntoView(false); | ||
} | ||
} | ||
return _objectSpread({}, state, { | ||
activeDescendant | ||
}); | ||
} | ||
// Initialize dropdown & set default state (while still letting props be mutable) | ||
case Action.INIT: | ||
{ | ||
// Users may specify created options within defaults, if they’re missing from the options array | ||
const defaults = action.defaults || []; | ||
const selected = action.options.filter(option => defaults.indexOf(option) !== -1); | ||
const created = defaults.filter(option => action.options.indexOf(option) === -1); | ||
return _objectSpread({}, state, { | ||
created, | ||
selected, | ||
visibleOptions: action.options | ||
}); | ||
} | ||
// Move up or down with keyboard | ||
case Action.MOVE_NEXT: | ||
case Action.MOVE_PREV: | ||
{ | ||
const match = state.activeDescendant.match(/\d*$/); // get number at end of ID | ||
if (!match) { | ||
return state; | ||
} | ||
const index = parseInt(match[0], 10); | ||
const selected = action.options[index]; | ||
const visibleIndex = state.visibleOptions.indexOf(selected); | ||
const options = [...action.options]; | ||
if (action.allowCreate === true) { | ||
options.push(state.search); // if allowCreate, allow keyboard to highlight newly-created item | ||
} | ||
let next; | ||
if (action.type === Action.MOVE_PREV) { | ||
const prevVisible = options.indexOf(state.visibleOptions[visibleIndex - 1]); | ||
const lastVisible = options.indexOf(state.visibleOptions[state.visibleOptions.length - 1]); | ||
next = prevVisible !== -1 ? prevVisible : lastVisible; | ||
} else { | ||
const nextVisible = options.indexOf(state.visibleOptions[visibleIndex + 1]); | ||
const firstVisible = options.indexOf(state.visibleOptions[0]); | ||
next = nextVisible !== -1 ? nextVisible : firstVisible; | ||
} | ||
const activeDescendant = optionId(action.name, next); | ||
if (action.wrapper) { | ||
const el = action.wrapper.querySelector("#".concat(activeDescendant)); | ||
if (el) { | ||
el.scrollIntoView(false); | ||
} | ||
} | ||
return _objectSpread({}, state, { | ||
activeDescendant | ||
}); | ||
} | ||
// Filter results | ||
case Action.SEARCH: | ||
{ | ||
const visibleOptions = action.options.filter(option => new RegExp(action.search, 'i').test(option)); | ||
const options = [...action.options]; | ||
if (action.allowCreate === true) { | ||
options.push(action.search); | ||
} | ||
return _objectSpread({}, state, { | ||
activeDescendant: optionId(action.name, options.indexOf(visibleOptions[0] || state.search)), | ||
search: action.search, | ||
visibleOptions | ||
}); | ||
} | ||
default: | ||
return state; | ||
} | ||
} | ||
const SelectZero = (_ref) => { | ||
@@ -402,5 +56,5 @@ let { | ||
className = 'rsz', | ||
defaultValue = [], | ||
max = Infinity, | ||
multi = false, | ||
name, | ||
noSearch = false, | ||
@@ -410,176 +64,140 @@ onChange, | ||
placeholder = 'Select', | ||
name | ||
value = [] | ||
} = _ref, | ||
rest = _objectWithoutProperties(_ref, ["allowCreate", "className", "defaultValue", "max", "multi", "noSearch", "onChange", "options", "placeholder", "name"]); | ||
rest = _objectWithoutProperties(_ref, ["allowCreate", "className", "max", "multi", "name", "noSearch", "onChange", "options", "placeholder", "value"]); | ||
const [isInitialized, setIsInitialized] = useState(false); | ||
const [searchListener, setSearchListener] = useState(); | ||
const [triggerListener, setTriggerListener] = useState(); | ||
// state | ||
const listRef = useRef(null); | ||
const searchRef = useRef(null); | ||
const triggerRef = useRef(null); | ||
const [state, dispatch] = useReducer(reducer, initialState); | ||
const [activeDescendantIndex, setActiveDescendantIndex] = useState(0); // Active descendant. Numbers are easier to manipulate than element IDs. | ||
function _onClick(option) { | ||
if (multi === true) { | ||
dispatch({ | ||
max, | ||
name, | ||
option, | ||
options, | ||
type: Action.ADD_MULTI_CLICK | ||
}); | ||
} else { | ||
dispatch({ | ||
name, | ||
option, | ||
options, | ||
type: Action.ADD_SINGLE_CLICK | ||
}); | ||
const [search, setSearch] = useState(''); | ||
const [isOpen, setIsOpen] = useState(false); // computed | ||
const visibleIndices = []; | ||
const visibleOptions = []; | ||
options.forEach((option, index) => { | ||
if (new RegExp(search, 'i').test(option)) { | ||
// filter results in one pass | ||
visibleOptions.push(option); | ||
visibleIndices.push(index); | ||
} | ||
} // effect 1: user callback | ||
}); | ||
const shouldDisplaySearch = noSearch !== true && options.length > 4; | ||
const shouldDisplayCreate = allowCreate === true && search.length > 0 && options.findIndex(option => option === search) === -1; | ||
const isMaxed = value.length === max; // methods | ||
function elId(component) { | ||
return "rsz-".concat(name, "-").concat(component); | ||
} | ||
useEffect(() => { | ||
if (typeof onChange === 'function') { | ||
onChange(state.selected, state.created); | ||
} | ||
}, [state.selected, state.created]); // eslint-disable-line react-hooks/exhaustive-deps | ||
// effect 2: initialization | ||
function optionId(option) { | ||
return elId("option-".concat(option)); | ||
} | ||
useEffect(() => { | ||
if (!isInitialized) { | ||
dispatch({ | ||
defaults: defaultValue, | ||
options, | ||
type: Action.INIT | ||
}); | ||
setIsInitialized(true); | ||
} | ||
}, [defaultValue, options, isInitialized]); // effect 3: maintain focus | ||
function scrollTo(wrapper, selector) { | ||
if (wrapper) { | ||
const el = wrapper.querySelector(selector); | ||
useEffect(() => { | ||
if (state.isOpen) { | ||
if (searchRef.current) { | ||
searchRef.current.focus(); | ||
} else if (triggerRef.current) { | ||
triggerRef.current.focus(); | ||
if (el) { | ||
el.scrollIntoView(false); | ||
} | ||
} else if (!state.isOpen && triggerRef.current) { | ||
triggerRef.current.focus(); | ||
} | ||
}, [state.activeDescendant, state.isOpen]); // effect 4: navigation listener | ||
} | ||
useEffect(() => { | ||
function onKeydown(evt) { | ||
// eslint-disable-next-line default-case | ||
switch (evt.key) { | ||
case KEY.ENTER: | ||
{ | ||
evt.preventDefault(); | ||
dispatch({ | ||
max, | ||
options, | ||
type: multi === true ? Action.ADD_MULTI_KEYBOARD : Action.ADD_SINGLE_KEYBOARD | ||
}); | ||
break; | ||
} | ||
// move down | ||
function addItem(option) { | ||
// multi | ||
if (multi === true) { | ||
let selected = [...value]; | ||
const index = selected.indexOf(option); | ||
case KEY.DOWN: | ||
{ | ||
evt.preventDefault(); | ||
dispatch({ | ||
allowCreate, | ||
name, | ||
options, | ||
type: Action.MOVE_NEXT, | ||
wrapper: listRef.current | ||
}); | ||
break; | ||
} | ||
// move to end | ||
if (index !== -1) { | ||
selected.splice(index, 1); // if existing, remove | ||
} else if (selected.length < max) { | ||
selected.push(option); // if new, add unless we’re at max | ||
} | ||
case KEY.END: | ||
dispatch({ | ||
allowCreate, | ||
name, | ||
options, | ||
type: Action.MOVE_TO_END, | ||
wrapper: listRef.current | ||
}); | ||
if (options.indexOf(option) !== -1) { | ||
selected = options.filter(order => selected.indexOf(order) !== -1); // if existing option, keep original order | ||
} | ||
onChange(selected); | ||
return; | ||
} // single | ||
onChange([option]); | ||
setIsOpen(false); | ||
} | ||
function onKeyDown(evt) { | ||
const first = visibleIndices[0]; | ||
const last = visibleIndices[visibleIndices.length - 1]; | ||
switch (evt.key) { | ||
// select active item | ||
case KEY.ENTER: | ||
{ | ||
evt.preventDefault(); | ||
addItem(options[activeDescendantIndex]); | ||
break; | ||
// move to start | ||
} | ||
// move down / up | ||
case KEY.HOME: | ||
dispatch({ | ||
allowCreate, | ||
name, | ||
options, | ||
type: Action.MOVE_TO_START, | ||
wrapper: listRef.current | ||
}); | ||
case KEY.DOWN: | ||
case KEY.UP: | ||
{ | ||
evt.preventDefault(); | ||
const sum = evt.key === KEY.UP ? -1 : 1; | ||
const fallback = evt.key === KEY.UP ? last : first; // if at beginning, loop around to end, and vice-versa | ||
const nextIndex = visibleIndices.indexOf(activeDescendantIndex) + sum; | ||
const next = visibleIndices[nextIndex] !== undefined ? visibleIndices[nextIndex] : fallback; | ||
setActiveDescendantIndex(next); // set to last index if at beginning of list | ||
scrollTo(listRef.current, "#".concat(optionId(next))); | ||
break; | ||
// move up | ||
} | ||
// move to start / end | ||
case KEY.ESC: | ||
dispatch({ | ||
name, | ||
options, | ||
type: Action.DROPDOWN_CLOSE | ||
}); | ||
case KEY.END: | ||
case KEY.HOME: | ||
{ | ||
const next = evt.key === KEY.HOME ? first : last; | ||
setActiveDescendantIndex(next); | ||
scrollTo(listRef.current, "#".concat(optionId(next))); | ||
break; | ||
// close | ||
} | ||
// close | ||
case KEY.UP: | ||
{ | ||
evt.preventDefault(); | ||
dispatch({ | ||
allowCreate, | ||
name, | ||
options, | ||
type: Action.MOVE_PREV, | ||
wrapper: listRef.current | ||
}); | ||
break; | ||
} | ||
} | ||
} | ||
case KEY.ESC: | ||
setIsOpen(false); | ||
break; | ||
if (!searchListener && searchRef.current) { | ||
searchRef.current.addEventListener('keydown', onKeydown); | ||
setSearchListener(true); | ||
default: | ||
if (noSearch) { | ||
evt.preventDefault(); | ||
} | ||
break; | ||
} | ||
}, [searchListener]); // eslint-disable-line react-hooks/exhaustive-deps | ||
// effect 5: trigger listener | ||
} // effect 1. maintain focus | ||
useEffect(() => { | ||
function onKeydown(e) { | ||
if (e.key === KEY.DOWN) { | ||
e.preventDefault(); | ||
dispatch({ | ||
name, | ||
options, | ||
type: Action.DROPDOWN_OPEN | ||
}); | ||
} | ||
if (searchRef.current && isOpen) { | ||
searchRef.current.focus(); | ||
} else if (triggerRef.current && !isOpen) { | ||
triggerRef.current.focus(); | ||
} | ||
}); // effect 2. active descendant | ||
if (triggerRef.current && !triggerListener) { | ||
triggerRef.current.addEventListener('keyup', onKeydown); | ||
setTriggerListener(true); | ||
useEffect(() => { | ||
if (visibleIndices.indexOf(activeDescendantIndex) === -1) { | ||
setActiveDescendantIndex(visibleIndices[0]); | ||
} | ||
}, [name, options, triggerListener]); | ||
const shouldDisplaySearch = noSearch !== true && options.length > 4; | ||
const shouldDisplayCreate = allowCreate !== false && state.search.length > 0 && state.visibleOptions.findIndex(option => option === state.search) === -1; // maintain focus on re-render | ||
if (searchRef.current && state.isOpen) { | ||
searchRef.current.focus(); | ||
} else if (triggerRef.current && !state.isOpen) { | ||
triggerRef.current.focus(); | ||
} | ||
const selection = [...state.selected, ...state.created]; | ||
}, [activeDescendantIndex, visibleIndices]); | ||
return React.createElement("div", Object.assign({ | ||
"aria-expanded": state.isOpen || undefined, | ||
"aria-expanded": isOpen === true, | ||
"aria-multiselectable": multi === true || undefined, | ||
@@ -591,18 +209,19 @@ className: className, | ||
}, React.createElement("button", { | ||
"aria-controls": elId(name, 'menu'), | ||
"aria-controls": elId('menu'), | ||
"aria-haspopup": "listbox", | ||
className: "rsz__trigger-button", | ||
onClick: () => setIsOpen(true), | ||
onKeyDown: e => { | ||
if (e.key === KEY.DOWN) { | ||
setIsOpen(true); | ||
} | ||
}, | ||
ref: triggerRef, | ||
type: "button", | ||
onClick: () => dispatch({ | ||
name, | ||
options, | ||
type: Action.DROPDOWN_TOGGLE | ||
}) | ||
type: "button" | ||
}, multi === true ? "Select options for ".concat(name) : "Select an option for ".concat(name), React.createElement("span", { | ||
"aria-hidden": true, | ||
className: "rsz__arrow" | ||
}, "\u2193")), selection.length > 0 ? React.createElement("ul", { | ||
}, "\u2193")), value.length > 0 ? React.createElement("ul", { | ||
className: "rsz__selection" | ||
}, selection.map(option => React.createElement("li", { | ||
}, value.map(option => React.createElement("li", { | ||
key: option, | ||
@@ -613,12 +232,6 @@ className: "rsz__selected" | ||
}, option), React.createElement("button", { | ||
"aria-label": "remove ".concat(option), | ||
className: "rsz__selected-action", | ||
"aria-hidden": true, | ||
type: "button", | ||
onClick: () => dispatch({ | ||
max, | ||
name, | ||
option, | ||
options, | ||
type: Action.ADD_MULTI_CLICK | ||
}) | ||
onClick: () => addItem(option), | ||
type: "button" | ||
}, "\u2715")))) : React.createElement("div", { | ||
@@ -629,8 +242,5 @@ className: "rsz__selection rsz__placeholder" | ||
className: "rsz__overlay", | ||
onClick: () => dispatch({ | ||
name, | ||
options, | ||
type: Action.DROPDOWN_CLOSE | ||
}) | ||
onClick: () => setIsOpen(false) | ||
}), React.createElement("menu", { | ||
id: elId('menu'), | ||
className: "rsz__dropdown-wrapper" | ||
@@ -640,16 +250,11 @@ }, React.createElement("div", { | ||
}, React.createElement("input", { | ||
"aria-activedescendant": state.activeDescendant, | ||
"aria-activedescendant": optionId(activeDescendantIndex), | ||
"aria-hidden": shouldDisplaySearch === false || undefined, | ||
"aria-label": "Filter options", | ||
className: "rsz__search", | ||
onChange: e => noSearch !== true && dispatch({ | ||
allowCreate, | ||
name, | ||
options, | ||
search: e.target.value, | ||
type: Action.SEARCH | ||
}), | ||
onChange: e => setSearch(e.target.value), | ||
onKeyDown: onKeyDown, | ||
ref: searchRef, | ||
type: "search", | ||
value: state.search | ||
value: search | ||
}), React.createElement("div", { | ||
@@ -660,9 +265,7 @@ className: "rsz__search-icon" | ||
ref: listRef | ||
}, state.visibleOptions.length > 0 ? state.visibleOptions.map(option => { | ||
const id = optionId(name, options.indexOf(option)); | ||
const isSelected = selection.indexOf(option) !== -1; | ||
const isMaxxed = selection.length === max; | ||
}, visibleOptions.map((option, index) => { | ||
const isSelected = value.indexOf(option) !== -1; | ||
let ariaSelected = isSelected; | ||
if (isMaxxed) { | ||
if (ariaSelected === false && isMaxed === true) { | ||
ariaSelected = undefined; | ||
@@ -676,21 +279,23 @@ } | ||
"aria-selected": ariaSelected, | ||
"data-highlighted": state.activeDescendant === id || undefined, | ||
disabled: isMaxxed && !isSelected, | ||
id: id, | ||
onClick: () => _onClick(option), | ||
"data-highlighted": activeDescendantIndex === visibleIndices[index] || undefined, | ||
disabled: isMaxed && !isSelected, | ||
id: optionId(visibleIndices[index]), | ||
onClick: () => addItem(option), | ||
role: "option", | ||
type: "button" | ||
}, option)); | ||
}) : React.createElement("span", { | ||
}), options.length > 0 && visibleOptions.length === 0 && React.createElement("span", { | ||
className: "rsz__no-results" | ||
}, "No results for \u201C", state.search, "\u201D"), shouldDisplayCreate && React.createElement("li", { | ||
}, "No results for \u201C", search, "\u201D"), shouldDisplayCreate && React.createElement("li", { | ||
className: "rsz__option" | ||
}, React.createElement("button", { | ||
className: "rsz__create", | ||
id: optionId(name, options.length), | ||
type: "button", | ||
onClick: () => _onClick(state.search) | ||
"data-highlighted": activeDescendantIndex === options.length || undefined, | ||
disabled: isMaxed, | ||
id: optionId(options.length), | ||
onClick: () => addItem(search), | ||
type: "button" | ||
}, "Create ", React.createElement("span", { | ||
className: "rsz__search-term" | ||
}, state.search))), allowCreate === true && state.search === '' && React.createElement("li", { | ||
}, search))), allowCreate === true && search === '' && React.createElement("li", { | ||
className: "rsz__option" | ||
@@ -702,3 +307,3 @@ }, React.createElement("span", { | ||
name: name, | ||
value: selection.join(',') | ||
value: value.join(',') | ||
})); | ||
@@ -708,2 +313,1 @@ }; | ||
export default SelectZero; | ||
export { elId, optionId }; |
@@ -1,1 +0,1 @@ | ||
import React,{useState,useRef,useReducer,useEffect}from"react";function _defineProperty(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function _objectSpread(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{},o=Object.keys(n);"function"==typeof Object.getOwnPropertySymbols&&(o=o.concat(Object.getOwnPropertySymbols(n).filter(function(e){return Object.getOwnPropertyDescriptor(n,e).enumerable}))),o.forEach(function(t){_defineProperty(e,t,n[t])})}return e}function _objectWithoutPropertiesLoose(e,t){if(null==e)return{};var n,o,a={},c=Object.keys(e);for(o=0;o<c.length;o++)n=c[o],t.indexOf(n)>=0||(a[n]=e[n]);return a}function _objectWithoutProperties(e,t){if(null==e)return{};var n,o,a=_objectWithoutPropertiesLoose(e,t);if(Object.getOwnPropertySymbols){var c=Object.getOwnPropertySymbols(e);for(o=0;o<c.length;o++)n=c[o],t.indexOf(n)>=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}function elId(e,t){return"rsz-".concat(e,"-").concat(t)}function optionId(e,t){return elId(e,"option-".concat(t))}var Action,KEY;!function(e){e.APPEND="APPEND",e.ADD_MULTI_CLICK="ADD_MULTI_CLICK",e.ADD_MULTI_KEYBOARD="ADD_MULTI_KEYBOARD",e.ADD_SINGLE_CLICK="ADD_SINGLE_CLICK",e.ADD_SINGLE_KEYBOARD="ADD_SINGLE_KEYBOARD",e.DROPDOWN_CLOSE="DROPDOWN_CLOSE",e.DROPDOWN_OPEN="DROPDOWN_OPEN",e.DROPDOWN_TOGGLE="DROPDOWN_TOGGLE",e.INIT="INIT",e.MOVE_NEXT="MOVE_NEXT",e.MOVE_PREV="MOVE_PREV",e.MOVE_TO_END="MOVE_TO_END",e.MOVE_TO_START="MOVE_TO_START",e.SEARCH="SEARCH"}(Action||(Action={})),function(e){e.DOWN="ArrowDown",e.END="End",e.ENTER="Enter",e.ESC="Escape",e.HOME="Home",e.UP="ArrowUp"}(KEY||(KEY={}));const initialState={activeDescendant:"0",created:[],isOpen:!1,search:"",selected:[],visibleOptions:[]};function reducer(e,t){switch(t.type){case Action.ADD_MULTI_CLICK:{const n=-1===t.options.indexOf(t.option),o=e.selected.length+e.created.length;if(n){const n=[...e.created],a=e.created.indexOf(t.option);return-1!==a?n.splice(a,1):o<t.max&&n.push(t.option),_objectSpread({},e,{created:n})}const a=[...e.selected],c=e.selected.indexOf(t.option);return-1!==c?a.splice(c,1):o<t.max&&a.push(t.option),_objectSpread({},e,{selected:t.options.filter(e=>-1!==a.indexOf(e))})}case Action.ADD_MULTI_KEYBOARD:{const n=e.activeDescendant.match(/\d*$/);if(!n)return e;const o=e.selected.length+e.created.length,a=parseInt(n[0],10);if(a>t.options.length-1){const n=[...e.created],a=e.search,c=e.created.indexOf(a);return-1===c?n.splice(c,1):o<t.max&&n.push(a),_objectSpread({},e,{created:n})}const c=t.options[a],r=[...e.selected],s=e.selected.indexOf(c);return-1!==s?r.splice(s,1):o<t.max&&r.push(c),_objectSpread({},e,{selected:t.options.filter(e=>-1!==r.indexOf(e))})}case Action.ADD_SINGLE_CLICK:{const n=_objectSpread({},e,{isOpen:!1,search:"",visibleOptions:t.options});return-1===t.options.indexOf(t.option)?_objectSpread({},n,{created:[t.option]}):_objectSpread({},n,{selected:[t.option]})}case Action.ADD_SINGLE_KEYBOARD:{const n=e.activeDescendant.match(/\d*$/);if(!n)return e;const o=parseInt(n[0],10),a=_objectSpread({},e,{isOpen:!1,visibleOptions:t.options});return o>t.options.length-1?_objectSpread({},a,{created:[e.search]}):_objectSpread({},a,{selected:[t.options[o]]})}case Action.DROPDOWN_CLOSE:if(!1===e.isOpen)return e;return _objectSpread({},e,{activeDescendant:optionId(t.name,0),isOpen:!1,search:"",visibleOptions:t.options});case Action.DROPDOWN_OPEN:if(!0===e.isOpen)return e;return _objectSpread({},e,{activeDescendant:optionId(t.name,0),isOpen:!0,search:"",visibleOptions:t.options});case Action.DROPDOWN_TOGGLE:{const n=!e.isOpen;return _objectSpread({},e,{activeDescendant:optionId(t.name,0),isOpen:n,search:"",visibleOptions:t.options})}case Action.MOVE_TO_END:case Action.MOVE_TO_START:{const n=t.type===Action.MOVE_TO_START?t.options.indexOf(e.visibleOptions[0]):t.options.indexOf(e.visibleOptions[e.visibleOptions.length-1]),o=!0===t.allowCreate?optionId(t.name,t.options.length):optionId(t.name,n);if(t.wrapper){const e=t.wrapper.querySelector("#".concat(o));e&&e.scrollIntoView(!1)}return _objectSpread({},e,{activeDescendant:o})}case Action.INIT:{const n=t.defaults||[],o=t.options.filter(e=>-1!==n.indexOf(e));return _objectSpread({},e,{created:n.filter(e=>-1===t.options.indexOf(e)),selected:o,visibleOptions:t.options})}case Action.MOVE_NEXT:case Action.MOVE_PREV:{const n=e.activeDescendant.match(/\d*$/);if(!n)return e;const o=parseInt(n[0],10),a=t.options[o],c=e.visibleOptions.indexOf(a),r=[...t.options];let s;if(!0===t.allowCreate&&r.push(e.search),t.type===Action.MOVE_PREV){const t=r.indexOf(e.visibleOptions[c-1]),n=r.indexOf(e.visibleOptions[e.visibleOptions.length-1]);s=-1!==t?t:n}else{const t=r.indexOf(e.visibleOptions[c+1]),n=r.indexOf(e.visibleOptions[0]);s=-1!==t?t:n}const i=optionId(t.name,s);if(t.wrapper){const e=t.wrapper.querySelector("#".concat(i));e&&e.scrollIntoView(!1)}return _objectSpread({},e,{activeDescendant:i})}case Action.SEARCH:{const n=t.options.filter(e=>new RegExp(t.search,"i").test(e)),o=[...t.options];return!0===t.allowCreate&&o.push(t.search),_objectSpread({},e,{activeDescendant:optionId(t.name,o.indexOf(n[0]||e.search)),search:t.search,visibleOptions:n})}default:return e}}const SelectZero=e=>{let{allowCreate:t=!1,className:n="rsz",defaultValue:o=[],max:a=1/0,multi:c=!1,noSearch:r=!1,onChange:s,options:i,placeholder:l="Select",name:p}=e,d=_objectWithoutProperties(e,["allowCreate","className","defaultValue","max","multi","noSearch","onChange","options","placeholder","name"]);const[O,u]=useState(!1),[_,E]=useState(),[f,m]=useState(),D=useRef(null),b=useRef(null),h=useRef(null),[A,N]=useReducer(reducer,initialState);function R(e){N(!0===c?{max:a,name:p,option:e,options:i,type:Action.ADD_MULTI_CLICK}:{name:p,option:e,options:i,type:Action.ADD_SINGLE_CLICK})}useEffect(()=>{"function"==typeof s&&s(A.selected,A.created)},[A.selected,A.created]),useEffect(()=>{O||(N({defaults:o,options:i,type:Action.INIT}),u(!0))},[o,i,O]),useEffect(()=>{A.isOpen?b.current?b.current.focus():h.current&&h.current.focus():!A.isOpen&&h.current&&h.current.focus()},[A.activeDescendant,A.isOpen]),useEffect(()=>{!_&&b.current&&(b.current.addEventListener("keydown",function(e){switch(e.key){case KEY.ENTER:e.preventDefault(),N({max:a,options:i,type:!0===c?Action.ADD_MULTI_KEYBOARD:Action.ADD_SINGLE_KEYBOARD});break;case KEY.DOWN:e.preventDefault(),N({allowCreate:t,name:p,options:i,type:Action.MOVE_NEXT,wrapper:D.current});break;case KEY.END:N({allowCreate:t,name:p,options:i,type:Action.MOVE_TO_END,wrapper:D.current});break;case KEY.HOME:N({allowCreate:t,name:p,options:i,type:Action.MOVE_TO_START,wrapper:D.current});break;case KEY.ESC:N({name:p,options:i,type:Action.DROPDOWN_CLOSE});break;case KEY.UP:e.preventDefault(),N({allowCreate:t,name:p,options:i,type:Action.MOVE_PREV,wrapper:D.current})}}),E(!0))},[_]),useEffect(()=>{h.current&&!f&&(h.current.addEventListener("keyup",function(e){e.key===KEY.DOWN&&(e.preventDefault(),N({name:p,options:i,type:Action.DROPDOWN_OPEN}))}),m(!0))},[p,i,f]);const v=!0!==r&&i.length>4,S=!1!==t&&A.search.length>0&&-1===A.visibleOptions.findIndex(e=>e===A.search);b.current&&A.isOpen?b.current.focus():h.current&&!A.isOpen&&h.current.focus();const I=[...A.selected,...A.created];return React.createElement("div",Object.assign({"aria-expanded":A.isOpen||void 0,"aria-multiselectable":!0===c||void 0,className:n,role:"listbox"},d),React.createElement("div",{className:"rsz__trigger"},React.createElement("button",{"aria-controls":elId(p,"menu"),"aria-haspopup":"listbox",className:"rsz__trigger-button",ref:h,type:"button",onClick:()=>N({name:p,options:i,type:Action.DROPDOWN_TOGGLE})},!0===c?"Select options for ".concat(p):"Select an option for ".concat(p),React.createElement("span",{"aria-hidden":!0,className:"rsz__arrow"},"↓")),I.length>0?React.createElement("ul",{className:"rsz__selection"},I.map(e=>React.createElement("li",{key:e,className:"rsz__selected"},React.createElement("span",{className:"rsz__selected-text"},e),React.createElement("button",{className:"rsz__selected-action","aria-hidden":!0,type:"button",onClick:()=>N({max:a,name:p,option:e,options:i,type:Action.ADD_MULTI_CLICK})},"✕")))):React.createElement("div",{className:"rsz__selection rsz__placeholder"},l)),React.createElement("div",{"aria-label":"Close dropdown",className:"rsz__overlay",onClick:()=>N({name:p,options:i,type:Action.DROPDOWN_CLOSE})}),React.createElement("menu",{className:"rsz__dropdown-wrapper"},React.createElement("div",{className:"rsz__dropdown"},React.createElement("input",{"aria-activedescendant":A.activeDescendant,"aria-hidden":!1===v||void 0,"aria-label":"Filter options",className:"rsz__search",onChange:e=>!0!==r&&N({allowCreate:t,name:p,options:i,search:e.target.value,type:Action.SEARCH}),ref:b,type:"search",value:A.search}),React.createElement("div",{className:"rsz__search-icon"}),React.createElement("ul",{className:"rsz__option-list",ref:D},A.visibleOptions.length>0?A.visibleOptions.map(e=>{const t=optionId(p,i.indexOf(e)),n=-1!==I.indexOf(e),o=I.length===a;let c=n;return o&&(c=void 0),React.createElement("li",{className:"rsz__option",key:e},React.createElement("button",{"aria-selected":c,"data-highlighted":A.activeDescendant===t||void 0,disabled:o&&!n,id:t,onClick:()=>R(e),role:"option",type:"button"},e))}):React.createElement("span",{className:"rsz__no-results"},"No results for “",A.search,"”"),S&&React.createElement("li",{className:"rsz__option"},React.createElement("button",{className:"rsz__create",id:optionId(p,i.length),type:"button",onClick:()=>R(A.search)},"Create ",React.createElement("span",{className:"rsz__search-term"},A.search))),!0===t&&""===A.search&&React.createElement("li",{className:"rsz__option"},React.createElement("span",{className:"rsz__create"},"Start typing to create an item"))))),React.createElement("input",{type:"hidden",name:p,value:I.join(",")}))};export default SelectZero;export{elId,optionId}; | ||
import React,{useRef,useState,useEffect}from"react";function _objectWithoutPropertiesLoose(e,t){if(null==e)return{};var a,n,r={},c=Object.keys(e);for(n=0;n<c.length;n++)a=c[n],t.indexOf(a)>=0||(r[a]=e[a]);return r}function _objectWithoutProperties(e,t){if(null==e)return{};var a,n,r=_objectWithoutPropertiesLoose(e,t);if(Object.getOwnPropertySymbols){var c=Object.getOwnPropertySymbols(e);for(n=0;n<c.length;n++)a=c[n],t.indexOf(a)>=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(r[a]=e[a])}return r}var KEY;!function(e){e.DOWN="ArrowDown",e.END="End",e.ENTER="Enter",e.ESC="Escape",e.HOME="Home",e.UP="ArrowUp"}(KEY||(KEY={}));const SelectZero=e=>{let{allowCreate:t=!1,className:a="rsz",max:n=1/0,multi:r=!1,name:c,noSearch:s=!1,onChange:l,options:o,placeholder:i="Select",value:u=[]}=e,m=_objectWithoutProperties(e,["allowCreate","className","max","multi","name","noSearch","onChange","options","placeholder","value"]);const p=useRef(null),d=useRef(null),E=useRef(null),[f,_]=useState(0),[h,b]=useState(""),[N,R]=useState(!1),g=[],v=[];o.forEach((e,t)=>{new RegExp(h,"i").test(e)&&(v.push(e),g.push(t))});const y=!0!==s&&o.length>4,z=!0===t&&h.length>0&&-1===o.findIndex(e=>e===h),O=u.length===n;function k(e){return"rsz-".concat(c,"-").concat(e)}function w(e){return k("option-".concat(e))}function x(e,t){if(e){const a=e.querySelector(t);a&&a.scrollIntoView(!1)}}function S(e){if(!0===r){let t=[...u];const a=t.indexOf(e);return-1!==a?t.splice(a,1):t.length<n&&t.push(e),-1!==o.indexOf(e)&&(t=o.filter(e=>-1!==t.indexOf(e))),void l(t)}l([e]),R(!1)}return useEffect(()=>{d.current&&N?d.current.focus():E.current&&!N&&E.current.focus()}),useEffect(()=>{-1===g.indexOf(f)&&_(g[0])},[f,g]),React.createElement("div",Object.assign({"aria-expanded":!0===N,"aria-multiselectable":!0===r||void 0,className:a,role:"listbox"},m),React.createElement("div",{className:"rsz__trigger"},React.createElement("button",{"aria-controls":k("menu"),"aria-haspopup":"listbox",className:"rsz__trigger-button",onClick:()=>R(!0),onKeyDown:e=>{e.key===KEY.DOWN&&R(!0)},ref:E,type:"button"},!0===r?"Select options for ".concat(c):"Select an option for ".concat(c),React.createElement("span",{"aria-hidden":!0,className:"rsz__arrow"},"↓")),u.length>0?React.createElement("ul",{className:"rsz__selection"},u.map(e=>React.createElement("li",{key:e,className:"rsz__selected"},React.createElement("span",{className:"rsz__selected-text"},e),React.createElement("button",{"aria-label":"remove ".concat(e),className:"rsz__selected-action",onClick:()=>S(e),type:"button"},"✕")))):React.createElement("div",{className:"rsz__selection rsz__placeholder"},i)),React.createElement("div",{"aria-label":"Close dropdown",className:"rsz__overlay",onClick:()=>R(!1)}),React.createElement("menu",{id:k("menu"),className:"rsz__dropdown-wrapper"},React.createElement("div",{className:"rsz__dropdown"},React.createElement("input",{"aria-activedescendant":w(f),"aria-hidden":!1===y||void 0,"aria-label":"Filter options",className:"rsz__search",onChange:e=>b(e.target.value),onKeyDown:function(e){const t=g[0],a=g[g.length-1];switch(e.key){case KEY.ENTER:e.preventDefault(),S(o[f]);break;case KEY.DOWN:case KEY.UP:{e.preventDefault();const n=e.key===KEY.UP?-1:1,r=e.key===KEY.UP?a:t,c=g.indexOf(f)+n,s=void 0!==g[c]?g[c]:r;_(s),x(p.current,"#".concat(w(s)));break}case KEY.END:case KEY.HOME:{const n=e.key===KEY.HOME?t:a;_(n),x(p.current,"#".concat(w(n)));break}case KEY.ESC:R(!1);break;default:s&&e.preventDefault()}},ref:d,type:"search",value:h}),React.createElement("div",{className:"rsz__search-icon"}),React.createElement("ul",{className:"rsz__option-list",ref:p},v.map((e,t)=>{const a=-1!==u.indexOf(e);let n=a;return!1===n&&!0===O&&(n=void 0),React.createElement("li",{className:"rsz__option",key:e},React.createElement("button",{"aria-selected":n,"data-highlighted":f===g[t]||void 0,disabled:O&&!a,id:w(g[t]),onClick:()=>S(e),role:"option",type:"button"},e))}),o.length>0&&0===v.length&&React.createElement("span",{className:"rsz__no-results"},"No results for “",h,"”"),z&&React.createElement("li",{className:"rsz__option"},React.createElement("button",{className:"rsz__create","data-highlighted":f===o.length||void 0,disabled:O,id:w(o.length),onClick:()=>S(h),type:"button"},"Create ",React.createElement("span",{className:"rsz__search-term"},h))),!0===t&&""===h&&React.createElement("li",{className:"rsz__option"},React.createElement("span",{className:"rsz__create"},"Start typing to create an item"))))),React.createElement("input",{type:"hidden",name:c,value:u.join(",")}))};export default SelectZero; |
{ | ||
"name": "@manifoldco/react-select-zero", | ||
"description": "Zero-dependency, a11y multiselect React component", | ||
"version": "0.0.1-0", | ||
"version": "0.0.1-1", | ||
"license": "ISC", | ||
@@ -6,0 +6,0 @@ "files": [ |
119
README.md
@@ -9,3 +9,3 @@ # 🥢 React Select Zero | ||
[react-select][react-select]. Supports single selection, multiselection, | ||
search, and full keyboard controls in a handsome `8 KB` component (`2.5 KB` | ||
search, and full keyboard controls in a handsome `5 KB` component (`1.8 KB` | ||
gzipped). | ||
@@ -23,3 +23,3 @@ | ||
| :------------------------------------- | ---------: | --------------: | | ||
| `@manifoldco/react-select-zero` | 🔥`8 KB`🔥 | `2.5 KB` | | ||
| `@manifoldco/react-select-zero` | 🔥`5 KB`🔥 | `1.8 KB` | | ||
| `@zendeskgarden/react-selection@6.0.1` | `26.6 KB` | `6.6 KB` | | ||
@@ -39,9 +39,14 @@ | `downshift` | `21.9 KB` | `7.1 KB` | | ||
```jsx | ||
<Select | ||
name="pokemon" | ||
options={['Bulbasaur', 'Charmander', 'Squirtle']} | ||
onChange={selected => console.log(selected)} // ['Bulbasaur'] | ||
> | ||
Select a Pokémon | ||
</Select> | ||
const [selection, setSelection] = useState([]); | ||
return ( | ||
<Select | ||
name="pokemon" | ||
options={['Bulbasaur', 'Charmander', 'Squirtle']} | ||
onChange={setSelection} // ['Bulbasaur'] | ||
value={selection} | ||
> | ||
Select a Pokémon | ||
</Select> | ||
); | ||
``` | ||
@@ -54,13 +59,50 @@ | ||
```jsx | ||
<Select name="pokemon" options={['Bulbasaur', 'Charmander', 'Squirtle']} multi> | ||
Select a Pokémon | ||
</Select> | ||
const [selection, setSelection] = useState([]); | ||
return ( | ||
<Select | ||
multi | ||
name="pokemon" | ||
onChange={setSelection} | ||
options={['Bulbasaur', 'Charmander', 'Squirtle']} | ||
value={selection} | ||
> | ||
Select a Pokémon | ||
</Select> | ||
); | ||
``` | ||
### Set initial selection | ||
```jsx | ||
const [selection, setSelection] = useState(['Bulbasaur']); | ||
return ( | ||
<Select | ||
name="pokemon" | ||
onChange={setSelection} | ||
options={['Bulbasaur', 'Charmander', 'Squirtle']} | ||
value={selection} | ||
> | ||
Select a Pokémon | ||
</Select> | ||
); | ||
``` | ||
### Hide search (shown by default) | ||
```jsx | ||
<Select name="pokemon" options={['Bulbasaur', 'Charmander', 'Squirtle']} noSearch> | ||
Select a Pokémon | ||
</Select> | ||
const [selection, setSelection] = useState([]); | ||
return ( | ||
<Select | ||
noSearch | ||
name="pokemon" | ||
onChange={setSelection} | ||
options={['Bulbasaur', 'Charmander', 'Squirtle']} | ||
value={selection} | ||
> | ||
Select a Pokémon | ||
</Select> | ||
); | ||
``` | ||
@@ -73,13 +115,15 @@ | ||
```jsx | ||
<Select | ||
name="pokemon" | ||
options={['Bulbasaur', 'Charmander', 'Squirtle']} | ||
allowCreate | ||
onChange={(selected, created) => { | ||
console.log(selected); // [] | ||
console.log(created); // ['Missingno'] | ||
}} | ||
> | ||
Select a Pokémon | ||
</Select> | ||
const [selection, setSelection] = useState([]); | ||
return ( | ||
<Select | ||
name="pokemon" | ||
options={['Bulbasaur', 'Charmander', 'Squirtle']} | ||
allowCreate | ||
onChange={setSelection} | ||
value={selection} | ||
> | ||
Select a Pokémon | ||
</Select> | ||
); | ||
``` | ||
@@ -91,13 +135,14 @@ | ||
| Name | Type | Default | Description | | ||
| :------------- | :--------- | :--------- | :---------------------------------------------------------------------------------------------- | | ||
| **`name`** | `string` | | **Required** Form name of this input. Query this like a normal form input. Also assits in a11y. | | ||
| **`options`** | `string[]` | | **Required**: Array of strings to display as options | | ||
| `allowCreate` | `boolean` | `false` | Set `<Select allowCreate />` to allow creating new entries (note: `noSearch` can’t be set) | | ||
| `defaultValue` | `string[]` | `[]` | Set `<Select defaultValue={['Charmander']} />` to set the default selected items. | | ||
| `max` | `number` | `Infinity` | Set maximum number of items (only works with `multi`) | | ||
| `multi` | `boolean` | `false` | Set `<Select multi />` to allow multiple selection | | ||
| `noSearch` | `boolean` | `false` | Set `<Select noSearch />` to hide searching (by default shows with > 5 options) | | ||
| `onChange` | `Function` | | Callback to fire when value changes | | ||
| `placeholder` | `string` | | Specify placeholder text | | ||
| Name | Type | Default | Description | | ||
| :------------- | :------------------- | :--------- | :---------------------------------------------------------------------------------------------- | | ||
| **`name`** | `string` | | **Required** Form name of this input. Query this like a normal form input. Also assits in a11y. | | ||
| **`onChange`** | `(string[]) => void` | | **Required** Form callback called when state changes | | ||
| **`options`** | `string[]` | | **Required** Array of strings to display as options | | ||
| **`value`** | `string[]` | | **Required** Set selected values | | ||
| `allowCreate` | `boolean` | `false` | Set `<Select allowCreate />` to allow creating new entries (note: `noSearch` can’t be set) | | ||
| `max` | `number` | `Infinity` | Set maximum number of items (only works with `multi`) | | ||
| `multi` | `boolean` | `false` | Set `<Select multi />` to allow multiple selection | | ||
| `noSearch` | `boolean` | `false` | Set `<Select noSearch />` to hide searching (by default shows with > 5 options) | | ||
| `onChange` | `Function` | | Callback to fire when value changes | | ||
| `placeholder` | `string` | | Specify placeholder text | | ||
@@ -104,0 +149,0 @@ ## 💅 Styling |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
205
58882
710
1