/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. * * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import React, { Component, InputHTMLAttributes, KeyboardEvent } from 'react'; import classNames from 'classnames'; import { Browser } from '../../../services/browser'; import { CommonProps } from '../../common'; import { keys } from '../../../services'; import { OuiFormControlLayout, OuiFormControlLayoutProps, } from '../form_control_layout'; import { OuiValidatableControl } from '../validatable_control'; export interface OuiFieldSearchProps extends CommonProps, InputHTMLAttributes { name?: string; id?: string; placeholder?: string; value?: string; isInvalid?: boolean; fullWidth?: boolean; isLoading?: boolean; /** * Called when the user presses [Enter] OR on change if the incremental prop is `true`. * If you don't need the on[Enter] functionality, prefer using onChange */ onSearch?: (value: string) => void; /** * When `true` the search will be executed (that is, the `onSearch` will be called) as the * user types. */ incremental?: boolean; /** * when `true` creates a shorter height input */ compressed?: boolean; inputRef?: (node: HTMLInputElement | null) => void; /** * Shows a button that quickly clears any input */ isClearable?: boolean; /** * Creates an input group with element(s) coming before input * `string` | `ReactElement` or an array of these */ prepend?: OuiFormControlLayoutProps['prepend']; /** * Creates an input group with element(s) coming after input. * `string` | `ReactElement` or an array of these */ append?: OuiFormControlLayoutProps['append']; } interface OuiFieldSearchState { value: string; } let isSearchSupported: boolean = false; export class OuiFieldSearch extends Component< OuiFieldSearchProps, OuiFieldSearchState > { static defaultProps = { fullWidth: false, isLoading: false, incremental: false, compressed: false, isClearable: true, }; state = { value: this.props.value || (this.props.defaultValue ? `${this.props.defaultValue}` : ''), }; inputElement: HTMLInputElement | null = null; cleanups: Array<() => void> = []; componentDidMount() { if (!this.inputElement) return; isSearchSupported = Browser.isEventSupported('search', this.inputElement); if (isSearchSupported) { const onSearch = (event?: Event) => { if (this.props.onSearch) { if (!event || !event.target || event.defaultPrevented) return; this.props.onSearch((event.target as HTMLInputElement).value); } }; this.inputElement.addEventListener('search', onSearch); this.cleanups.push(() => { if (!this.inputElement) return; this.inputElement.removeEventListener('search', onSearch); }); } const onChange = (event: Event) => { if ( event.target && (event.target as HTMLInputElement).value !== this.state.value ) { this.setState({ value: (event.target as HTMLInputElement).value, }); if (this.props.onSearch) { this.props.onSearch((event.target as HTMLInputElement).value); } } }; this.inputElement.addEventListener('change', onChange); } onClear = () => { // clear the field's value // 1. React doesn't listen for `change` events, instead it maps `input` events to `change` // 2. React only fires the mapped `change` event if the element's value has changed // 3. An input's value is, in addition to other methods, tracked by intercepting element.value = '...' // // So we have to go below the element's value setter to avoid React intercepting it, // only then will React treat the value as different and fire its `change` event // // https://stackoverflow.com/questions/23892547/what-is-the-best-way-to-trigger-onchange-event-in-react-js const nativeInputValue = Object.getOwnPropertyDescriptor( HTMLInputElement.prototype, 'value' ); const nativeInputValueSetter = nativeInputValue ? nativeInputValue.set : undefined; if (nativeInputValueSetter) { nativeInputValueSetter.call(this.inputElement, ''); } // dispatch input event, with IE11 support/fallback let event; if ('Event' in window && typeof Event === 'function') { event = new Event('input', { bubbles: true, cancelable: false, }); } else { // IE11 event = document.createEvent('Event'); event.initEvent('input', true, false); } if (this.inputElement) { if (event) { this.inputElement.dispatchEvent(event); } // set focus on the search field this.inputElement.focus(); this.inputElement.dispatchEvent(new Event('change')); } this.setState({ value: '' }); const { incremental, onSearch } = this.props; if (onSearch && incremental) { onSearch(''); } }; componentWillUnmount() { this.cleanups.forEach((cleanup) => cleanup()); } setRef = (inputElement: HTMLInputElement | null) => { this.inputElement = inputElement; if (this.props.inputRef) { this.props.inputRef(inputElement); } }; onKeyUp = ( event: KeyboardEvent, incremental?: boolean, onSearch?: (value: string) => void ) => { this.setState({ value: (event.target as HTMLInputElement).value }); if (this.props.onKeyUp) { this.props.onKeyUp(event); if (event.defaultPrevented) { return; } } if ( onSearch && ((event.key !== keys.ENTER && incremental) || (event.key === keys.ENTER && !isSearchSupported)) ) { onSearch((event.target as HTMLInputElement).value); } }; render() { const { className, id, name, placeholder, isInvalid, fullWidth, isLoading, inputRef, incremental, compressed, onSearch, isClearable, append, prepend, ...rest } = this.props; let value = this.props.value; if (typeof this.props.value !== 'string') value = this.state.value; const classes = classNames( 'ouiFieldSearch', { 'ouiFieldSearch--fullWidth': fullWidth, 'ouiFieldSearch--compressed': compressed, 'ouiFieldSearch--inGroup': prepend || append, 'ouiFieldSearch-isLoading': isLoading, 'ouiFieldSearch-isClearable': isClearable && value, }, className ); return ( this.onKeyUp(e, incremental, onSearch)} ref={this.setRef} {...rest} /> ); } }