@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
1,488 lines • 52.3 kB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
var _IconPrimary, _AlignmentHelper;
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { warn, isTrue, isTouchDevice, makeUniqueId, extendPropsWithContextInClassComponent, validateDOMAttributes, dispatchCustomElementEvent, getStatusState, combineDescribedBy, convertJsxToString, escapeRegexChars, getClosestParent, keycode } from "../../shared/component-helper.js";
import { IS_MAC, IS_WIN, IS_EDGE, debounce, hasSelectedText } from "../../shared/helpers.js";
import AlignmentHelper from "../../shared/AlignmentHelper.js";
import { spacingPropTypes, createSpacingClasses } from "../space/SpacingHelper.js";
import { pickFormElementProps } from "../../shared/helpers/filterValidProps.js";
import Suffix from "../../shared/helpers/Suffix.js";
import AriaLive from "../aria-live/AriaLive.js";
import FormLabel from "../form-label/FormLabel.js";
import FormStatus from "../form-status/FormStatus.js";
import IconPrimary from "../icon-primary/IconPrimary.js";
import Input, { SubmitButton } from "../input/Input.js";
import ProgressIndicator from "../progress-indicator/ProgressIndicator.js";
import DrawerList from "../../fragments/drawer-list/DrawerList.js";
import { ItemContent } from "../../fragments/drawer-list/DrawerListItem.js";
import DrawerListContext from "../../fragments/drawer-list/DrawerListContext.js";
import DrawerListProvider from "../../fragments/drawer-list/DrawerListProvider.js";
import { drawerListPropTypes, parseContentTitle, getCurrentData, getCurrentIndex, normalizeData } from "../../fragments/drawer-list/DrawerListHelpers.js";
export default class Autocomplete extends React.PureComponent {
static defaultProps = {
id: null,
mode: 'sync',
title: 'Option Menu',
placeholder: null,
no_options: null,
show_all: null,
aria_live_options: null,
indicator_label: null,
show_options_sr: null,
selected_sr: null,
submit_button_title: null,
submit_button_icon: 'chevron_down',
input_ref: null,
icon: null,
icon_size: null,
icon_position: 'left',
triangle_position: null,
input_icon: 'loupe',
label: null,
label_direction: null,
label_sr_only: null,
keep_value: null,
keep_selection: null,
keep_value_and_selection: null,
show_clear_button: null,
status: null,
status_state: 'error',
status_props: null,
status_no_animation: null,
globalStatus: null,
suffix: null,
disable_filter: false,
disable_reorder: false,
scrollable: true,
focusable: false,
disable_highlighting: false,
max_height: null,
direction: 'auto',
skip_portal: null,
no_animation: false,
no_scroll_animation: false,
show_submit_button: false,
submit_element: null,
prevent_selection: false,
size: 'default',
align_autocomplete: null,
options_render: null,
data: null,
search_in_word_index: null,
search_numbers: null,
default_value: null,
value: 'initval',
input_value: 'initval',
autoComplete: 'off',
open_on_focus: false,
prevent_close: false,
keep_open: false,
opened: null,
disabled: null,
stretch: null,
skeleton: null,
portal_class: null,
drawer_class: null,
page_offset: null,
observer_element: null,
enable_body_lock: false,
className: null,
children: null,
on_show: null,
on_hide: null,
on_type: null,
on_focus: null,
on_blur: null,
on_change: null,
on_select: null,
on_state_update: null,
input_element: null
};
constructor(props) {
super(props);
this._id = props.id || makeUniqueId();
}
render() {
return React.createElement(DrawerListProvider, _extends({}, this.props, {
id: this._id,
data: this.props.data || this.props.children,
opened: null,
tagName: "dnb-autocomplete",
ignore_events: false,
prevent_focus: true,
skip_keysearch: true
}), React.createElement(AutocompleteInstance, _extends({}, this.props, {
id: this._id
})));
}
}
process.env.NODE_ENV !== "production" ? Autocomplete.propTypes = {
...spacingPropTypes,
...drawerListPropTypes,
id: PropTypes.string,
mode: PropTypes.oneOf(['sync', 'async']),
title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
no_options: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
show_all: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
aria_live_options: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
indicator_label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
show_options_sr: PropTypes.string,
selected_sr: PropTypes.string,
submit_button_title: PropTypes.string,
submit_button_icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.func]),
input_ref: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
icon_size: PropTypes.string,
icon_position: PropTypes.oneOf(['left', 'right']),
triangle_position: PropTypes.oneOf(['left', 'right']),
input_icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.func]),
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]),
label_direction: PropTypes.oneOf(['horizontal', 'vertical']),
label_sr_only: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
keep_value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
keep_selection: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
keep_value_and_selection: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
show_clear_button: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
status: PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.func, PropTypes.node]),
status_state: PropTypes.string,
status_props: PropTypes.object,
status_no_animation: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
globalStatus: PropTypes.shape({
id: PropTypes.string,
message: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
}),
suffix: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]),
disable_filter: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
disable_reorder: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
scrollable: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
focusable: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
disable_highlighting: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
direction: PropTypes.oneOf(['auto', 'top', 'bottom']),
max_height: PropTypes.number,
skip_portal: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
no_animation: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
no_scroll_animation: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
show_submit_button: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
submit_element: PropTypes.node,
prevent_selection: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
size: PropTypes.oneOf(['default', 'small', 'medium', 'large']),
align_autocomplete: PropTypes.oneOf(['left', 'right']),
options_render: PropTypes.oneOfType([PropTypes.object, PropTypes.func, PropTypes.node]),
input_element: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
data: PropTypes.oneOfType([PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node, PropTypes.object]), PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.oneOfType([PropTypes.string, PropTypes.node]), PropTypes.shape({
selectedKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
selected_key: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
selected_value: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
suffix_value: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
content: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.arrayOf(PropTypes.string)])
})]))]),
search_in_word_index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
searchMatch: PropTypes.oneOf(['word', 'starts-with']),
search_numbers: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
default_value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
input_value: PropTypes.string,
open_on_focus: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
prevent_close: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
keep_open: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
opened: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
disabled: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
stretch: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
skeleton: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
portal_class: PropTypes.string,
drawer_class: PropTypes.string,
page_offset: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
observer_element: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
enable_body_lock: PropTypes.bool,
class: PropTypes.string,
className: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node, PropTypes.object, PropTypes.array]),
on_show: PropTypes.func,
on_type: PropTypes.func,
on_focus: PropTypes.func,
on_blur: PropTypes.func,
on_hide: PropTypes.func,
on_change: PropTypes.func,
on_select: PropTypes.func,
on_state_update: PropTypes.func,
onClear: PropTypes.func
} : void 0;
class AutocompleteInstance extends React.PureComponent {
static defaultProps = Autocomplete.defaultProps;
static contextType = DrawerListContext;
static parseDataItem(dataItem) {
const searchWord = parseContentTitle(dataItem.search_content || dataItem, {
separator: ' '
});
if (typeof searchWord !== 'string' && Array.isArray(searchWord)) {
return AutocompleteInstance.parseDataItem(searchWord);
}
return searchWord;
}
static createSearchIndex(data) {
return data.map(dataItem => {
const contentChunk = AutocompleteInstance.parseDataItem(dataItem);
return {
dataItem,
contentChunk
};
});
}
static getCurrentDataTitle(selected_item, data) {
const currentData = getCurrentData(selected_item, data);
return parseContentTitle(currentData, {
separator: ' ',
preferSelectedValue: true
});
}
static getDerivedStateFromProps(props, state) {
if (state._listenForPropChanges) {
state.disableHighlighting = isTrue(props.disable_highlighting);
if (props.input_value !== 'initval') {
state.inputValue = props.input_value;
}
if (props?.data?.length > 0 && state?.prevData?.length === 0) {
let selectedItem = state.selected_item;
if (props.default_value) {
selectedItem = props.default_value;
}
if (!props.default_value && props.value && props.value !== 'initval') {
selectedItem = props.value;
}
const currentData = getCurrentData(selectedItem, normalizeData(props.data));
state.inputValue = parseContentTitle(currentData, {
separator: ' ',
preferSelectedValue: true
});
}
if (props.data !== state.prevData) {
state.updateData(props.data);
state.prevData = props.data;
}
}
state._listenForPropChanges = true;
return state;
}
constructor(props, context) {
super(props);
this.attributes = {};
this.state = this.state || {};
this.state._listenForPropChanges = true;
this.state.mode = props.mode;
this.state.prevData = props.data;
this.state.updateData = this.updateData;
if (context.drawerList && context.drawerList.current_title) {
this.state.inputValue = context.drawerList.current_title;
}
this._ref = React.createRef();
this._refShell = React.createRef();
this._refInput = React.createRef();
this.isTouchDevice = isTouchDevice();
this.skipFilter = isTrue(props.disable_filter);
this.skipReorder = isTrue(props.disable_reorder);
this.wasVisible = false;
}
componentDidMount() {
if (isTrue(this.props.opened)) {
this.runFilterToHighlight({
fillDataIfEmpty: true
});
this.setVisible();
}
}
componentDidUpdate(prevProps) {
if ((this.context.drawerList.opened || this.state.hasFocus) && prevProps.data !== this.props.data) {
this.setSearchIndex({
overwriteSearchIndex: true
}, () => {
this.runFilterWithSideEffects(this.state.inputValue);
});
}
if (prevProps.value !== this.props.value) {
this.revalidateSelectedItem();
this.revalidateInputValue();
}
}
componentWillUnmount() {
clearTimeout(this._selectTimeout);
clearTimeout(this._focusTimeout);
clearTimeout(this._blurTimeout);
}
setVisible = (args = null, onStateComplete = null) => {
this.wasVisible = true;
this.context.drawerList.setWrapperElement(this._ref.current).setVisible(args, onStateComplete);
};
setHidden = (args = null, onStateComplete = null) => {
this.context.drawerList.setHidden(args, onStateComplete);
this.setState({
hasFocus: false,
hasBlur: false
});
};
toggleVisible = (args = null, onStateComplete = null) => {
args = args || {};
if (typeof args.hasFilter === 'undefined') {
args.hasFilter = false;
}
if (isTrue(this.props.disabled)) {
return;
}
if (!args.hasFilter && !isTrue(this.props.prevent_close) && !this.context.drawerList.hidden && this.context.drawerList.isOpen) {
this.setHidden(null, onStateComplete);
} else {
this.setVisibleByContext(null, onStateComplete);
}
};
toggleVisibleAndFocusOptions = () => {
this.context.drawerList.toggleVisible(null, isVisible => {
if (isVisible) {
this.focusDrawerList();
}
});
};
setVisibleByContext = (options = {}, onStateComplete = null) => {
const skipFilter = this.state.showAllNextTime;
if (skipFilter) {
this.setState({
showAllNextTime: false,
_listenForPropChanges: false
});
}
this.runFilterToHighlight({
fillDataIfEmpty: true,
skipFilter,
...options
});
this.setVisible(null, onStateComplete);
};
onInputChangeHandler = ({
value,
event
}) => {
this.setState({
typedInputValue: value,
inputValue: value,
_listenForPropChanges: false
});
dispatchCustomElementEvent(this, 'on_type', {
value,
event,
...this.getEventObjects('on_type')
});
value = String(value).trim();
if (value !== this.state.inputValue) {
this.runFilterWithSideEffects(value);
}
};
runFilterWithSideEffects = (value, options = {}) => {
const data = this.runFilter(value, options);
const count = this.countData(data);
const {
keep_value,
keep_selection,
keep_value_and_selection
} = this.props;
if (value?.length > 0) {
if (count === 0) {
if (this.state.mode !== 'async') {
this.showNoOptionsItem();
}
} else if (count > 0) {
this.context.drawerList.setData(this.wrapWithShowAll(data));
this.context.drawerList.setState({
cache_hash: value + count
});
if (count === 1) {
this.context.drawerList.setState({
active_item: data[0].__id
});
}
}
} else {
if (!isTrue(keep_value) && !isTrue(keep_selection) && !isTrue(keep_value_and_selection)) {
this.totalReset();
} else if (isTrue(keep_value)) {
this.resetSelectedItem();
}
this.showAllItems();
if (this.state.inputValue?.length > 0) {
this.setVisible();
}
}
if (this.state.hasFocus) {
this.setVisible();
}
return data;
};
runFilterToHighlight = ({
fillDataIfEmpty = false,
...options
} = {}, value = this.state.inputValue) => {
const possibleTitle = AutocompleteInstance.getCurrentDataTitle(this.context.drawerList.selected_item, this.context.drawerList.original_data);
if (value === possibleTitle) {
return;
}
value = String(value || '').trim();
this.setState({
disableHighlighting: false,
_listenForPropChanges: false
});
let data = this.runFilter(value, options);
if (fillDataIfEmpty && data.length === 0 && value === '') {
data = this.context.drawerList.original_data;
}
this.context.drawerList.setData(this.wrapWithShowAll(data));
this.context.drawerList.setState({
cache_hash: value + this.countData(data)
});
return data;
};
wrapWithShowAll = data => {
if (!data || !this.hasFilterActive(data)) {
return data;
}
const lastItem = this.context.drawerList.original_data.slice(-1)[0];
if (lastItem && !lastItem.show_all) {
const lastActiveItem = data.slice(-1)[0];
if (lastActiveItem) {
data.push({
__id: lastItem.__id + 1,
lastActiveItem: lastActiveItem.__id,
class_name: 'dnb-autocomplete__show-all',
show_all: true,
active_item: false,
selected_item: false,
content: React.createElement(React.Fragment, null, _IconPrimary || (_IconPrimary = React.createElement(IconPrimary, {
icon: "arrow_down"
})), this._props.show_all)
});
}
}
return data;
};
setInputValue = inputValue => {
this.setState({
inputValue,
_listenForPropChanges: false
});
};
emptyData = () => {
this._cacheMemory = {};
this.clearInputValue();
this.context.drawerList.setData(() => [], () => {
this.setSearchIndex({
overwriteSearchIndex: true
});
this.resetActiveItem();
this.totalReset();
}, {
overwriteOriginalData: true
});
};
clearInputValue = () => {
this.setState({
inputValue: '',
typedInputValue: null,
_listenForPropChanges: false
});
};
resetInputValue = () => {
const {
input_value,
keep_value,
keep_value_and_selection
} = this.props;
if (isTrue(keep_value) || isTrue(keep_value_and_selection) || input_value !== 'initval' && input_value.length > 0) {
return;
}
clearTimeout(this._selectTimeout);
this._selectTimeout = setTimeout(() => {
if (this.hasSelectedItem()) {
const inputValue = AutocompleteInstance.getCurrentDataTitle(this.context.drawerList.selected_item, this.context.drawerList.original_data);
this.setInputValue(inputValue);
} else {
this.clearInputValue();
}
}, 1);
};
showNoOptionsItem = () => {
this.resetActiveItem();
this.ignoreEvents();
this.context.drawerList.setData(this.props.no_options === false ? [] : [{
class_name: 'dnb-autocomplete__no-options',
content: this._props.no_options,
ignore_events: true,
__id: 'no_options'
}]);
this.context.drawerList.setState({
cache_hash: 'no_options'
});
this.setVisible();
};
showIndicatorItem = () => {
this.resetActiveItem();
this.ignoreEvents();
this.context.drawerList.setData([{
class_name: 'dnb-autocomplete__indicator',
content: React.createElement(ProgressIndicator, {
label: this._props.indicator_label
}),
ignore_events: true,
__id: 'indicator'
}]);
this.context.drawerList.setState({
cache_hash: 'indicator'
});
this.setVisible();
};
showIndicator = () => {
if (!this.state.visibleIndicator) {
this.setState({
visibleIndicator: true,
_listenForPropChanges: false
});
}
};
hideIndicator = () => {
this.setState({
visibleIndicator: false,
_listenForPropChanges: false
});
};
setMode = mode => {
this.setState({
mode,
_listenForPropChanges: false
});
};
revalidateInputValue = () => {
const {
input_value,
value
} = this.props;
if (input_value && input_value !== 'initval') {
return;
}
const selected_item = getCurrentIndex(value, this.context.drawerList.original_data);
const inputValue = AutocompleteInstance.getCurrentDataTitle(selected_item, this.context.drawerList.original_data);
this.setInputValue(inputValue);
};
revalidateSelectedItem = () => {
const selected_item = getCurrentIndex(this.props.value, this.context.drawerList.original_data);
this.context.drawerList.setState({
selected_item
});
};
hasDatasetChanged = rawData => {
const {
selected_item
} = this.context.drawerList;
if (parseFloat(selected_item) > -1) {
const newItem = rawData?.[selected_item];
const oldItem = this.context.drawerList.original_data[selected_item];
if (typeof newItem?.selectedKey !== 'undefined' ? newItem?.selectedKey !== oldItem?.selectedKey : newItem?.selected_key !== oldItem?.selected_key) {
return true;
}
}
return false;
};
updateData = rawData => {
const hasChanged = this.hasDatasetChanged(rawData);
this.context.drawerList.setState({
cache_hash: 'updateData'
}, () => {
if (hasChanged) {
const {
value
} = this.props;
if (value && value !== 'initval') {
this.revalidateSelectedItem();
this.revalidateInputValue();
} else {
this.resetSelectedItem();
}
}
});
this.context.drawerList.setData(() => rawData, newData => {
this.setSearchIndex({
overwriteSearchIndex: true,
data: newData
}, () => {
const {
typedInputValue
} = this.state;
if (typedInputValue?.length > 0) {
const filteredData = this.runFilterWithSideEffects(typedInputValue);
if (this.countData(filteredData) === 0) {
if (this.state.mode !== 'async') {
this.showNoOptionsItem();
}
}
} else {
this.resetActiveItem();
if (this.context.drawerList.opened) {
this.showAllItems();
}
}
});
}, {
overwriteOriginalData: true
});
return this;
};
onInputKeyDownHandler = ({
event: e
}) => {
const key = keycode(e);
switch (key) {
case 'page up':
case 'page down':
case 'home':
case 'end':
case 'down':
case 'up':
e.preventDefault();
break;
}
switch (key) {
case 'up':
case 'down':
if (!this.context.drawerList.opened) {
this.setVisible();
}
break;
case 'escape':
case 'esc':
this.setState({
showAllNextTime: true,
_listenForPropChanges: false
});
break;
case 'enter':
e.preventDefault();
if (!this.context.drawerList.opened && this.hasFilterActive()) {
this.ignoreEvents();
this.showAll();
}
if ((!this.hasValidData() || !this.hasSelectedItem()) && !this.hasActiveItem()) {
this.toggleVisible();
} else {
this.setVisible();
}
break;
}
};
onInputClickHandler = e => {
if (!this.context.drawerList.opened && this.hasFilterActive()) {
this.ignoreEvents();
this.showAll();
}
const {
value
} = e.target;
this.setVisibleByContext({
value
});
};
onInputFocusHandler = event => {
if (this.state.skipFocusDuringChange) {
return;
}
const {
open_on_focus,
keep_value_and_selection
} = this.props;
if (!this.state.hasFocus) {
if (isTrue(open_on_focus) && this.hasValidData()) {
const {
value
} = event.target;
this.setVisibleByContext({
value
});
} else {
this.setSearchIndex();
}
if (isTrue(keep_value_and_selection)) {
this.showAll();
}
this.setState({
hasFocus: true,
hasBlur: false
});
dispatchCustomElementEvent(this, 'on_focus', {
event,
...this.getEventObjects('on_focus')
});
}
};
reserveActivityHandler = (event = null) => {
this.__preventFiringBlurEvent = Boolean(event.key === 'enter' || (event?.currentTarget ? getClosestParent('dnb-drawer-list', event.currentTarget) || getClosestParent('dnb-input__submit-button__button', event.currentTarget) : false));
if (this.__preventFiringBlurEvent) {
setTimeout(() => {
this.__preventFiringBlurEvent = false;
}, isTrue(this.props.no_animation) ? 1 : DrawerList.blurDelay);
}
};
onBlurHandler = event => {
if (this.__preventFiringBlurEvent || this.context.drawerList.hasFocusOnElement || this.state.hasBlur) {
this.__preventFiringBlurEvent = null;
return false;
}
const {
open_on_focus,
keep_value,
keep_value_and_selection,
prevent_selection,
no_animation
} = this.props;
this.setState({
hasBlur: true,
hasFocus: false
});
if (!isTrue(keep_value) && !isTrue(keep_value_and_selection)) {
this.setState({
typedInputValue: null,
_listenForPropChanges: false
});
}
if (!isTrue(prevent_selection)) {
const existingValue = this.state.inputValue;
this.resetInputValue();
const resetAfterClose = () => {
if (!isTrue(keep_value) || !existingValue || this.hasSelectedItem()) {
this.resetActiveItem();
}
this.resetFilter();
};
if (isTrue(no_animation)) {
resetAfterClose();
} else {
clearTimeout(this._blurTimeout);
this._blurTimeout = setTimeout(resetAfterClose, DrawerList.blurDelay);
}
}
if (isTrue(open_on_focus)) {
this.setHidden();
}
dispatchCustomElementEvent(this, 'on_blur', {
event,
...this.getEventObjects('on_blur')
});
};
onTriggerKeyDownHandler = e => {
const key = keycode(e);
switch (key) {
case 'space':
case 'enter':
{
this.setVisible();
}
break;
}
switch (key) {
case 'space':
case 'enter':
case 'page up':
case 'page down':
case 'down':
case 'up':
{
e.preventDefault();
this.focusInput();
}
break;
}
};
focusDrawerList = () => {
try {
this.context.drawerList._refUl.current.focus({
preventScroll: true
});
} catch (e) {}
};
focusInput = () => {
try {
this._refInput.current._ref.current.focus({
preventScroll: true
});
} catch (e) {
warn(e);
}
};
getEventObjects = key => {
const attributes = this.attributes;
return {
attributes,
dataList: this.context.drawerList.data,
updateData: this.updateData,
revalidateSelectedItem: this.revalidateSelectedItem,
revalidateInputValue: this.revalidateInputValue,
resetSelectedItem: this.resetSelectedItem,
clearInputValue: this.clearInputValue,
showAllItems: this.showAllItems,
setVisible: this.setVisible,
resetInputValue: this.resetInputValue,
setHidden: this.setHidden,
emptyData: this.emptyData,
focusInput: this.focusInput,
setInputValue: this.setInputValue,
showNoOptionsItem: this.showNoOptionsItem,
showIndicatorItem: this.showIndicatorItem,
showIndicator: this.showIndicator,
hideIndicator: this.hideIndicator,
setMode: this.setMode,
debounce: (func, props = {}, wait = 250) => {
this.dbf = this.dbf || {};
return (this.dbf[key] || (this.dbf[key] = debounce(func, wait, {
context: this
})))(props);
}
};
};
hasInjectedDataItem = (data = this.context.drawerList.data) => {
const lastItem = data.slice(-1)[0];
return lastItem ? lastItem.show_all || lastItem.__id === 'no_options' : false;
};
countData = (data = this.context.drawerList.data) => {
const count = data.length;
return count > 0 && this.hasInjectedDataItem(data) ? count - 1 : count;
};
hasValidData = (data = this.context.drawerList.data) => {
if (this.countData(data) > 0) {
const first = data[0];
if (!first.show_all && !['no_options', 'indicator'].includes(first.__id)) {
return true;
}
}
return false;
};
hasSelectedItem = () => {
return parseFloat(this.context.drawerList.selected_item) > -1;
};
hasActiveItem = () => {
return parseFloat(this.context.drawerList.active_item) > -1;
};
hasFilterActive = (data = this.context.drawerList.data) => {
return !(this.context.drawerList.original_data && this.context.drawerList.original_data.length === this.countData(data));
};
setSearchIndex({
overwriteSearchIndex = false,
data = this.context.drawerList.original_data
} = {}, cb) {
this._cacheMemory = {};
if (!overwriteSearchIndex && this.state.searchIndex) {
return this.state.searchIndex;
}
const searchIndex = AutocompleteInstance.createSearchIndex(data);
this.setState({
searchIndex,
_listenForPropChanges: false
}, cb);
return searchIndex;
}
ignoreEvents = () => {
clearTimeout(this.showAllTimeout);
this.context.drawerList.setState({
ignore_events: true
}, () => {
this.showAllTimeout = setTimeout(() => {
this.context && this.context.drawerList && this.context.drawerList.setState({
ignore_events: false
});
}, 10);
});
};
showAll = () => {
this.resetFilter();
this.context.drawerList.setState({
cache_hash: 'all'
});
this.runFilterToHighlight({
skipFilter: true,
fillDataIfEmpty: true
});
};
showAllItems = () => {
this.resetFilter();
this.context.drawerList.setState({
cache_hash: 'all'
});
this.context.drawerList.setActiveItemAndScrollToIt(this.context.drawerList.selected_item, {
scrollTo: false
});
};
totalReset = () => {
this.setState({
inputValue: null,
typedInputValue: null,
_listenForPropChanges: false
});
this.resetActiveItem();
this.resetSelectedItem();
};
resetActiveItem = () => {
this.context.drawerList.setState({
active_item: null
});
};
resetSelectedItem = () => {
const hasHadValue = this.hasSelectedItem();
this.context.drawerList.setState({
selected_item: null
}, () => {
if (hasHadValue) {
dispatchCustomElementEvent(this, 'on_change', {
...this.getEventObjects('on_change')
});
}
});
};
resetFilter = () => {
this.context.drawerList.setData(this.context.drawerList.original_data);
};
runFilter = (value, {
data = null,
searchIndex = this.state.searchIndex,
searchNumbers = isTrue(this.props.search_numbers),
inWordIndex = parseFloat((_this$props$search_in => (_this$props$search_in = this.props.search_in_word_index) !== null && _this$props$search_in !== void 0 ? _this$props$search_in : this.skipFilter ? 1 : 3)()) - 1,
disableHighlighting = false,
skipFilter = false,
skipReorder = false
} = {}) => {
if (data) {
searchIndex = this.setSearchIndex({
data
});
} else if (!searchIndex) {
searchIndex = this.setSearchIndex();
}
if (typeof searchIndex === 'undefined') {
return [];
}
const startsWithMatch = this.props.searchMatch === 'starts-with';
const rawValue = value !== null && value !== void 0 ? value : '';
let searchWords = rawValue.split(/\s+/g).filter(Boolean);
if (startsWithMatch) {
const hasLetters = /[\p{L}]/u.test(rawValue);
const hasNumbers = /[\p{N}]/u.test(rawValue);
if (startsWithMatch && searchNumbers && hasNumbers && !hasLetters) {
const normalizedNumeric = rawValue.replace(/[^\p{N}]+/gu, '');
searchWords = normalizedNumeric ? [normalizedNumeric] : [];
}
}
const getWordBoundary = wordIndex => startsWithMatch && wordIndex === 0 ? '^' : searchNumbers ? '' : '^|\\s';
const searchWordsData = searchWords.map((word, wordIndex) => {
const processedWord = searchNumbers ? word.replace(/[^\p{L}\p{N}]+/gu, '') : escapeRegexChars(word);
const wordBoundary = getWordBoundary(wordIndex);
return {
originalWord: word,
processedWord,
wordIndex,
filterRegex: new RegExp(wordIndex >= inWordIndex ? `${processedWord}` : `(${wordBoundary})${processedWord}`, 'i'),
scoreRegex: new RegExp(`(${wordBoundary})${escapeRegexChars(word)}`, 'ig')
};
});
const firstWordRegex = searchWords.length > 0 ? new RegExp(`^${escapeRegexChars(searchWords[0])}`, 'i') : null;
const findSearchWords = contentChunk => {
if (typeof contentChunk !== 'string') {
return [];
}
return searchWordsData.filter(({
filterRegex,
processedWord
}) => {
if (filterRegex.test(contentChunk)) {
return true;
}
if (searchNumbers && filterRegex.test(contentChunk.replace(/[^0-9]/g, ''))) {
return true;
}
return false;
}).map(({
originalWord,
wordIndex,
scoreRegex
}) => {
let wordScore = 0;
wordScore += (contentChunk.match(scoreRegex) || []).length;
if (wordIndex === 0 && firstWordRegex) {
const isFirstWord = firstWordRegex.test(contentChunk.split(' ')[0]);
if (isFirstWord) {
wordScore += searchWords.length + 1;
}
}
return {
word: originalWord,
wordIndex,
wordScore
};
});
};
const strS = '\uFFFE';
const strE = '\uFFFF';
searchIndex = searchIndex.map((item, i) => {
const listOfFoundWords = findSearchWords(item.contentChunk, i);
const allWordsAreNumeric = searchNumbers ? searchWords.every(word => /^[\p{N}\s.,]+$/u.test(word)) : false;
const hasMultipleNumericTerms = searchNumbers && searchWords && searchWords.length > 1 && allWordsAreNumeric;
if (hasMultipleNumericTerms && listOfFoundWords.length !== searchWords.length) {
return {
totalScore: 0,
item
};
}
if (typeof item.dataItem === 'string') {
item.dataItem = {
content: item.dataItem
};
}
if (!item.dataItem.render) {
item.dataItem = {
...item.dataItem
};
}
item.dataItem.render = (children, id) => {
if (disableHighlighting || this.state.disableHighlighting) {
return children;
}
const cacheHash = id + value;
this._cacheMemory = this._cacheMemory || {};
if (this._cacheMemory[cacheHash]) {
return this._cacheMemory[cacheHash];
}
const isComponent = typeof children !== 'string' && React.isValidElement(children);
if (isComponent && Array.isArray(children?.props?.children)) {
children = children.props.children;
} else if (!Array.isArray(children)) {
children = [children];
}
children = children.map(originalChild => ({
originalChild,
segment: convertJsxToString(originalChild, ' ')
}));
children = children.map(({
originalChild,
segment
}, idx) => {
searchWords.forEach((word, wordIndex) => {
if (segment) {
word = escapeRegexChars(word);
if (searchNumbers) {
const cleanedWord = word.replace(/[^\p{L}\p{N}]+/gu, '');
if (cleanedWord) {
const escapedWord = escapeRegexChars(cleanedWord);
segment = segment.replace(new RegExp(`(${escapedWord})`, 'gi'), match => {
if (match.includes(strS)) {
return match;
}
return `${strS}${match}${strE}`;
});
}
} else {
if (wordIndex >= inWordIndex) {
segment = segment.replace(new RegExp(`(${word})`, 'gi'), `${strS}$1${strE}`);
} else {
segment = segment.replace(new RegExp(`(${getWordBoundary(wordIndex)})(${word})`, 'gi'), `$1${strS}$2${strE}`);
}
}
}
});
let result = segment;
if (segment.includes(strS)) {
const startRepeatRegex = new RegExp(`(${strS})+`, 'g');
const endRepeatRegex = new RegExp(`(${strE})+`, 'g');
const adjacentRegex = new RegExp(`(${strE}${strS})`, 'g');
const splitRegex = new RegExp(`(${strS}|${strE})`, 'g');
const normalized = segment.replace(startRepeatRegex, strS).replace(endRepeatRegex, strE).replace(adjacentRegex, '');
const tokens = normalized.split(splitRegex).filter(Boolean);
let isHighlighted = false;
let highlightIndex = 0;
const parts = tokens.map(token => {
if (token === strS) {
isHighlighted = true;
return null;
}
if (token === strE) {
isHighlighted = false;
return null;
}
if (isHighlighted) {
const key = `highlight-${cacheHash}-${idx}-${highlightIndex++}`;
return React.createElement("span", {
key: key,
className: "dnb-drawer-list__option__item--highlight"
}, token);
}
return token;
});
result = React.createElement("span", {
key: cacheHash + idx
}, parts);
} else {
result = React.createElement("span", {
key: cacheHash + idx
}, segment);
}
if (isComponent) {
if (Array.isArray(originalChild?.props?.children)) {
result = originalChild.props.children.map(Comp => {
return Comp === originalChild || Comp.props && Comp.props.children === originalChild ? result : Comp;
});
} else if (typeof originalChild === 'string') {
result = originalChild;
}
if (React.isValidElement(originalChild)) {
result = React.cloneElement(originalChild, {
key: 'clone' + cacheHash + idx
}, result);
}
}
return result;
});
return this._cacheMemory[cacheHash] = children;
};
if (this.skipFilter || skipFilter) {
return item.dataItem;
}
let totalScore = listOfFoundWords.length;
for (const {
wordScore
} of listOfFoundWords) {
totalScore += wordScore;
}
return {
totalScore,
item
};
});
if (!this.skipFilter && !skipFilter) {
searchIndex = searchIndex.filter(({
totalScore
}) => totalScore);
if (!this.skipReorder && !skipReorder) {
searchIndex = searchIndex.sort(({
totalScore: a
}, {
totalScore: b
}) => b - a);
}
searchIndex = searchIndex.map(({
item
}) => item.dataItem);
}
return searchIndex;
};
onHideHandler = (args = {}) => {
const res = dispatchCustomElementEvent(this, 'on_hide', {
...args,
...this.getEventObjects('on_hide')
});
if (res !== false) {
this.setFocusOnInput();
}
return res;
};
setVisibleAndFocusOnInput = () => {
if (!this.state.hasFocus && !hasSelectedText()) {
this.setFocusOnInput();
this.setVisible();
}
};
setFocusOnInput() {
this.setState({
hasFocus: true
}, () => {
this.focusInput();
this.setState({
hasFocus: false
});
});
}
onSelectHandler = args => {
if (parseFloat(args.active_item) > -1) {
dispatchCustomElementEvent(this, 'on_select', {
...args,
...this.getEventObjects('on_select')
});
}
};
onPreChangeHandler = ({
data
}) => {
if (data && data.show_all) {
this.showAll();
const active_item = data.lastActiveItem;
if (parseFloat(active_item) > -1) {
this.context.drawerList.setActiveItemAndScrollToIt(active_item, {
scrollTo: false
});
}
this.setFocusOnInput();
return false;
}
};
onChangeHandler = args => {
const selected_item = args.selected_item;
const {
prevent_selection,
keep_open
} = this.props;
if (!isTrue(prevent_selection)) {
if (!isTrue(keep_open)) {
this.setState({
skipFocusDuringChange: true,
disableHighlighting: true,
_listenForPropChanges: false
});
this.setHidden();
this.focusDrawerList();
this.setState({
skipFocusDuringChange: false,
_listenForPropChanges: false
}, () => this.setFocusOnInput());
}
const inputValue = AutocompleteInstance.getCurrentDataTitle(selected_item, this.context.drawerList.data);
this.setInputValue(inputValue);
}
if (typeof args.data?.render === 'function') {
delete args.data.render;
}
dispatchCustomElementEvent(this, 'on_change', {
...args,
...this.getEventObjects('on_change')
});
};
getAriaLiveUpdate() {
const {
opened
} = this.context.drawerList;
if (opened) {
const {
aria_live_options,
no_options
} = this._props;
const count = this.countData();
let newString = null;
if (count > 0) {
newString = String(aria_live_options).replace('%s', count);
} else {
newString = no_options;
}
return newString;
}
return '';
}
getVoiceOverActiveItem(selected_sr) {
const {
active_item,
selected_item
} = this.context.drawerList;
const currentDataItem = getCurrentData(active_item, this.context.drawerList.data);
return React.createElement(AriaLive, {
hidden: !IS_MAC,
priority: "high",
delay: 0
}, currentDataItem && React.createElement(React.Fragment, null, active_item === selected_item ? React.createElement(React.Fragment, null, selected_sr, " ") : null, React.createElement(ItemContent, null, currentDataItem)));
}
render() {
const props = this._props = extendPropsWithContextInClassComponent(this.props, Autocomplete.defaultProps, this.context.getTranslation(this.props).Autocomplete, pickFormElementProps(this.context?.FormRow), pickFormElementProps(this.context?.formElement), this.context.Autocomplete);
const {
title,
placeholder,
label,
label_direction,
label_sr_only,
icon,
icon_size,
input_icon,
size,
align_autocomplete,
fixed_position,
status,
status_state,
status_props,
status_no_animation,
globalStatus,
suffix,
scrollable,
focusable,
keep_open,
keep_value,
keep_value_and_selection,
show_clear_button,
prevent_close,
no_animation,
no_scroll_animation,
show_submit_button,
submit_element,
input_element: CustomInput,
options_render,
prevent_selection,
max_height,
default_value,
search_numbers,
search_in_word_index,
searchMatch,
show_options_sr,
selected_sr,
submit_button_title,
submit_button_icon,
portal_class,
drawer_class,
input_ref,
className,
disabled,
stretch,
skeleton,
triangle_position,
icon_position,
skip_portal,
independent_width,
autoComplete,
mode: _mode,
data: _data,
children: _children,
direction: _direction,
id: _id,
opened: _opened,
value: _value,
input_value: _input_value,
indicator_label,
no_options,
show_all,
aria_live_options,
disable_highlighting,
onClear,
...attributes
} = props;
const showStatus = getStatusState(status);
const {
inputValue,
visibleIndicator
} = this.state;
const {
id,
hidden,
selected_item,
direction,
opened
} = this.context.drawerList;
const isExpanded = Boolean(opened) && this.hasValidData();
delete attributes.onBlur;
this.attributes = validateDOMAttributes(null, attributes);
Object.assign(this.context.drawerList.attributes, this.attributes);
const mainParams = {
className: classnames("dnb-autocomplete dnb-form-component", createSpacingClasses(props), className, direction && `dnb-autocomplete--${direction}`, disabled && 'dnb-autocomplete--disabled', opened && 'dnb-autocomplete--opened', label_direction && `dnb-autocomplete--${label_direction}`, icon_position && `dnb-autocomplete--icon-position-${icon_position}`, align_autocomplete && `dnb-autocomplete--${align_autocomplete}`, visibleIndicator && 'dnb-autocomplete--show-indicator', size && `dnb-autocomplete--${size}`, isTrue(stretch) && `dnb-autocomplete--stretch`, status && `dnb-autocomplete__status--${status_state}`, showStatus && 'dnb-autocomplete__form-status')
};
const shellParams = {
className: 'dnb-autocomplete__shell dnb-no-focus',
ref: this._refShell
};
const inputParams = {
className: 'dnb-autocomplete__input',
id,
value: inputValue,
autoCapitalize: 'none',
spellCheck: 'false',
autoCorrect: 'off',
autoComplete,
role: 'combobox',
'aria-autocomplete': 'both',
'aria-controls': isExpanded ? `${id}-ul` : undefined,
'aria-haspopup': 'listbox',
'aria-expanded': isExpanded,
onMouseDown: this.onInputClickHandler,
onKeyDown: this.onInputKeyDownHandler,
onChange: this.onInputChangeHandler,
onFocus: this.onInputFocusHandler,
onBlur: this.onBlurHandler,
icon_position,
inner_ref: input_ref,
disabled,
skeleton,
...attributes
};
if (!(parseFloat(selected_item) > -1)) {
inputParams.placeholder = placeholder || title;
if (!(IS_WIN && IS_EDGE)) {
inputParams['aria-placeholder'] = undefined;
}
}
if (isExpanded) {
inputParams['aria-activedescendant'] = this.context.drawerList.ariaActiveDescendant;
}
if (showStatus || suffix) {
inputParams['aria-describedby'] = combineDescribedBy(inputParams, showStatus ? id + '-status' : null, suffix ? id + '-suffix' : null);
}
let submitButton = false;
const triggerParams = {
id: id + '-submit-button',
disabled,
status: status ? status_state : null,
onKeyDown: this.onTriggerKeyDownHandler,
onSubmit: this.toggleVisible,
onMouseDown: this.reserveActivityHandler,
'aria-haspopup': 'listbox',
'aria-expanded': isExpanded,
'aria-label': !hidden ? submit_button_title : undefined,
tooltip: show_submit_button ? submit_button_title : null,
className: opened ? 'dnb-button--active' : null
};
if (submit_element && React.isValidElement(submit_element)) {
submitButton = React.cloneElement(submit_element, triggerParams);
} else if (isTrue(show_submit_button)) {
submitButton = React.createElement(SubmitButton, _extends({
icon: submit_button_icon,
icon_size: icon_size || (size === 'large' ? 'medium' : 'default'),
variant: "secondary",
size: size === 'default' ? 'medium' : size,
type: "button",
status: status,
status_state: status_state,
status_props: status_props
}, triggerParams));
}
const currentDataItem = getCurrentData(selected_item, this.context.drawerList.original_data);
const innerId = currentDataItem?.suffix_value && showStatus ? `${id}-inner` : null;
validateDOMAttributes(null, mainParams);
validateDOMAttributes(null, shellParams);
return React.createElement("span", mainParams, label && React.createElement(FormLabel, {
id: id + '-label',
forId: id,
text: label,
labelDirection: label_direction,
srOnly: label_sr_only,
disabled: disabled,
skeleton: skeleton,
onClick: this.toggleVisible
}), React.createElement("span", {
className: "dnb-autocomplete__inner",
ref: this._ref,
id: innerId
}, _AlignmentHelper || (_AlignmentHelper = React.createElement(AlignmentHelper, null)), React.createElement(FormStatus, _extends({
show: showStatus,
id: id + '-form-status',
globalStatus: globalStatus,
label: label,
text_id: id + '-status',
text: status,
state: status_state,
no_animation: status_no_animation,
skeleton: skeleton,
width_selector: innerId
}, status_props)), React.createElement("span", {
className: "dnb-autocomplete__row"
}, React.createElement("span", shellParams, CustomInput ? React.createElement(CustomInput, inputParams) : React.createElement(Input, _extends({
icon: visibleIndicator ? React.createElement(ProgressIndicator, {
size: size === 'large' ? 'medium' : 'small'
}) : input_icon,
icon_size: icon_size || (size === 'large' ? 'medium' : 'default'),
size: size,
status: status ? status_state : null,
status_state: status_state,
type: null,
inner_element: currentDataItem?.suffix_value && React.createElement("span", {
onClick: disabled ? null : this.set