/*!
*
* 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 `