/*! * * Copyright (c) "Neo4j" * Neo4j Sweden AB [http://neo4j.com] * * This file is part of Neo4j. * * Neo4j is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ type ComboboxOption = { label: string; value: string; /** If the option is disabled */ isDisabled?: boolean; /** A visual element to display before the label. Incompatible with multi select. */ leadingVisual?: React.ReactNode; /** A description to display below the label. */ description?: string; }; type ComboboxOptionGroup = { label: string; options: TOption[]; }; type ComboboxFilterOption = ((option: TOption, searchValue: string) => boolean) | false; type ComboboxSize = 'small' | 'medium' | 'large'; /** * A row at the end of the panel that acts instead of selecting. * * @remarks * Deliberately not a `ComboboxOption`, since an action never becomes part of * the value. Use it for something that leads out of the selection, such as a * link into a create flow. */ type ComboboxAction = { /** Text of the row. */ label: string; /** * Called when the row is activated, with the current search value so the * action can carry the query with it. The selection is left untouched. */ onSelect: (context: { searchValue: string; }) => void; /** A description to display below the label. */ description?: string; /** A visual element to display before the label. */ leadingVisual?: React.ReactNode; /** If the action is disabled. */ isDisabled?: boolean; /** * Whether a divider is rendered above the action, separating it from the * action before it. Ignored on the first action, which already sits below * the divider that separates actions from options. */ hasDivider?: boolean; }; type ComboboxBaseProps = { /** Accessible label for the combobox. Rendered above the control. */ label: string; /** * Name used to submit the selected option value through a native form. * Above 300 options, only the selection is mirrored to the form, so browser * autofill no longer offers the option list. */ name?: string; /** Id of the form the combobox belongs to when it is outside that form. */ formId?: string; /** Hint for browser autofill on the native form control. */ autoComplete?: string; /** Ref to the focusable combobox trigger. */ triggerRef?: React.Ref; /** Whether the label is clipped out of the layout, leaving it to screen readers only. */ hasVisuallyHiddenLabel?: boolean; /** Assistive text below the control. Replaced by `errorText` while there is an error. */ helpText?: React.ReactNode; /** Error message below the control. Puts the control in its error state. */ errorText?: React.ReactNode; /** * Whether a selection is required. Sets `aria-required` and, when `name` is * provided, enables native form validation. */ isRequired?: boolean; /** Whether Required or Optional is shown at the end of the label row, based on the `isRequired` prop */ hasRequiredIndicator?: boolean; /** The options to display in the combobox. Either an array of options or an array of option groups. */ options: TOption[] | ComboboxOptionGroup[]; /** * Whether a search input is rendered inside the panel. On by default; turn * it off for a short list (roughly 10 options or fewer) where filtering * adds nothing. */ isSearchable?: boolean; /** The search value in controlled mode. */ searchValue?: string; /** The initial search value in uncontrolled mode. */ defaultSearchValue?: string; /** Called when the search value changes. */ onSearchChange?: (searchValue: string) => void; /** * How options are matched against the search value. Defaults to a * case-insensitive match on the option label. Pass `false` to skip internal * filtering when the options are already filtered, such as when searching * server side. */ filterOption?: ComboboxFilterOption; /** Placeholder for the search input, which also names it. */ searchPlaceholder?: string; /** * Whether the panel is waiting for options. Replaces the empty message while * there are no options. */ isLoading?: boolean; /** Text of the loading status. Defaults to `Loading...`. */ loadingMessage?: React.ReactNode; /** * Whether the search value can be turned into a new option, shown as an extra * row at the end of the list. Requires `isSearchable`. */ isCreatable?: boolean; /** * Called after a created option has been selected, so it can be persisted * into `options`. Created options are not added to `options` by the combobox. */ onCreateOption?: (option: TOption) => void; /** * Builds the option to create from the search value. Defaults to using the * search value as both label and value, which is only enough when the options * carry no fields of their own beyond `ComboboxOption`. */ createOption?: (searchValue: string) => TOption; /** Text of the create row. Defaults to `Create ""`. */ formatCreateLabel?: (searchValue: string) => React.ReactNode; /** * Whether the search value can become a new option. Defaults to requiring a * non-blank value that does not exactly match an existing option or a * currently selected one. */ isValidNewOption?: (searchValue: string, options: TOption[]) => boolean; /** * Rows that act instead of selecting, pinned below the scrolling options * after a divider so they stay visible however long the list is. They are * always shown, never filtered by the search value, and never part of the * selection. */ actions?: ComboboxAction[]; /** Whether the panel is controlled open. */ isOpen?: boolean; /** Called when the panel opens or closes. */ onOpenChange?: (isOpen: boolean) => void; /** Whether a button for clearing the selection is rendered in the trigger. */ isClearable?: boolean; /** Text shown in the trigger while nothing is selected. */ placeholder?: string; /** Whether the combobox is disabled. */ isDisabled?: boolean; /** Size of the combobox control. */ size?: ComboboxSize; /** Whether the combobox fills the available width. */ isFluid?: boolean; /** Whether the control hugs its content without a background or resting outline. */ isClean?: boolean; /** Whether the panel is wrapped in a floating portal. Defaults to true outside a dialog. */ isPortaled?: boolean; /** Portal target element. Defaults to `document.body`. */ portalTarget?: HTMLElement | null; /** Positioning strategy used by Floating UI. */ floatingStrategy?: 'absolute' | 'fixed'; /** * Replaces the built-in trigger. Spread `triggerProps` onto the focusable * element that should open the panel; the panel is anchored to it. No root * element is rendered, so the trigger is a direct child of whatever contains * the combobox. `isFluid`, `isClean`, `size`, `className`, `style`, * `htmlAttributes` and `ref` are ignored; use `triggerRef` to reach the * trigger element. */ renderTrigger?: (triggerProps: ComboboxTriggerProps, state: ComboboxTriggerState) => React.ReactNode; /** * Replaces how selected values and option labels are presented, keeping the * behavior and the ARIA of the combobox. */ components?: ComboboxComponents; }; /** * Presentation overrides for the parts of the combobox that show an option. * * @remarks * Each override is handed the option itself, so widening the option type is * what gives them typed access to fields of your own, such as a color. Only * presentation is replaced; selection, filtering, keyboard interaction and the * ARIA of the surrounding elements stay with the combobox. Use `renderTrigger` * instead to replace the control as a whole. */ type ComboboxComponents = { /** Replaces the tag rendered per selected option in multi select. */ MultiValue?: React.ComponentType>; /** Replaces the value shown in the trigger in single select. Must be non-interactive. */ SingleValue?: React.ComponentType>; /** Replaces the label of an option. The selection and description chrome stays. */ OptionLabel?: React.ComponentType>; }; /** Props of a `MultiValue` override, rendered once per selected option. */ type ComboboxMultiValueProps = { option: TOption; /** Position of the option in the selection. */ index: number; isDisabled: boolean; size: ComboboxSize; /** * Deselects this option. Nothing calls it unless the override renders an * affordance for it, which also makes focus after the removal its own * responsibility. */ remove: () => void; }; /** Props of a `SingleValue` override, rendered inside the trigger. */ type ComboboxSingleValueProps = { option: TOption; isDisabled: boolean; size: ComboboxSize; }; /** Props of an `OptionLabel` override, rendered inside each option. */ type ComboboxOptionLabelProps = { option: TOption; isSelected: boolean; isDisabled: boolean; /** Whether the option currently holds virtual focus. */ isFocused: boolean; isMulti: boolean; size: ComboboxSize; }; /** * Props that must be spread onto a custom trigger element. Includes the * Floating UI reference props, so the element it lands on becomes the anchor * the panel is positioned against. */ type ComboboxTriggerProps = React.HTMLAttributes & { ref: React.Ref; role: 'combobox'; 'aria-haspopup': 'dialog' | 'listbox'; 'aria-expanded': boolean; 'aria-disabled': boolean | undefined; 'aria-invalid': boolean | undefined; 'aria-required': boolean | undefined; 'aria-controls': string | undefined; 'aria-activedescendant': string | undefined; 'aria-describedby': string | undefined; 'aria-label'?: string; 'aria-labelledby'?: string; }; /** * The label, help text, and error text of the combobox, for rendering them from * a custom trigger. * * @remarks * A custom trigger is named through `aria-label` and carries the * `aria-describedby` of whichever message is currently shown, so it has to * render that message with the id it is handed here. */ type ComboboxFieldState = { label: string; hasVisuallyHiddenLabel: boolean; helpText?: React.ReactNode; errorText?: React.ReactNode; hasError: boolean; isRequired: boolean; hasRequiredIndicator: boolean; /** Id the trigger's `aria-labelledby` points at when the label is rendered. */ labelId: string; /** Id of the trigger, so a rendered `