@limetech/lime-elements
Version:
887 lines (886 loc) • 27.4 kB
JavaScript
import { h, } from '@stencil/core';
import { isDescendant } from '../../util/dom';
import { ARROW_DOWN, ARROW_UP, ENTER, ESCAPE, TAB } from '../../util/keycodes';
import { createRandomString } from '../../util/random-string';
import { getIconFillColor, getIconName } from '../icon/get-icon-props';
import { debounce } from 'lodash-es';
const SEARCH_DEBOUNCE = 300;
const CHIP_SET_TAG_NAME = 'limel-chip-set';
const DEFAULT_SEARCHER_MAX_RESULTS = 20;
/**
* @exampleComponent limel-example-picker-basic
* @exampleComponent limel-example-picker-multiple
* @exampleComponent limel-example-picker-icons
* @exampleComponent limel-example-picker-pictures
* @exampleComponent limel-example-picker-value-as-object
* @exampleComponent limel-example-picker-value-as-object-with-actions
* @exampleComponent limel-example-picker-empty-suggestions
* @exampleComponent limel-example-picker-leading-icon
* @exampleComponent limel-example-picker-static-actions
* @exampleComponent limel-example-picker-composite
*/
export class Picker {
constructor() {
// Should NOT be decorated with State(), since this
// should not trigger a re-render by itself.
this.chipSetEditMode = false;
this.getValueId = (item) => {
const value = item.value;
if (!!value && typeof value === 'object') {
return value.id;
}
return value;
};
this.createChips = (value) => {
if (!value) {
return [];
}
if (this.multiple) {
const listItems = value;
return listItems.map(this.createChip);
}
const listItem = value;
return [this.createChip(listItem)];
};
this.createChip = (listItem) => {
const name = getIconName(listItem.icon);
const color = getIconFillColor(listItem.icon, listItem.iconColor);
const valueId = this.getValueId(listItem);
return {
id: `${valueId}`,
text: listItem.text,
removable: true,
icon: name ? { name: name, color: color } : undefined,
image: listItem.image,
value: listItem,
menuItems: listItem.actions,
};
};
this.search = async (query) => {
const timeoutId = setTimeout(() => {
this.loading = true;
});
const searcher = this.searcher || this.defaultSearcher;
const result = (await searcher(this.textValue));
// If the search function resolves immediately,
// the loading spinner will not be shown.
clearTimeout(timeoutId);
this.handleSearchResult(query, result);
};
this.defaultSearcher = async (query) => {
if (query === '') {
return this.allItems.slice(0, DEFAULT_SEARCHER_MAX_RESULTS);
}
const filteredItems = this.allItems.filter((item) => {
let searchText = item.text.toLowerCase();
if (item.secondaryText) {
searchText =
searchText + ' ' + item.secondaryText.toLowerCase();
}
return searchText.includes(query.toLowerCase());
});
return filteredItems.slice(0, DEFAULT_SEARCHER_MAX_RESULTS);
};
this.disabled = false;
this.readonly = false;
this.label = undefined;
this.searchLabel = undefined;
this.helperText = undefined;
this.leadingIcon = undefined;
this.emptyResultMessage = undefined;
this.required = false;
this.invalid = false;
this.value = undefined;
this.searcher = undefined;
this.allItems = [];
this.multiple = false;
this.delimiter = null;
this.actions = [];
this.actionPosition = 'bottom';
this.actionScrollBehavior = 'sticky';
this.badgeIcons = false;
this.items = undefined;
this.textValue = '';
this.loading = false;
this.chips = [];
this.handleTextInput = this.handleTextInput.bind(this);
this.handleInputKeyDown = this.handleInputKeyDown.bind(this);
this.handleDropdownKeyDown = this.handleDropdownKeyDown.bind(this);
this.handleInputFieldFocus = this.handleInputFieldFocus.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleInteract = this.handleInteract.bind(this);
this.handleListChange = this.handleListChange.bind(this);
this.handleActionListChange = this.handleActionListChange.bind(this);
this.handleStopEditAndBlur = this.handleStopEditAndBlur.bind(this);
this.handleCloseMenu = this.handleCloseMenu.bind(this);
this.onListKeyDown = this.onListKeyDown.bind(this);
this.portalId = createRandomString();
this.debouncedSearch = debounce(this.search, SEARCH_DEBOUNCE);
}
componentWillLoad() {
this.chips = this.createChips(this.value);
}
componentDidLoad() {
this.chipSet = this.host.shadowRoot.querySelector(CHIP_SET_TAG_NAME);
}
disconnectedCallback() {
this.debouncedSearch.cancel();
}
async componentWillUpdate() {
this.chipSetEditMode = false;
if (this.chipSet) {
this.chipSetEditMode = await this.chipSet.getEditMode();
}
}
render() {
const props = {};
if (!this.multiple) {
props.maxItems = 1;
}
return [
h("limel-chip-set", Object.assign({ type: "input", inputType: "search", label: this.label, helperText: this.helperText, leadingIcon: this.leadingIcon, value: this.chips, disabled: this.disabled, invalid: this.invalid, delimiter: this.renderDelimiter(), readonly: this.readonly, required: this.required, searchLabel: this.searchLabel, onInput: this.handleTextInput, onKeyDown: this.handleInputKeyDown, onChange: this.handleChange, onInteract: this.handleInteract, onStartEdit: this.handleInputFieldFocus, onStopEdit: this.handleStopEditAndBlur, emptyInputOnBlur: false, clearAllButton: this.multiple && !this.chipSetEditMode }, props)),
this.renderDropdown(),
];
}
onChangeValue() {
this.chips = this.createChips(this.value);
}
renderDelimiter() {
if (this.multiple) {
return this.delimiter;
}
return null;
}
/**
* Renders the dropdown with the items to pick from, or a spinner if the picker
* is waiting for items to be received
*
* @returns picker dropdown
*/
renderDropdown() {
const dropDownContent = this.getDropdownContent();
const content = [];
if (this.shouldShowDropDownContent()) {
const actionContent = this.getActionContent();
if (this.actionPosition === 'top') {
content.push(actionContent);
}
if (dropDownContent) {
content.push(dropDownContent);
}
if (this.actionPosition === 'bottom') {
content.push(actionContent);
}
}
return this.renderPortal(content);
}
getActionContent() {
var _a, _b;
const actionCount = (_b = (_a = this.actions) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0;
if (actionCount === 0) {
return null;
}
return [
h("limel-list", { class: {
'static-actions-list': true,
'is-on-top': this.actionPosition === 'top',
'is-at-bottom': this.actionPosition === 'bottom',
'has-position-sticky': this.actionScrollBehavior === 'sticky',
}, badgeIcons: true, type: 'selectable', onChange: this.handleActionListChange, items: this.actions.map(this.removeUnusedPropertiesOnAction) }),
];
}
removeUnusedPropertiesOnAction(action) {
return Object.assign(Object.assign({}, action), { actions: [] });
}
shouldShowDropDownContent() {
if (this.isFull()) {
return false;
}
return !!this.chipSetEditMode;
}
getDropdownContent() {
var _a;
if (!this.shouldShowDropDownContent()) {
return;
}
if (this.loading) {
return this.renderSpinner();
}
if (!((_a = this.items) === null || _a === void 0 ? void 0 : _a.length)) {
return this.renderEmptyMessage();
}
return this.renderListResult();
}
/**
* Returns true if the picker is "full"
* The picker is considered to be full if it has a value and only one is allowed
*
* @returns true if the picker is full
*/
isFull() {
return !this.multiple && !!this.value;
}
renderSpinner() {
return (h("div", { style: {
width: '100%',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
padding: '1rem 0',
} }, h("limel-spinner", { limeBranded: false })));
}
renderEmptyMessage() {
if (!this.emptyResultMessage) {
return;
}
const style = {
color: 'rgb(var(--contrast-1100))',
'text-align': 'center',
margin: '0.5rem 1rem',
};
return h("p", { style: style }, this.emptyResultMessage);
}
renderListResult() {
return (h("limel-list", { badgeIcons: this.badgeIcons, onChange: this.handleListChange, onKeyDown: this.onListKeyDown, type: "selectable", items: this.items }));
}
onListKeyDown(event) {
const keyFound = [TAB, ESCAPE, ENTER].includes(event.key);
if (keyFound) {
this.chipSet.setFocus();
}
}
renderPortal(content = []) {
const dropdownZIndex = getComputedStyle(this.host).getPropertyValue('--dropdown-z-index');
return (h("limel-portal", { visible: content.length > 0, containerId: this.portalId, inheritParentWidth: true, containerStyle: { 'z-index': dropdownZIndex } }, h("limel-menu-surface", { open: content.length > 0, allowClicksElement: this.host, style: {
'--menu-surface-width': '100%',
'max-height': 'inherit',
display: 'flex',
}, onDismiss: this.handleCloseMenu }, content)));
}
/**
* Check if a descendant still has focus. If not, reset text value and search result.
*/
handleStopEditAndBlur() {
// In browsers where shadow DOM is not supported activeElement on shadowRoot will return null
// However, document.activeElement will return the actual focused element instead of the outermost shadow host
const element = this.host.shadowRoot.activeElement || document.activeElement;
const portalElement = document.querySelector(`#${this.portalId}`);
if (isDescendant(element, this.host) ||
isDescendant(element, portalElement)) {
return;
}
this.clearInputField();
}
/**
* Input handler for the input field
*
* @param event - event
*/
async handleTextInput(event) {
event.stopPropagation();
const query = event.detail;
this.textValue = query;
this.debouncedSearch(query);
// If the search-query is an empty string, bypass debouncing.
if (query === '') {
this.debouncedSearch.flush();
}
}
/**
* Change handler for the list
*
* @param event - event
*/
handleListChange(event) {
var _a;
event.stopPropagation();
if (!this.value || this.value !== event.detail) {
let newValue = event.detail;
if (this.multiple) {
newValue = [
...this.value,
event.detail,
];
}
this.change.emit(newValue);
this.items = [];
}
if (this.multiple) {
this.textValue = '';
(_a = this.chipSet) === null || _a === void 0 ? void 0 : _a.setFocus(true);
}
}
/**
* Change handler for the list
*
* @param event - event
*/
handleActionListChange(event) {
event.stopPropagation();
if (!event.detail) {
return;
}
this.action.emit(event.detail.value);
this.items = [];
}
/**
* Focus handler for the chip set
* Prevent focus if the picker has a value and does not support multiple values
*/
handleInputFieldFocus() {
const query = this.textValue;
this.debouncedSearch(query);
}
handleChange(event) {
event.stopPropagation();
let newValue = null;
if (this.multiple) {
const chips = event.detail;
newValue = chips.map((chip) => {
return this.value.find((item) => {
const valueId = this.getValueId(item);
return `${valueId}` === chip.id;
});
});
}
this.change.emit(newValue);
}
handleInteract(event) {
event.stopPropagation();
this.interact.emit(event.detail ? event.detail.value : event.detail);
}
/**
* Key handler for the input field
* Will change focus to the first/last item in the dropdown list to enable selection with the keyboard
*
* @param event - event
*/
handleInputKeyDown(event) {
const isForwardTab = event.key === TAB &&
!event.altKey &&
!event.metaKey &&
!event.shiftKey;
const isUp = event.key === ARROW_UP;
const isDown = event.key === ARROW_DOWN;
if (!isForwardTab && !isUp && !isDown) {
return;
}
const list = document.querySelector(` #${this.portalId} limel-list`);
if (!list) {
return;
}
event.preventDefault();
if (isForwardTab || isDown) {
const listElement = list.shadowRoot.querySelector('.mdc-deprecated-list-item:first-child');
listElement.focus();
return;
}
if (isUp) {
const listElement = list.shadowRoot.querySelector('.mdc-deprecated-list-item:last-child');
listElement.focus();
}
}
/**
* Key handler for the dropdown
*
* @param event - event
*/
handleDropdownKeyDown(event) {
const isEscape = event.key === ESCAPE;
if (isEscape) {
event.preventDefault();
this.textValue = '';
this.chipSet.setFocus(true);
}
}
handleSearchResult(query, result) {
if (query === this.textValue) {
this.items = result;
if (this.multiple) {
const values = this.value;
this.items = result.filter((item) => {
return !values.includes(item);
});
}
this.loading = false;
}
}
handleCloseMenu() {
if (this.items.length > 0) {
return;
}
this.clearInputField();
}
clearInputField() {
this.chipSet.emptyInput();
this.textValue = '';
this.handleSearchResult('', []);
this.debouncedSearch.cancel();
}
static get is() { return "limel-picker"; }
static get encapsulation() { return "shadow"; }
static get originalStyleUrls() {
return {
"$": ["picker.scss"]
};
}
static get styleUrls() {
return {
"$": ["picker.css"]
};
}
static get properties() {
return {
"disabled": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "True if the picker should be disabled"
},
"attribute": "disabled",
"reflect": false,
"defaultValue": "false"
},
"readonly": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Set to `true` to disable adding and removing items,\nbut allow interaction with existing items."
},
"attribute": "readonly",
"reflect": true,
"defaultValue": "false"
},
"label": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Text to display for the input field of the picker"
},
"attribute": "label",
"reflect": false
},
"searchLabel": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Search label to display in the input field when searching"
},
"attribute": "search-label",
"reflect": false
},
"helperText": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Optional helper text to display below the input field when it has focus"
},
"attribute": "helper-text",
"reflect": true
},
"leadingIcon": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Leading icon to show to the far left in the text field"
},
"attribute": "leading-icon",
"reflect": false
},
"emptyResultMessage": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "A message to display when the search returned an empty result"
},
"attribute": "empty-result-message",
"reflect": false
},
"required": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "True if the control requires a value"
},
"attribute": "required",
"reflect": false,
"defaultValue": "false"
},
"invalid": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Set to `true` to indicate that the current value of the input field is\ninvalid."
},
"attribute": "invalid",
"reflect": true,
"defaultValue": "false"
},
"value": {
"type": "unknown",
"mutable": false,
"complexType": {
"original": "ListItem<PickerValue> | Array<ListItem<PickerValue>>",
"resolved": "ListItem<PickerValue> | ListItem<PickerValue>[]",
"references": {
"ListItem": {
"location": "import",
"path": "../list/list-item.types"
},
"PickerValue": {
"location": "import",
"path": "./value.types"
},
"Array": {
"location": "global"
}
}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Currently selected value or values. Where the value can be an object."
}
},
"searcher": {
"type": "unknown",
"mutable": false,
"complexType": {
"original": "Searcher",
"resolved": "(query: string) => Promise<(ListSeparator | ListItem<any>)[]>",
"references": {
"Searcher": {
"location": "import",
"path": "../picker/searcher.types"
}
}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "A search function that takes a search-string as an argument,\nand returns a promise that will eventually be resolved with\nan array of `ListItem`:s.\n\nSee the docs for the type `Searcher` for type information on\nthe searcher function itself."
}
},
"allItems": {
"type": "unknown",
"mutable": false,
"complexType": {
"original": "Array<ListItem<PickerValue>>",
"resolved": "ListItem<PickerValue>[]",
"references": {
"Array": {
"location": "global"
},
"ListItem": {
"location": "import",
"path": "../list/list-item.types"
},
"PickerValue": {
"location": "import",
"path": "./value.types"
}
}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Only used if no `searcher` is provided. The picker will then use a\ndefault search function that filters the `allItems` based on the\n`text` and `secondaryText` properties of the items.\nThis way, custom search functions are typically only needed when the\nsearch is done on the server.\nFor performance reasons, the default searcher will never return more\nthan 20 items, but if there are more than 20 items, the rest can be\nfound by typing more characters in the search field."
},
"defaultValue": "[]"
},
"multiple": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "True if multiple values are allowed"
},
"attribute": "multiple",
"reflect": false,
"defaultValue": "false"
},
"delimiter": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Sets delimiters between chips. Works only when `multiple` is `true`."
},
"attribute": "delimiter",
"reflect": true,
"defaultValue": "null"
},
"actions": {
"type": "unknown",
"mutable": false,
"complexType": {
"original": "Array<ListItem<Action>>",
"resolved": "ListItem<Action>[]",
"references": {
"Array": {
"location": "global"
},
"ListItem": {
"location": "import",
"path": "../list/list-item.types"
},
"Action": {
"location": "import",
"path": "../collapsible-section/action"
}
}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Static actions that can be clicked by the user."
},
"defaultValue": "[]"
},
"actionPosition": {
"type": "string",
"mutable": false,
"complexType": {
"original": "ActionPosition",
"resolved": "\"bottom\" | \"top\"",
"references": {
"ActionPosition": {
"location": "import",
"path": "../picker/actions.types"
}
}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Position of the custom static actions in the picker's results dropdown.\nCan be set to `'top'` or `'bottom'`."
},
"attribute": "action-position",
"reflect": false,
"defaultValue": "'bottom'"
},
"actionScrollBehavior": {
"type": "string",
"mutable": false,
"complexType": {
"original": "ActionScrollBehavior",
"resolved": "\"scroll\" | \"sticky\"",
"references": {
"ActionScrollBehavior": {
"location": "import",
"path": "../picker/actions.types"
}
}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Scroll behavior of the custom static actions, when user scrolls\nin the picker's results dropdown. Can be set to `'scroll'` which means\nthe action items will scroll together with the list, or `'sticky'` which\nretains their position at the top or bottom of the drop down while\nscrolling."
},
"attribute": "action-scroll-behavior",
"reflect": false,
"defaultValue": "'sticky'"
},
"badgeIcons": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Whether badge icons should be used in the result list or not"
},
"attribute": "badge-icons",
"reflect": true,
"defaultValue": "false"
}
};
}
static get states() {
return {
"items": {},
"textValue": {},
"loading": {},
"chips": {}
};
}
static get events() {
return [{
"method": "change",
"name": "change",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Fired when a new value has been selected from the picker"
},
"complexType": {
"original": "ListItem<PickerValue> | Array<ListItem<PickerValue>>",
"resolved": "ListItem<PickerValue> | ListItem<PickerValue>[]",
"references": {
"ListItem": {
"location": "import",
"path": "../list/list-item.types"
},
"PickerValue": {
"location": "import",
"path": "./value.types"
},
"Array": {
"location": "global"
}
}
}
}, {
"method": "interact",
"name": "interact",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Fired when clicking on a selected value"
},
"complexType": {
"original": "ListItem<PickerValue>",
"resolved": "ListItem<PickerValue>",
"references": {
"ListItem": {
"location": "import",
"path": "../list/list-item.types"
},
"PickerValue": {
"location": "import",
"path": "./value.types"
}
}
}
}, {
"method": "action",
"name": "action",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Emitted when the user selects an action."
},
"complexType": {
"original": "Action",
"resolved": "Action",
"references": {
"Action": {
"location": "import",
"path": "../collapsible-section/action"
}
}
}
}];
}
static get elementRef() { return "host"; }
static get watchers() {
return [{
"propName": "value",
"methodName": "onChangeValue"
}];
}
}
//# sourceMappingURL=picker.js.map