/* * 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, { ChangeEventHandler, Component, FocusEventHandler, RefCallback, } from 'react'; import classNames from 'classnames'; import AutosizeInput from 'react-input-autosize'; import { OuiScreenReaderOnly } from '../../accessibility'; import { OuiFormControlLayout, OuiFormControlLayoutProps, } from '../../form/form_control_layout'; import { OuiIcon, IconType } from '../../icon'; import { OuiComboBoxPill } from './combo_box_pill'; import { htmlIdGenerator } from '../../../services'; import { OuiFormControlLayoutIconsProps } from '../../form/form_control_layout/form_control_layout_icons'; import { OuiComboBoxOptionOption, OuiComboBoxSingleSelectionShape, OptionHandler, UpdatePositionHandler, } from '../types'; import { CommonProps } from '../../common'; export interface OuiComboBoxInputProps extends CommonProps { autoSizeInputRef?: RefCallback; compressed: boolean; focusedOptionId?: string; fullWidth?: boolean; hasSelectedOptions: boolean; icon?: IconType | boolean; id?: string; inputRef?: RefCallback; isDisabled?: boolean; isListOpen: boolean; noIcon: boolean; onBlur?: FocusEventHandler; onChange?: (searchValue: string) => void; onClear?: () => void; onClick?: () => void; onCloseListClick: () => void; onFocus: FocusEventHandler; onOpenListClick: () => void; onRemoveOption?: OptionHandler; placeholder?: string; rootId: ReturnType; searchValue: string; selectedOptions?: Array>; singleSelection?: boolean | OuiComboBoxSingleSelectionShape; toggleButtonRef?: RefCallback; updatePosition: UpdatePositionHandler; value?: string; prepend?: OuiFormControlLayoutProps['prepend']; append?: OuiFormControlLayoutProps['append']; isLoading?: boolean; autoFocus?: boolean; } interface OuiComboBoxInputState { hasFocus: boolean; } export class OuiComboBoxInput extends Component< OuiComboBoxInputProps, OuiComboBoxInputState > { state: OuiComboBoxInputState = { hasFocus: false, }; updatePosition = () => { // Wait a beat for the DOM to update, since we depend on DOM elements' bounds. requestAnimationFrame(() => { this.props.updatePosition(); }); }; onFocus: FocusEventHandler = (event) => { this.props.onFocus(event); this.setState({ hasFocus: true, }); }; onBlur: FocusEventHandler = (event) => { if (this.props.onBlur) { this.props.onBlur(event); } this.setState({ hasFocus: false, }); }; componentDidUpdate(prevProps: OuiComboBoxInputProps) { const { searchValue } = prevProps; // We need to update the position of everything if the user enters enough input to change // the size of the input. if (searchValue !== this.props.searchValue) { this.updatePosition(); } } inputOnChange: ChangeEventHandler = (event) => { const { onChange, searchValue } = this.props; if (onChange) { onChange(event.target.value as typeof searchValue); } }; inputRefCallback = (ref: HTMLInputElement & AutosizeInput) => { const { autoSizeInputRef } = this.props; if (autoSizeInputRef) { autoSizeInputRef(ref); } }; render() { const { compressed, focusedOptionId, fullWidth, hasSelectedOptions, icon: iconProp, id, inputRef, isDisabled, isListOpen, noIcon, onClear, onClick, onCloseListClick, onOpenListClick, onRemoveOption, placeholder, rootId, searchValue, selectedOptions, singleSelection: singleSelectionProp, toggleButtonRef, value, prepend, append, isLoading, autoFocus, } = this.props; const singleSelection = Boolean(singleSelectionProp); const asPlainText = (singleSelectionProp && typeof singleSelectionProp === 'object' && singleSelectionProp.asPlainText) || false; const pills = selectedOptions ? selectedOptions.map((option) => { const { key, label, color, onClick, ...rest } = option; const pillOnClose = isDisabled || singleSelection || onClick ? undefined : onRemoveOption; return ( {label} ); }) : null; let removeOptionMessage; let removeOptionMessageId; if (this.state.hasFocus) { const readPlaceholder = placeholder ? `${placeholder}.` : ''; const removeOptionMessageContent = `Combo box. Selected. ${ searchValue ? `${searchValue}. Selected. ` : '' }${ selectedOptions && selectedOptions.length > 0 ? `${value}. Press Backspace to delete ${ selectedOptions[selectedOptions.length - 1].label }. ` : '' }Combo box input. ${readPlaceholder} Type some text or, to display a list of choices, press Down Arrow. ` + 'To exit the list of choices, press Escape.'; removeOptionMessageId = htmlIdGenerator()(); // aria-live="assertive" will read this message aloud immediately once it enters the DOM. // We'll render to the DOM when the input gains focus and remove it when the input loses focus. // We'll use aria-hidden to prevent default aria information from being read by the screen // reader. removeOptionMessage = ( {removeOptionMessageContent} ); } let placeholderMessage; if ( placeholder && selectedOptions && !selectedOptions.length && !searchValue ) { placeholderMessage = (

{placeholder}

); } const clickProps: OuiFormControlLayoutIconsProps = {}; if (!isDisabled && onClear && hasSelectedOptions) { clickProps.clear = { 'data-test-subj': 'comboBoxClearButton', onClick: onClear, }; } let formControlIcon: OuiFormControlLayoutIconsProps['icon']; if (!noIcon) { formControlIcon = { 'aria-label': isListOpen ? 'Close list of options' : 'Open list of options', 'data-test-subj': 'comboBoxToggleListButton', disabled: isDisabled, onClick: isListOpen && !isDisabled ? onCloseListClick : onOpenListClick, ref: toggleButtonRef, side: 'right', type: 'arrowDown', }; } let icon; if (!!iconProp) { const iconClasses = classNames('ouiComboBoxPill', 'ouiComboBoxIcon'); icon = ( ); } const wrapClasses = classNames('ouiComboBox__inputWrap', { 'ouiComboBox__inputWrap--compressed': compressed, 'ouiComboBox__inputWrap--fullWidth': fullWidth, 'ouiComboBox__inputWrap--noWrap': singleSelection, 'ouiComboBox__inputWrap-isLoading': isLoading, 'ouiComboBox__inputWrap-isClearable': onClear, 'ouiComboBox__inputWrap--inGroup': prepend || append, }); return (
{icon} {!singleSelection || !searchValue ? pills : null} {placeholderMessage} {removeOptionMessage}
); } }