@stories-js/core
Version:
Stories web components to build stories
647 lines • 19.9 kB
JavaScript
/*!
* (C) StoriesJS https://storiesjs.org - GPL-2.0 License
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Component, Host, h, Prop, Element, State, Watch, Event, Method, forceUpdate } from '@stencil/core';
import { debounceEvent, raf } from '../../helpers';
import { createColorClasses } from '../../utils';
export class Searchbar {
constructor() {
this.shouldAlignLeft = true;
this.focused = false;
/**
* Set the cancel button icon. Only applies to `md` mode.
* Defaults to `"arrow-back-sharp"`.
*/
this.cancelButtonIcon = 'arrow-back-sharp';
/**
* Set the amount of time, in milliseconds, to wait to trigger the `ionChange` event after each keystroke. This also impacts form bindings such as `ngModel` or `v-model`.
*/
this.debounce = 250;
/**
* If `true`, the user cannot interact with the input.
*/
this.disabled = false;
/**
* Set the input's placeholder.
* `placeholder` can accept either plaintext or HTML as a string.
* To display characters normally reserved for HTML, they
* must be escaped. For example `<Stories>` would become
* `<Stories>`
*/
this.placeholder = 'Search';
/**
* Sets the behavior for the cancel button. Defaults to `"never"`.
* Setting to `"focus"` shows the cancel button on focus.
* Setting to `"never"` hides the cancel button.
* Setting to `"always"` shows the cancel button regardless
* of focus state.
*/
this.showCancelButton = 'never';
/**
* Sets the behavior for the clear button. Defaults to `"focus"`.
* Setting to `"focus"` shows the clear button on focus if the
* input is not empty.
* Setting to `"never"` hides the clear button.
* Setting to `"always"` shows the clear button regardless
* of focus state, but only if the input is not empty.
*/
this.showClearButton = 'always';
/**
* Set the type of the input.
*/
this.type = 'search';
/**
* the value of the searchbar.
*/
this.value = '';
/**
* Clears the input field and triggers the control change.
*/
this.onClearInput = (ev, shouldFocus) => {
this.storiesClear.emit();
if (ev) {
ev.preventDefault();
ev.stopPropagation();
}
// wait for 4 frames
setTimeout(() => {
const value = this.getValue();
if (value !== '') {
this.value = '';
this.storiesInput.emit();
/**
* When tapping clear button
* ensure input is focused after
* clearing input so users
* can quickly start typing.
*/
if (shouldFocus && !this.focused) {
this.setFocus();
}
}
}, 16 * 4);
};
/**
* Update the Searchbar input value when the input changes
*/
this.onInput = (ev) => {
const input = ev.target;
if (input) {
this.value = input.value;
}
this.storiesInput.emit(ev);
};
/**
* Sets the Searchbar to not focused and checks if it should align left
* based on whether there is a value in the searchbar or not.
*/
this.onBlur = () => {
this.focused = false;
this.storiesBlur.emit();
this.positionElements();
};
/**
* Sets the Searchbar to focused and active on input focus.
*/
this.onFocus = () => {
this.focused = true;
this.storiesFocus.emit();
this.positionElements();
};
}
debounceChanged() {
this.storiesChange = debounceEvent(this.storiesChange, this.debounce);
}
valueChanged() {
const inputEl = this.nativeInput;
const value = this.getValue();
if (inputEl && inputEl.value !== value) {
inputEl.value = value;
}
this.storiesChange.emit({ value });
}
showCancelButtonChanged() {
requestAnimationFrame(() => {
this.positionElements();
forceUpdate(this);
});
}
connectedCallback() {
this.emitStyle();
}
componentDidLoad() {
this.positionElements();
this.debounceChanged();
}
emitStyle() {
this.storiesStyle.emit({
'searchbar': true
});
}
/**
* Sets focus on the specified `stories-searchbar`. Use this method instead of the global
* `input.focus()`.
*/
async setFocus() {
if (this.nativeInput) {
this.nativeInput.focus();
}
}
/**
* Returns the native `<input>` element used under the hood.
*/
getInputElement() {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return Promise.resolve(this.nativeInput);
}
/**
* Positions the input search icon, placeholder, and the cancel button
* based on the input value and if it is focused. (ios only)
*/
positionElements() {
const value = this.getValue();
const prevAlignLeft = this.shouldAlignLeft;
const shouldAlignLeft = (value.trim() !== '' || !!this.focused);
this.shouldAlignLeft = shouldAlignLeft;
if (prevAlignLeft !== shouldAlignLeft) {
this.positionPlaceholder();
}
}
/**
* Positions the input placeholder
*/
positionPlaceholder() {
const inputEl = this.nativeInput;
if (!inputEl) {
return;
}
const isRTL = document.dir === 'rtl';
const iconEl = (this.el.shadowRoot || this.el).querySelector('.searchbar-search-icon');
if (this.shouldAlignLeft) {
inputEl.removeAttribute('style');
iconEl.removeAttribute('style');
}
else {
// Create a dummy span to get the placeholder width
const doc = document;
const tempSpan = doc.createElement('span');
tempSpan.innerText = this.placeholder || '';
doc.body.appendChild(tempSpan);
// Get the width of the span then remove it
raf(() => {
const textWidth = tempSpan.offsetWidth;
tempSpan.remove();
// Calculate the input padding
const inputLeft = 'calc(50% - ' + (textWidth / 2) + 'px)';
// Calculate the icon margin
const iconLeft = 'calc(50% - ' + ((textWidth / 2) + 30) + 'px)';
// Set the input padding start and icon margin start
if (isRTL) {
inputEl.style.paddingRight = inputLeft;
iconEl.style.marginRight = iconLeft;
}
else {
inputEl.style.paddingLeft = inputLeft;
iconEl.style.marginLeft = iconLeft;
}
});
}
}
getValue() {
return this.value || '';
}
hasValue() {
return this.getValue() !== '';
}
/**
* Determines whether or not the clear button should be visible onscreen.
* Clear button should be shown if one of two conditions applies:
* 1. `showClearButton` is set to `always`.
* 2. `showClearButton` is set to `focus`, and the searchbar has been focused.
*/
shouldShowClearButton() {
if ((this.showClearButton === 'never') || (this.showClearButton === 'focus' && !this.focused)) {
return false;
}
return true;
}
render() {
const clearIcon = this.clearIcon || 'close-sharp';
const searchIcon = this.searchIcon || 'search-sharp';
return (h(Host, { "aria-disabled": this.disabled ? 'true' : null, class: createColorClasses(this.color, {
'searchbar-disabled': this.disabled,
'searchbar-has-value': this.hasValue(),
'searchbar-left-aligned': this.shouldAlignLeft,
'searchbar-has-focus': this.focused,
'searchbar-should-show-clear': this.shouldShowClearButton(),
}), role: "search" },
h("div", { class: "searchbar-input-container" },
h("input", { ref: el => this.nativeInput = el, "aria-label": "search text", class: "searchbar-input", disabled: this.disabled, inputMode: this.inputmode, onBlur: this.onBlur, onFocus: this.onFocus, onInput: this.onInput, placeholder: this.placeholder, type: this.type, value: this.getValue() }),
h("stories-icon", { "aria-hidden": "true", class: "searchbar-search-icon", name: searchIcon }),
h("button", { "aria-label": "reset", class: "searchbar-clear-button", onMouseDown: ev => this.onClearInput(ev, true), onTouchStart: ev => this.onClearInput(ev, true), type: "button", "no-blur": true },
h("stories-icon", { "aria-hidden": "true", class: "searchbar-clear-icon", name: clearIcon })))));
}
static get is() { return "stories-searchbar"; }
static get encapsulation() { return "shadow"; }
static get originalStyleUrls() { return {
"$": ["searchbar.scss"]
}; }
static get styleUrls() { return {
"$": ["searchbar.css"]
}; }
static get properties() { return {
"color": {
"type": "string",
"mutable": false,
"complexType": {
"original": "Color",
"resolved": "string",
"references": {
"Color": {
"location": "import",
"path": "../../types"
}
}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "The color to use from your application's color palette.\nDefault options are: `\"primary\"`, `\"secondary\"`, `\"tertiary\"`, `\"success\"`, `\"warning\"`, `\"danger\"`, `\"light\"`, `\"medium\"`, and `\"dark\"`.\nFor more information on colors, see [theming](/docs/theming/basics)."
},
"attribute": "color",
"reflect": true
},
"cancelButtonIcon": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Set the cancel button icon. Only applies to `md` mode.\nDefaults to `\"arrow-back-sharp\"`."
},
"attribute": "cancel-button-icon",
"reflect": false,
"defaultValue": "'arrow-back-sharp'"
},
"clearIcon": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Set the clear icon. Defaults to `\"close-circle\"` for `ios` and `\"close-sharp\"` for `md`."
},
"attribute": "clear-icon",
"reflect": false
},
"debounce": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Set the amount of time, in milliseconds, to wait to trigger the `ionChange` event after each keystroke. This also impacts form bindings such as `ngModel` or `v-model`."
},
"attribute": "debounce",
"reflect": false,
"defaultValue": "250"
},
"disabled": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "If `true`, the user cannot interact with the input."
},
"attribute": "disabled",
"reflect": false,
"defaultValue": "false"
},
"inputmode": {
"type": "string",
"mutable": false,
"complexType": {
"original": "'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'",
"resolved": "\"decimal\" | \"email\" | \"none\" | \"numeric\" | \"search\" | \"tel\" | \"text\" | \"url\"",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "A hint to the browser for which keyboard to display.\nPossible values: `\"none\"`, `\"text\"`, `\"tel\"`, `\"url\"`,\n`\"email\"`, `\"numeric\"`, `\"decimal\"`, and `\"search\"`."
},
"attribute": "inputmode",
"reflect": false
},
"placeholder": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Set the input's placeholder.\n`placeholder` can accept either plaintext or HTML as a string.\nTo display characters normally reserved for HTML, they\nmust be escaped. For example `<Stories>` would become\n`<Stories>`"
},
"attribute": "placeholder",
"reflect": false,
"defaultValue": "'Search'"
},
"searchIcon": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "The icon to use as the search icon. Defaults to `\"search-outline\"` in\n`ios` mode and `\"search-sharp\"` in `md` mode."
},
"attribute": "search-icon",
"reflect": false
},
"showCancelButton": {
"type": "string",
"mutable": false,
"complexType": {
"original": "'never' | 'focus' | 'always'",
"resolved": "\"always\" | \"focus\" | \"never\"",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Sets the behavior for the cancel button. Defaults to `\"never\"`.\nSetting to `\"focus\"` shows the cancel button on focus.\nSetting to `\"never\"` hides the cancel button.\nSetting to `\"always\"` shows the cancel button regardless\nof focus state."
},
"attribute": "show-cancel-button",
"reflect": false,
"defaultValue": "'never'"
},
"showClearButton": {
"type": "string",
"mutable": false,
"complexType": {
"original": "'never' | 'focus' | 'always'",
"resolved": "\"always\" | \"focus\" | \"never\"",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Sets the behavior for the clear button. Defaults to `\"focus\"`.\nSetting to `\"focus\"` shows the clear button on focus if the\ninput is not empty.\nSetting to `\"never\"` hides the clear button.\nSetting to `\"always\"` shows the clear button regardless\nof focus state, but only if the input is not empty."
},
"attribute": "show-clear-button",
"reflect": false,
"defaultValue": "'always'"
},
"type": {
"type": "string",
"mutable": false,
"complexType": {
"original": "'text' | 'password' | 'email' | 'number' | 'search' | 'tel' | 'url'",
"resolved": "\"email\" | \"number\" | \"password\" | \"search\" | \"tel\" | \"text\" | \"url\"",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Set the type of the input."
},
"attribute": "type",
"reflect": false,
"defaultValue": "'search'"
},
"value": {
"type": "string",
"mutable": true,
"complexType": {
"original": "string | null",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "the value of the searchbar."
},
"attribute": "value",
"reflect": false,
"defaultValue": "''"
}
}; }
static get states() { return {
"focused": {}
}; }
static get events() { return [{
"method": "storiesInput",
"name": "storiesInput",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Emitted when a keyboard input occurred."
},
"complexType": {
"original": "KeyboardEvent",
"resolved": "KeyboardEvent",
"references": {
"KeyboardEvent": {
"location": "global"
}
}
}
}, {
"method": "storiesChange",
"name": "storiesChange",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Emitted when the value has changed."
},
"complexType": {
"original": "SearchbarChangeEventDetail",
"resolved": "SearchbarChangeEventDetail",
"references": {
"SearchbarChangeEventDetail": {
"location": "import",
"path": "../../types"
}
}
}
}, {
"method": "storiesCancel",
"name": "storiesCancel",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Emitted when the cancel button is clicked."
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}, {
"method": "storiesClear",
"name": "storiesClear",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Emitted when the clear input button is clicked."
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}, {
"method": "storiesBlur",
"name": "storiesBlur",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Emitted when the input loses focus."
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}, {
"method": "storiesFocus",
"name": "storiesFocus",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Emitted when the input has focus."
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}, {
"method": "storiesStyle",
"name": "storiesStyle",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [{
"name": "internal",
"text": undefined
}],
"text": "Emitted when the styles change."
},
"complexType": {
"original": "StyleEventDetail",
"resolved": "StyleEventDetail",
"references": {
"StyleEventDetail": {
"location": "import",
"path": "../../types"
}
}
}
}]; }
static get methods() { return {
"setFocus": {
"complexType": {
"signature": "() => Promise<void>",
"parameters": [],
"references": {
"Promise": {
"location": "global"
}
},
"return": "Promise<void>"
},
"docs": {
"text": "Sets focus on the specified `stories-searchbar`. Use this method instead of the global\n`input.focus()`.",
"tags": []
}
},
"getInputElement": {
"complexType": {
"signature": "() => Promise<HTMLInputElement>",
"parameters": [],
"references": {
"Promise": {
"location": "global"
},
"HTMLInputElement": {
"location": "global"
}
},
"return": "Promise<HTMLInputElement>"
},
"docs": {
"text": "Returns the native `<input>` element used under the hood.",
"tags": []
}
}
}; }
static get elementRef() { return "el"; }
static get watchers() { return [{
"propName": "debounce",
"methodName": "debounceChanged"
}, {
"propName": "value",
"methodName": "valueChanged"
}, {
"propName": "showCancelButton",
"methodName": "showCancelButtonChanged"
}]; }
}
//# sourceMappingURL=searchbar.js.map