@neo4j-ndl/react 4.18.13 → 4.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/LICENSES.txt +54 -0
- package/lib/NOTICE.txt +8 -0
- package/lib/cjs/_common/use-semi-controlled-state.js +6 -1
- package/lib/cjs/_common/use-semi-controlled-state.js.map +1 -1
- package/lib/cjs/combobox/Combobox.js +1031 -0
- package/lib/cjs/combobox/Combobox.js.map +1 -0
- package/lib/cjs/combobox/combobox-types.js +23 -0
- package/lib/cjs/combobox/combobox-types.js.map +1 -0
- package/lib/cjs/combobox/combobox-utils.js +129 -0
- package/lib/cjs/combobox/combobox-utils.js.map +1 -0
- package/lib/cjs/combobox/index.js +26 -0
- package/lib/cjs/combobox/index.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-action-on-select.story.js +82 -0
- package/lib/cjs/combobox/stories/combobox-action-on-select.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-actions.story.js +78 -0
- package/lib/cjs/combobox/stories/combobox-actions.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-async.story.js +75 -0
- package/lib/cjs/combobox/stories/combobox-async.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-clean.story.js +38 -0
- package/lib/cjs/combobox/stories/combobox-clean.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-creatable.story.js +41 -0
- package/lib/cjs/combobox/stories/combobox-creatable.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-custom-multi.story.js +45 -0
- package/lib/cjs/combobox/stories/combobox-custom-multi.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-custom-single.story.js +44 -0
- package/lib/cjs/combobox/stories/combobox-custom-single.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-custom-trigger.story.js +60 -0
- package/lib/cjs/combobox/stories/combobox-custom-trigger.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-default.story.js +42 -0
- package/lib/cjs/combobox/stories/combobox-default.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-descriptions.story.js +45 -0
- package/lib/cjs/combobox/stories/combobox-descriptions.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-disabled.story.js +34 -0
- package/lib/cjs/combobox/stories/combobox-disabled.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-fluid.story.js +34 -0
- package/lib/cjs/combobox/stories/combobox-fluid.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-groups.story.js +49 -0
- package/lib/cjs/combobox/stories/combobox-groups.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-hidden-label.story.js +33 -0
- package/lib/cjs/combobox/stories/combobox-hidden-label.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-icons.story.js +46 -0
- package/lib/cjs/combobox/stories/combobox-icons.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-in-dialog.story.js +40 -0
- package/lib/cjs/combobox/stories/combobox-in-dialog.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-in-popover-dialog.story.js +40 -0
- package/lib/cjs/combobox/stories/combobox-in-popover-dialog.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-in-popover.story.js +36 -0
- package/lib/cjs/combobox/stories/combobox-in-popover.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-large-data-set.story.js +36 -0
- package/lib/cjs/combobox/stories/combobox-large-data-set.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-max-rows.story.js +42 -0
- package/lib/cjs/combobox/stories/combobox-max-rows.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-multi.story.js +45 -0
- package/lib/cjs/combobox/stories/combobox-multi.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-native-form.story.js +49 -0
- package/lib/cjs/combobox/stories/combobox-native-form.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-select-only.story.js +38 -0
- package/lib/cjs/combobox/stories/combobox-select-only.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-sizes.story.js +33 -0
- package/lib/cjs/combobox/stories/combobox-sizes.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-unlimited-rows.story.js +42 -0
- package/lib/cjs/combobox/stories/combobox-unlimited-rows.story.js.map +1 -0
- package/lib/cjs/combobox/stories/combobox-validation.story.js +33 -0
- package/lib/cjs/combobox/stories/combobox-validation.story.js.map +1 -0
- package/lib/cjs/combobox/stories/index.js +133 -0
- package/lib/cjs/combobox/stories/index.js.map +1 -0
- package/lib/cjs/combobox/use-combobox-multi-value-overflow.js +173 -0
- package/lib/cjs/combobox/use-combobox-multi-value-overflow.js.map +1 -0
- package/lib/cjs/combobox/use-combobox-popover.js +80 -0
- package/lib/cjs/combobox/use-combobox-popover.js.map +1 -0
- package/lib/cjs/date-picker/DatePicker.js.map +1 -1
- package/lib/cjs/dismissible-tag/DismissibleTag.js +1 -1
- package/lib/cjs/dismissible-tag/DismissibleTag.js.map +1 -1
- package/lib/cjs/index.js +1 -0
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/select/Select.js +3 -0
- package/lib/cjs/select/Select.js.map +1 -1
- package/lib/cjs/text-area/TextArea.js.map +1 -1
- package/lib/cjs/text-input/TextInput.js.map +1 -1
- package/lib/esm/_common/use-semi-controlled-state.js +6 -1
- package/lib/esm/_common/use-semi-controlled-state.js.map +1 -1
- package/lib/esm/combobox/Combobox.js +1024 -0
- package/lib/esm/combobox/Combobox.js.map +1 -0
- package/lib/esm/combobox/combobox-types.js +22 -0
- package/lib/esm/combobox/combobox-types.js.map +1 -0
- package/lib/esm/combobox/combobox-utils.js +112 -0
- package/lib/esm/combobox/combobox-utils.js.map +1 -0
- package/lib/esm/combobox/index.js +22 -0
- package/lib/esm/combobox/index.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-action-on-select.story.js +80 -0
- package/lib/esm/combobox/stories/combobox-action-on-select.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-actions.story.js +76 -0
- package/lib/esm/combobox/stories/combobox-actions.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-async.story.js +73 -0
- package/lib/esm/combobox/stories/combobox-async.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-clean.story.js +36 -0
- package/lib/esm/combobox/stories/combobox-clean.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-creatable.story.js +39 -0
- package/lib/esm/combobox/stories/combobox-creatable.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-custom-multi.story.js +43 -0
- package/lib/esm/combobox/stories/combobox-custom-multi.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-custom-single.story.js +42 -0
- package/lib/esm/combobox/stories/combobox-custom-single.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-custom-trigger.story.js +58 -0
- package/lib/esm/combobox/stories/combobox-custom-trigger.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-default.story.js +40 -0
- package/lib/esm/combobox/stories/combobox-default.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-descriptions.story.js +43 -0
- package/lib/esm/combobox/stories/combobox-descriptions.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-disabled.story.js +32 -0
- package/lib/esm/combobox/stories/combobox-disabled.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-fluid.story.js +32 -0
- package/lib/esm/combobox/stories/combobox-fluid.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-groups.story.js +47 -0
- package/lib/esm/combobox/stories/combobox-groups.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-hidden-label.story.js +31 -0
- package/lib/esm/combobox/stories/combobox-hidden-label.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-icons.story.js +44 -0
- package/lib/esm/combobox/stories/combobox-icons.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-in-dialog.story.js +38 -0
- package/lib/esm/combobox/stories/combobox-in-dialog.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-in-popover-dialog.story.js +38 -0
- package/lib/esm/combobox/stories/combobox-in-popover-dialog.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-in-popover.story.js +34 -0
- package/lib/esm/combobox/stories/combobox-in-popover.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-large-data-set.story.js +34 -0
- package/lib/esm/combobox/stories/combobox-large-data-set.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-max-rows.story.js +40 -0
- package/lib/esm/combobox/stories/combobox-max-rows.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-multi.story.js +43 -0
- package/lib/esm/combobox/stories/combobox-multi.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-native-form.story.js +47 -0
- package/lib/esm/combobox/stories/combobox-native-form.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-select-only.story.js +36 -0
- package/lib/esm/combobox/stories/combobox-select-only.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-sizes.story.js +31 -0
- package/lib/esm/combobox/stories/combobox-sizes.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-unlimited-rows.story.js +40 -0
- package/lib/esm/combobox/stories/combobox-unlimited-rows.story.js.map +1 -0
- package/lib/esm/combobox/stories/combobox-validation.story.js +31 -0
- package/lib/esm/combobox/stories/combobox-validation.story.js.map +1 -0
- package/lib/esm/combobox/stories/index.js +100 -0
- package/lib/esm/combobox/stories/index.js.map +1 -0
- package/lib/esm/combobox/use-combobox-multi-value-overflow.js +166 -0
- package/lib/esm/combobox/use-combobox-multi-value-overflow.js.map +1 -0
- package/lib/esm/combobox/use-combobox-popover.js +77 -0
- package/lib/esm/combobox/use-combobox-popover.js.map +1 -0
- package/lib/esm/date-picker/DatePicker.js.map +1 -1
- package/lib/esm/dismissible-tag/DismissibleTag.js +1 -1
- package/lib/esm/dismissible-tag/DismissibleTag.js.map +1 -1
- package/lib/esm/index.js +1 -0
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/select/Select.js +3 -0
- package/lib/esm/select/Select.js.map +1 -1
- package/lib/esm/text-area/TextArea.js.map +1 -1
- package/lib/esm/text-input/TextInput.js.map +1 -1
- package/lib/types/_common/use-semi-controlled-state.d.ts +5 -1
- package/lib/types/_common/use-semi-controlled-state.d.ts.map +1 -1
- package/lib/types/combobox/Combobox.d.ts +24 -0
- package/lib/types/combobox/Combobox.d.ts.map +1 -0
- package/lib/types/combobox/combobox-types.d.ts +419 -0
- package/lib/types/combobox/combobox-types.d.ts.map +1 -0
- package/lib/types/combobox/combobox-utils.d.ts +81 -0
- package/lib/types/combobox/combobox-utils.d.ts.map +1 -0
- package/lib/types/combobox/index.d.ts +23 -0
- package/lib/types/combobox/index.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-action-on-select.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-action-on-select.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-actions.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-actions.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-async.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-async.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-clean.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-clean.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-creatable.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-creatable.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-custom-multi.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-custom-multi.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-custom-single.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-custom-single.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-custom-trigger.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-custom-trigger.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-default.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-default.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-descriptions.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-descriptions.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-disabled.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-disabled.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-fluid.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-fluid.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-groups.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-groups.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-hidden-label.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-hidden-label.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-icons.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-icons.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-in-dialog.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-in-dialog.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-in-popover-dialog.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-in-popover-dialog.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-in-popover.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-in-popover.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-large-data-set.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-large-data-set.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-max-rows.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-max-rows.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-multi.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-multi.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-native-form.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-native-form.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-select-only.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-select-only.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-sizes.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-sizes.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-unlimited-rows.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-unlimited-rows.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/combobox-validation.story.d.ts +24 -0
- package/lib/types/combobox/stories/combobox-validation.story.d.ts.map +1 -0
- package/lib/types/combobox/stories/index.d.ts +73 -0
- package/lib/types/combobox/stories/index.d.ts.map +1 -0
- package/lib/types/combobox/use-combobox-multi-value-overflow.d.ts +76 -0
- package/lib/types/combobox/use-combobox-multi-value-overflow.d.ts.map +1 -0
- package/lib/types/combobox/use-combobox-popover.d.ts +66 -0
- package/lib/types/combobox/use-combobox-popover.d.ts.map +1 -0
- package/lib/types/date-picker/DatePicker.d.ts.map +1 -1
- package/lib/types/dismissible-tag/DismissibleTag.d.ts.map +1 -1
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/select/Select.d.ts +3 -0
- package/lib/types/select/Select.d.ts.map +1 -1
- package/lib/types/text-input/TextInput.d.ts.map +1 -1
- package/package.json +10 -4
- package/skills/ndl-react/SKILL.md +20 -0
- package/skills/ndl-react/components/combobox.md +1487 -0
|
@@ -0,0 +1,1487 @@
|
|
|
1
|
+
# Combobox
|
|
2
|
+
|
|
3
|
+
Import: `import { Combobox } from '@neo4j-ndl/react'`
|
|
4
|
+
|
|
5
|
+
## Props
|
|
6
|
+
|
|
7
|
+
### Combobox
|
|
8
|
+
|
|
9
|
+
| Prop | Type | Required | Default | Description |
|
|
10
|
+
|------|------|----------|---------|-------------|
|
|
11
|
+
| `actions` | `ComboboxAction[]` | | | 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. |
|
|
12
|
+
| `autoComplete` | `string` | | | Hint for browser autofill on the native form control. |
|
|
13
|
+
| `components` | `ComboboxComponents<TOption>` | | `{}` | Replaces how selected values and option labels are presented, keeping the behavior and the ARIA of the combobox. |
|
|
14
|
+
| `createOption` | `((searchValue: string) => TOption)` | | `<TOption extends ComboboxOption>( searchValue: string, ): TOption => ({ label: searchValue, value: searchValue, }) as TOption` | 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`. |
|
|
15
|
+
| `defaultSearchValue` | `string` | | | The initial search value in uncontrolled mode. |
|
|
16
|
+
| `defaultValue` | `ComboboxOption \| TOption[] \| null` | | | The initially selected option in uncontrolled mode. The initially selected options in uncontrolled mode. |
|
|
17
|
+
| `errorText` | `ReactNode` | | | Error message below the control. Puts the control in its error state. |
|
|
18
|
+
| `filterOption` | `ComboboxFilterOption<TOption>` | | | 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. |
|
|
19
|
+
| `floatingStrategy` | `'absolute' \| 'fixed'` | | | Positioning strategy used by Floating UI. |
|
|
20
|
+
| `formatCreateLabel` | `((searchValue: string) => ReactNode)` | | `` (searchValue: string) => `Create "${searchValue}"` `` | Text of the create row. Defaults to `Create "<search value>"`. |
|
|
21
|
+
| `formId` | `string` | | | Id of the form the combobox belongs to when it is outside that form. |
|
|
22
|
+
| `hasRequiredIndicator` | `boolean` | | `false` | Whether Required or Optional is shown at the end of the label row. |
|
|
23
|
+
| `hasVisuallyHiddenLabel` | `boolean` | | `false` | Whether the label is clipped out of the layout, leaving it to screen readers only. |
|
|
24
|
+
| `helpText` | `ReactNode` | | | Assistive text below the control. Replaced by `errorText` while there is an error. |
|
|
25
|
+
| `isClean` | `boolean` | | `false` | Whether the control hugs its content without a background or resting outline. |
|
|
26
|
+
| `isClearable` | `boolean` | | `false` | Whether a button for clearing the selection is rendered in the trigger. |
|
|
27
|
+
| `isCreatable` | `boolean` | | `false` | Whether the search value can be turned into a new option, shown as an extra row at the end of the list. Requires `isSearchable`. |
|
|
28
|
+
| `isDisabled` | `boolean` | | `false` | Whether the combobox is disabled. |
|
|
29
|
+
| `isFluid` | `boolean` | | `false` | Whether the combobox fills the available width. |
|
|
30
|
+
| `isLoading` | `boolean` | | `false` | Whether the panel is waiting for options. Replaces the empty message while there are no options. |
|
|
31
|
+
| `isMulti` | `boolean` | | `false` | Whether multiple options can be selected. |
|
|
32
|
+
| `isOpen` | `boolean` | | | Whether the panel is controlled open. |
|
|
33
|
+
| `isPortaled` | `boolean` | | | Whether the panel is wrapped in a floating portal. Defaults to true outside a dialog. |
|
|
34
|
+
| `isRequired` | `boolean` | | `false` | Whether a selection is required. Sets `aria-required` and, when `name` is provided, enables native form validation. |
|
|
35
|
+
| `isSearchable` | `boolean` | | `true` | 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. |
|
|
36
|
+
| `isValidNewOption` | `((searchValue: string, options: TOption[]) => boolean)` | | `( searchValue: string, options: ComboboxOption[], ) => searchValue.trim() !== '' && !options.some((option) => matchesExactly(option, searchValue))` | 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. |
|
|
37
|
+
| `label` | `string` | ✅ | | Accessible label for the combobox. Rendered above the control. |
|
|
38
|
+
| `loadingMessage` | `ReactNode` | | `Loading...` | Text of the loading status. Defaults to `Loading...`. |
|
|
39
|
+
| `maxRows` | `number \| null` | | `1` | Maximum number of rows the selected tags may wrap into, a single row by default. Tags that do not fit are collapsed into a read-only counter tag, such as `+4`, at the end of the last row. Pass `null` to let the control grow with the selection. |
|
|
40
|
+
| `name` | `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. |
|
|
41
|
+
| `onChange` | `((value: TOption \| null) => void) \| ((value: TOption[]) => void)` | | | Called when the selected option changes. `null` when the selection is cleared. Called when the selected options change. |
|
|
42
|
+
| `onCreateOption` | `((option: TOption) => void)` | | | 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. |
|
|
43
|
+
| `onOpenChange` | `((isOpen: boolean) => void)` | | | Called when the panel opens or closes. |
|
|
44
|
+
| `onSearchChange` | `((searchValue: string) => void)` | | | Called when the search value changes. |
|
|
45
|
+
| `options` | `TOption[] \| ComboboxOptionGroup<TOption>[]` | ✅ | | The options to display in the combobox. Either an array of options or an array of option groups. |
|
|
46
|
+
| `placeholder` | `string` | | `Select...` | Text shown in the trigger while nothing is selected. |
|
|
47
|
+
| `portalTarget` | `HTMLElement \| null` | | | Portal target element. Defaults to `document.body`. |
|
|
48
|
+
| `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
|
|
49
|
+
| `renderTrigger` | `((triggerProps: ComboboxTriggerProps, state: ComboboxTriggerState<TOption>) => ReactNode)` | | | Replaces the built-in trigger. Spread `triggerProps` onto the focusable element that should open the panel; the panel is anchored to it. The root element then lays out as `display: contents`, so the trigger is placed by whatever contains the combobox, and `isFluid`, `isClean`, `size`, `className` and `style` no longer shape it. |
|
|
50
|
+
| `searchPlaceholder` | `string` | | `Search` | Placeholder for the search input, which also names it. |
|
|
51
|
+
| `searchValue` | `string` | | | The search value in controlled mode. |
|
|
52
|
+
| `size` | `'large' \| 'medium' \| 'small'` | | `medium` | Size of the combobox control. |
|
|
53
|
+
| `triggerRef` | `Ref<HTMLElement>` | | | Ref to the focusable combobox trigger. |
|
|
54
|
+
| `value` | `ComboboxOption \| TOption[] \| null` | | | The selected option in controlled mode. The selected options in controlled mode. |
|
|
55
|
+
|
|
56
|
+
## Accessibility
|
|
57
|
+
|
|
58
|
+
## Two compositions
|
|
59
|
+
|
|
60
|
+
`isSearchable` decides which WAI-ARIA combobox pattern applies. The two must not be mixed.
|
|
61
|
+
|
|
62
|
+
**Searchable** (the default) — the trigger opens a dialog that contains its own combobox. DOM focus moves to the search input, so the trigger must not carry `aria-activedescendant`.
|
|
63
|
+
|
|
64
|
+
| Element | Roles and attributes |
|
|
65
|
+
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
66
|
+
| Trigger | `role="combobox"`, `aria-haspopup="dialog"`, `aria-expanded`, `aria-controls` pointing at the panel |
|
|
67
|
+
| Panel | `role="dialog"`, `aria-label` from `label` |
|
|
68
|
+
| Search input | `role="combobox"`, `aria-haspopup="listbox"`, `aria-autocomplete="list"`, `aria-expanded`, `aria-controls`, `aria-activedescendant` |
|
|
69
|
+
| Listbox | `role="listbox"`, `aria-label` from `label` |
|
|
70
|
+
|
|
71
|
+
**Not searchable** (`isSearchable={false}`) — the select-only combobox. The trigger is the only `role="combobox"`, DOM focus stays on it while the panel is open, and it owns `aria-activedescendant`.
|
|
72
|
+
|
|
73
|
+
| Element | Roles and attributes |
|
|
74
|
+
| ------- | ------------------------------------------------------------------------------------------------------- |
|
|
75
|
+
| Trigger | `role="combobox"`, `aria-haspopup="listbox"`, `aria-expanded`, `aria-controls`, `aria-activedescendant` |
|
|
76
|
+
| Panel | Plain container, no role |
|
|
77
|
+
| Listbox | `role="listbox"`, `aria-label` from `label` |
|
|
78
|
+
|
|
79
|
+
Nesting a second `role="combobox"` inside the popup is deliberate. The trigger is the form control and owns the field label, the current value, and validation ARIA such as `aria-invalid` and `aria-describedby`; the search input is only a filter control and never carries any of those.
|
|
80
|
+
|
|
81
|
+
## Naming
|
|
82
|
+
|
|
83
|
+
`label` is required and is the only accessible name in the API. It names the trigger, the listbox, and the dialog panel, so there is nothing to keep in sync. The separate `name` prop identifies a value during native form submission and does not participate in accessible naming.
|
|
84
|
+
|
|
85
|
+
Clicking the rendered label moves focus to the trigger without opening the panel, matching a native select label.
|
|
86
|
+
|
|
87
|
+
`hasVisuallyHiddenLabel` clips the label row out of the layout instead of removing it, so the name survives. Use it when something else already names the control, such as a toolbar heading or a preceding sentence, and never as a way to skip the label.
|
|
88
|
+
|
|
89
|
+
The search input inside the panel is named by `searchPlaceholder`. A placeholder alone is not a valid accessible name, so the same text is also rendered as a visually hidden label of the input.
|
|
90
|
+
|
|
91
|
+
## Validation and messages
|
|
92
|
+
|
|
93
|
+
The trigger is the form control, so it carries all of the validation ARIA.
|
|
94
|
+
|
|
95
|
+
| Prop | Effect on the trigger |
|
|
96
|
+
| ------------ | ----------------------------------------------------------------------- |
|
|
97
|
+
| `helpText` | `aria-describedby` pointing at the help text |
|
|
98
|
+
| `errorText` | `aria-invalid="true"` and `aria-describedby` pointing at the error text |
|
|
99
|
+
| `isRequired` | `aria-required="true"` |
|
|
100
|
+
|
|
101
|
+
- An error replaces the help text, so only one message is ever rendered and only that one is described. Do not put the error in `helpText` as well.
|
|
102
|
+
- The message container is a polite live region that is always in the document, so a validation error is announced when it appears.
|
|
103
|
+
- `hasRequiredIndicator` adds Required or Optional at the end of the label row without changing the accessible name; requiredness is exposed through `aria-required` alone.
|
|
104
|
+
- Without `name`, `isRequired` only marks the trigger with `aria-required`; validation belongs to the form around it.
|
|
105
|
+
- With `name`, `isRequired` also enables native constraint validation. If submission is attempted without a selection, the hidden native control prevents submission, moves focus to the visible trigger, and puts the browser validation message in the existing error live region. An explicit `errorText` takes precedence over that message.
|
|
106
|
+
|
|
107
|
+
## Native forms
|
|
108
|
+
|
|
109
|
+
Setting `name` opts the combobox into native form behavior through a visually hidden `<select>`. The select is `aria-hidden` because the visible trigger already exposes the label, value, requiredness, and validation state.
|
|
110
|
+
|
|
111
|
+
- Single selection submits the selected option's `value`, or an empty string when nothing is selected.
|
|
112
|
+
- Multi selection uses a native `multiple` select, so `FormData.getAll(name)` returns one entry per selected option.
|
|
113
|
+
- Selected creatable values are included even before the consumer adds them to `options`.
|
|
114
|
+
- `form` associates the value with a form elsewhere in the document, and `autoComplete` is forwarded for browser autofill.
|
|
115
|
+
- A disabled combobox is omitted from submission, matching native controls.
|
|
116
|
+
- Native or autofill changes update the combobox, while combobox changes dispatch a bubbling native `change` event for form listeners.
|
|
117
|
+
- `form.reset()` restores `defaultValue` and `defaultSearchValue` and closes uncontrolled state. Controlled values remain owned by their props.
|
|
118
|
+
|
|
119
|
+
## Roles and attributes on the list
|
|
120
|
+
|
|
121
|
+
- Options have `role="option"` with `aria-selected` and, when applicable, `aria-disabled`. Action rows are the one exception and carry no `aria-selected`, since nothing about them is selectable.
|
|
122
|
+
- Multi-select listboxes set `aria-multiselectable="true"`.
|
|
123
|
+
- Multi-select checkboxes are visual indicators only and are kept out of the accessibility tree, the tab order, and hit testing. Selection is exposed through each option's `aria-selected` state.
|
|
124
|
+
- Option groups use `role="group"` and are named through `aria-labelledby` by their visible group header.
|
|
125
|
+
- The empty state has `role="status"` so filtering down to no matches is announced. Loading uses the same status slot when `isLoading` is set and there are no options yet.
|
|
126
|
+
|
|
127
|
+
### Large lists
|
|
128
|
+
|
|
129
|
+
Above roughly a hundred rows the listbox windows its rendering: only the visible slice of rows is in the DOM. Below that threshold nothing changes.
|
|
130
|
+
|
|
131
|
+
- Every option carries `aria-setsize` and `aria-posinset` while windowed, so screen readers announce the position in the full list even though the DOM only holds a slice.
|
|
132
|
+
- Group nesting cannot survive windowing, so above the threshold group headers render as presentational rows instead of `role="group"` containers. The visual grouping is unchanged; the group name is not part of the options' ARIA while windowed.
|
|
133
|
+
- Keyboard navigation is unchanged. Moving the active option scrolls it into the window first, so `aria-activedescendant` always resolves to a rendered element.
|
|
134
|
+
|
|
135
|
+
## Structure of the control
|
|
136
|
+
|
|
137
|
+
The focusable `role="combobox"` button is the control's first Tab stop. Subsequent Tab presses move through each multi-value remove button in value order and then to the clear button. Clicking anywhere else in the control opens the panel.
|
|
138
|
+
|
|
139
|
+
A disabled combobox keeps its place in the tab order and is marked with `aria-disabled` rather than the native `disabled` attribute, so it stays discoverable to screen reader users. Selected multi-values remain visible as read-only tags, with no remove or clear buttons, and all selection mutation actions are ignored.
|
|
140
|
+
|
|
141
|
+
### Limiting the rows of a multi selection
|
|
142
|
+
|
|
143
|
+
The selected tags are kept to a single row by default. Tags that do not fit are collapsed into one read-only counter tag at the end of the last row, shown as `+N`. `maxRows` raises the cap, and `maxRows={null}` removes it so the control grows with the selection.
|
|
144
|
+
|
|
145
|
+
- Collapsed tags are taken out of the layout, the accessibility tree, and the tab order, so Tab moves through the visible remove buttons only.
|
|
146
|
+
- The counter is not interactive. It is announced as `N more selected` followed by the collapsed labels, and carries the same list as a `title` tooltip for pointer users. With very many collapsed values, both name at most the first twenty labels and summarize the rest as `and N more`.
|
|
147
|
+
- From one thousand collapsed values the visible counter rounds to whole thousands - `+2K` for 1500 - so it keeps a bounded width. The announced count stays exact.
|
|
148
|
+
- Under a row limit, a selection far larger than the limit could ever show only renders the tags that could possibly be visible; everything past them is part of the counter. With `maxRows={null}` every tag renders, so unbounded rows with thousands of selected values is on the consumer.
|
|
149
|
+
- A collapsed value cannot be removed from the control directly; deselect it through the listbox, where every selected option stays reachable, or with the clear button.
|
|
150
|
+
- Removing a visible tag can bring a collapsed one back into view; focus then moves to the next visible remove button rather than being dropped on a collapsed one.
|
|
151
|
+
|
|
152
|
+
## Keyboard interaction
|
|
153
|
+
|
|
154
|
+
### Trigger, panel closed
|
|
155
|
+
|
|
156
|
+
| Key | Behavior |
|
|
157
|
+
| ----------------- | ------------------------------------------------------------------------- |
|
|
158
|
+
| `ArrowDown` | Opens the panel. When not searchable, also makes the first option active. |
|
|
159
|
+
| `ArrowUp` | Opens the panel. When not searchable, also makes the last option active. |
|
|
160
|
+
| `Enter` / `Space` | Opens the panel without making an option active. |
|
|
161
|
+
|
|
162
|
+
Printable-character typeahead on a closed select-only combobox is not implemented.
|
|
163
|
+
|
|
164
|
+
### Multi-value remove buttons
|
|
165
|
+
|
|
166
|
+
| Key | Behavior |
|
|
167
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
168
|
+
| `Enter` / `Space` | Removes the value, then focuses the following remove button or returns focus to the combobox when there is no following value. |
|
|
169
|
+
| `ArrowDown` | Focuses the combobox and opens the panel, activating the first option when the panel does not contain a search. |
|
|
170
|
+
| `ArrowUp` | Focuses the combobox and opens the panel, activating the last option when the panel does not contain a search. |
|
|
171
|
+
|
|
172
|
+
### Clear button
|
|
173
|
+
|
|
174
|
+
| Key | Behavior |
|
|
175
|
+
| ----------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
176
|
+
| `Enter` / `Space` | Clears the selection. |
|
|
177
|
+
| `ArrowDown` | Focuses the combobox and opens the panel, activating the first option when the panel does not contain a search. |
|
|
178
|
+
| `ArrowUp` | Focuses the combobox and opens the panel, activating the last option when the panel does not contain a search. |
|
|
179
|
+
|
|
180
|
+
### Trigger, panel open and not searchable
|
|
181
|
+
|
|
182
|
+
Focus stays on the trigger, so it drives the list.
|
|
183
|
+
|
|
184
|
+
| Key | Behavior |
|
|
185
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
186
|
+
| `ArrowDown` | Moves virtual focus to the next option, clamping at the end of the list. With no active option, moves to the first. |
|
|
187
|
+
| `ArrowUp` | Moves virtual focus to the previous option. With no active option, moves to the last. |
|
|
188
|
+
| `Home` / `End` | Moves virtual focus to the first / last option. |
|
|
189
|
+
| `Enter` | Selects the active option; a disabled active option is left alone. With no active option, closes the panel. |
|
|
190
|
+
| `Space` | Matches `Enter`. |
|
|
191
|
+
| `Escape` | Closes the panel without propagating to an enclosing component. |
|
|
192
|
+
|
|
193
|
+
### Search input
|
|
194
|
+
|
|
195
|
+
Focus is inside the panel, so the trigger sees nothing.
|
|
196
|
+
|
|
197
|
+
| Key | Behavior |
|
|
198
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
199
|
+
| `ArrowDown` | Moves virtual focus to the next option, clamping at the end of the list. With no active option, moves to the first. |
|
|
200
|
+
| `ArrowUp` | Moves virtual focus to the previous option. With no active option, moves to the last. |
|
|
201
|
+
| `Enter` | Selects the active option, creates from the search value when the create row is active, or runs an active action. Multi-select toggles it. Does nothing while no option is active or the active option is disabled. |
|
|
202
|
+
| `Escape` | Clears the search value and closes the panel without propagating to an enclosing component. |
|
|
203
|
+
| `Home` / `End` | Left to native text-cursor behavior. |
|
|
204
|
+
|
|
205
|
+
Disabled options are included in keyboard navigation but cannot be selected, following the [APG guidance on the focusability of disabled controls](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_disabled_controls), which keeps them discoverable to screen reader users. Each one carries `aria-disabled="true"` so its state is announced when it takes virtual focus.
|
|
206
|
+
|
|
207
|
+
No option is active until the user navigates or hovers, and filtering the list clears the active option, so typing a query never puts a selection one keystroke away.
|
|
208
|
+
|
|
209
|
+
## Creating options
|
|
210
|
+
|
|
211
|
+
`isCreatable` adds one more `role="option"` at the end of the list, so creating uses the same virtual focus, hover, and activation path as every other option. It carries `aria-selected="false"`, and its accessible name is the create text, which by default reads `Create "<search value>"`.
|
|
212
|
+
|
|
213
|
+
- Creating needs text to build the option from, so `isCreatable` requires `isSearchable` and warns when it is missing.
|
|
214
|
+
- The create row is only reached by an explicit `ArrowDown`, `ArrowUp`, or hover, and never sits under `Enter` by default, so typing alone never puts a create action one keystroke away.
|
|
215
|
+
- When the query matches nothing, the create row is the only entry in the listbox and replaces the `role="status"` empty state.
|
|
216
|
+
- Activating it selects the created option, so it is announced through the trigger like any other value, and the search value resets so the list is usable again.
|
|
217
|
+
|
|
218
|
+
## Actions
|
|
219
|
+
|
|
220
|
+
`actions` adds rows at the end of the list, after a divider, that act instead of selecting. The use case is leaving the selection behind: a row like Create project that navigates into a create flow.
|
|
221
|
+
|
|
222
|
+
An action is a `role="option"` of the listbox, which gives it virtual focus, hover, and `Enter` like any option. It carries no `aria-selected`, so a screen reader never announces it as an unselected choice.
|
|
223
|
+
|
|
224
|
+
Visually, actions are pinned below the scrolling options so they stay in view however long the list is. In the DOM they sit after the scrollable listbox, which claims them through `aria-owns`; in the accessibility tree they remain the last options of the listbox, so keyboard navigation and `aria-activedescendant` treat them exactly like rows at the end of the list. When no option matches, there is no scrolling listbox and the pinned container itself becomes the listbox the trigger controls.
|
|
225
|
+
|
|
226
|
+
- Activating an action runs its `onSelect` and closes the panel, which returns focus to the trigger. The selection is left untouched and `onChange` does not fire.
|
|
227
|
+
- Actions are not results, so they are never filtered by the search value. When nothing matches, the `role="status"` empty state is announced above the list rather than replacing it, and the actions stay reachable.
|
|
228
|
+
- Give an action a label that reads as an action, since the label is its accessible name and nothing in the ARIA says it is not one of the choices.
|
|
229
|
+
- A disabled action stays reachable by keyboard navigation but ignores activation and clicks, the same as a disabled option.
|
|
230
|
+
- Dividers around actions — the one separating them from the options and any `hasDivider` one between two actions — are `aria-hidden` decoration, so a screen reader hears only the rows.
|
|
231
|
+
|
|
232
|
+
## Focus movement
|
|
233
|
+
|
|
234
|
+
- Opening a searchable combobox moves focus to the search input. Opening a select-only one leaves focus on the trigger.
|
|
235
|
+
- Closing the panel returns focus to the trigger, whichever way it closed: selection, `Escape`, or an outside click.
|
|
236
|
+
- Single select closes on selection. Multi-select stays open so several options can be picked in one pass, matching `Select`.
|
|
237
|
+
|
|
238
|
+
## Pointer and keyboard virtual focus
|
|
239
|
+
|
|
240
|
+
Hovering an option makes it the active option, so hover, `aria-activedescendant`, and what `Enter` selects always agree; hovering a disabled option leaves the active option unchanged. Only keyboard and filter changes scroll the active option into view, since scrolling on hover would move the list out from under the cursor.
|
|
241
|
+
|
|
242
|
+
## Custom triggers
|
|
243
|
+
|
|
244
|
+
`renderTrigger` receives the full set of trigger props, including `role="combobox"`, the ARIA above, a keyboard handler, and the ref the panel anchors to. Spread all of them onto a single focusable element. Spreading them onto a non-focusable element breaks keyboard access, since nothing else in the composition is tabbable.
|
|
245
|
+
|
|
246
|
+
A custom trigger replaces the label and the messages as well as the control, which is what makes an alternative field layout possible. Two things follow from that:
|
|
247
|
+
|
|
248
|
+
- It is named with `aria-label` rather than `aria-labelledby`, since it is free to leave the label unrendered.
|
|
249
|
+
- It still carries the `aria-describedby` of the message that would have been rendered. `state.field` hands over the label, the help text, the error text, and the ids they need, so render the message that `field.hasError` selects with `field.errorTextId` or `field.helpTextId`. Setting `helpText` or `errorText` and rendering neither leaves the trigger described by an element that does not exist.
|
|
250
|
+
|
|
251
|
+
## Custom value and option rendering
|
|
252
|
+
|
|
253
|
+
`components` replaces how an option is presented, never how it behaves. Selection, filtering, virtual focus, and the roles and states of the surrounding elements stay with the combobox, so an override can be as plain as a span.
|
|
254
|
+
|
|
255
|
+
| Override | Replaces | Rendered |
|
|
256
|
+
| ------------- | -------------------------------------------------- | ----------------------------------- |
|
|
257
|
+
| `MultiValue` | The tag of a selected value, remove button and all | In the control, next to the trigger |
|
|
258
|
+
| `SingleValue` | The value text of a single select | Inside the trigger |
|
|
259
|
+
| `OptionLabel` | The label of an option | Inside the option |
|
|
260
|
+
|
|
261
|
+
- A `MultiValue` without a removal affordance of its own leaves the list and the clear button as the ways to deselect, so the multi-value remove button keyboard table above no longer applies. Do not also drop `isClearable` unless reopening the panel is an acceptable way back.
|
|
262
|
+
- An override that does render a control has to stop the click from reaching the control, which would otherwise open the panel, and has to place focus itself once the value it lived in is gone. `remove` deselects the option and knows nothing about focus.
|
|
263
|
+
- `SingleValue` is rendered inside the trigger, which is what makes its content read as the current value. It must stay non-interactive, and inline: a block element inside a button is invalid.
|
|
264
|
+
- `OptionLabel` replaces the label of the option rather than the option itself. `role="option"`, `aria-selected`, `aria-disabled`, the multi-select checkbox, and the description are all kept, so option state never rests on the custom presentation alone. Color in particular carries nothing on its own.
|
|
265
|
+
- Every override is handed `isDisabled`, so a disabled combobox is presented as disabled rather than looking active.
|
|
266
|
+
|
|
267
|
+
## Implementation guidelines
|
|
268
|
+
|
|
269
|
+
- Provide a concise `label`. It names the trigger, the listbox, and the dialog panel.
|
|
270
|
+
- Reach for `hasVisuallyHiddenLabel` rather than dropping the label when the design has no room for it.
|
|
271
|
+
- Do not rely on leading visuals or selection indicators to communicate option state.
|
|
272
|
+
- Keep the label of an option readable as text when overriding `OptionLabel`, since it is the accessible name of the option.
|
|
273
|
+
|
|
274
|
+
## Examples
|
|
275
|
+
|
|
276
|
+
### Default
|
|
277
|
+
|
|
278
|
+
```tsx
|
|
279
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
280
|
+
|
|
281
|
+
import { Combobox } from '@neo4j-ndl/react';
|
|
282
|
+
|
|
283
|
+
const Component = () => {
|
|
284
|
+
let options = [
|
|
285
|
+
{
|
|
286
|
+
label: 'Option 1 ',
|
|
287
|
+
value: 'option-1',
|
|
288
|
+
},
|
|
289
|
+
{ label: 'Option 2', value: 'option-2' },
|
|
290
|
+
{
|
|
291
|
+
label: 'Option 3 has a really, really, really, really, long label',
|
|
292
|
+
value: 'option-3',
|
|
293
|
+
},
|
|
294
|
+
{ isDisabled: true, label: 'Option 4', value: 'option-4' },
|
|
295
|
+
];
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<Combobox label="Products" options={options} defaultValue={options[0]} />
|
|
299
|
+
);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
export default Component;
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Action On Select
|
|
306
|
+
|
|
307
|
+
```tsx
|
|
308
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
309
|
+
|
|
310
|
+
import { Combobox, IconButton, TextArea } from '@neo4j-ndl/react';
|
|
311
|
+
import { CommandLineIconOutline } from '@neo4j-ndl/react/icons';
|
|
312
|
+
import { useRef, useState } from 'react';
|
|
313
|
+
|
|
314
|
+
const commands = [
|
|
315
|
+
{
|
|
316
|
+
description: 'Return a small sample of nodes',
|
|
317
|
+
label: 'Match nodes',
|
|
318
|
+
value: 'MATCH (n) RETURN n LIMIT 25',
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
description: 'Show the labels and relationships in the database',
|
|
322
|
+
label: 'Visualize schema',
|
|
323
|
+
value: 'CALL db.schema.visualization()',
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
description: 'List the indexes available to the current user',
|
|
327
|
+
label: 'Show indexes',
|
|
328
|
+
value: 'SHOW INDEXES',
|
|
329
|
+
},
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
const Component = () => {
|
|
333
|
+
const [terminalValue, setTerminalValue] = useState('');
|
|
334
|
+
const [searchValue, setSearchValue] = useState('');
|
|
335
|
+
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
|
336
|
+
return (
|
|
337
|
+
<div className="n-flex n-gap-token-2 n-items-center">
|
|
338
|
+
<TextArea
|
|
339
|
+
ref={textAreaRef}
|
|
340
|
+
label="Command input"
|
|
341
|
+
value={terminalValue}
|
|
342
|
+
htmlAttributes={{
|
|
343
|
+
onChange: (event) => setTerminalValue(event.target.value),
|
|
344
|
+
rows: 5,
|
|
345
|
+
}}
|
|
346
|
+
helpText="Use the button below to search for a command."
|
|
347
|
+
/>
|
|
348
|
+
<Combobox
|
|
349
|
+
label="Insert command"
|
|
350
|
+
options={commands}
|
|
351
|
+
searchPlaceholder="Search commands"
|
|
352
|
+
searchValue={searchValue}
|
|
353
|
+
onSearchChange={setSearchValue}
|
|
354
|
+
// The picker activates an option but never keeps it as its value.
|
|
355
|
+
// Keeping the controlled value empty avoids a persistent selected state.
|
|
356
|
+
value={null}
|
|
357
|
+
onChange={(command) => {
|
|
358
|
+
if (command === null) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
setTerminalValue((currentValue) =>
|
|
363
|
+
currentValue === ''
|
|
364
|
+
? command.value
|
|
365
|
+
: `${currentValue}\n${command.value}`,
|
|
366
|
+
);
|
|
367
|
+
setSearchValue('');
|
|
368
|
+
textAreaRef.current?.focus();
|
|
369
|
+
}}
|
|
370
|
+
renderTrigger={({ ref, ...triggerProps }) => (
|
|
371
|
+
<IconButton
|
|
372
|
+
ref={ref as React.Ref<HTMLButtonElement>}
|
|
373
|
+
description="Insert command"
|
|
374
|
+
htmlAttributes={triggerProps}
|
|
375
|
+
>
|
|
376
|
+
<CommandLineIconOutline />
|
|
377
|
+
</IconButton>
|
|
378
|
+
)}
|
|
379
|
+
/>
|
|
380
|
+
</div>
|
|
381
|
+
);
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
export default Component;
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Actions
|
|
388
|
+
|
|
389
|
+
```tsx
|
|
390
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
391
|
+
|
|
392
|
+
import { Combobox } from '@neo4j-ndl/react';
|
|
393
|
+
import { PlusIconOutline } from '@neo4j-ndl/react/icons';
|
|
394
|
+
|
|
395
|
+
const options = [
|
|
396
|
+
{ label: 'Movie recommendations', value: 'movie-recommendations' },
|
|
397
|
+
{ label: 'Fraud detection', value: 'fraud-detection' },
|
|
398
|
+
{ label: 'Supply chain', value: 'supply-chain' },
|
|
399
|
+
{ label: 'Customer 360', value: 'customer-360' },
|
|
400
|
+
{ label: 'Knowledge graph', value: 'knowledge-graph' },
|
|
401
|
+
{ label: 'Network topology', value: 'network-topology' },
|
|
402
|
+
{ label: 'Identity resolution', value: 'identity-resolution' },
|
|
403
|
+
{ label: 'Master data management', value: 'master-data-management' },
|
|
404
|
+
{ label: 'Social network analysis', value: 'social-network-analysis' },
|
|
405
|
+
{ label: 'Route optimization', value: 'route-optimization' },
|
|
406
|
+
{ label: 'Recommendation engine', value: 'recommendation-engine' },
|
|
407
|
+
{ label: 'Access control', value: 'access-control' },
|
|
408
|
+
{ label: 'Data lineage', value: 'data-lineage' },
|
|
409
|
+
{ label: 'Entity resolution', value: 'entity-resolution' },
|
|
410
|
+
{ label: 'Bill of materials', value: 'bill-of-materials' },
|
|
411
|
+
{ label: 'Impact analysis', value: 'impact-analysis' },
|
|
412
|
+
{ label: 'Anti money laundering', value: 'anti-money-laundering' },
|
|
413
|
+
{ label: 'Dependency mapping', value: 'dependency-mapping' },
|
|
414
|
+
{ label: 'Patient journey', value: 'patient-journey' },
|
|
415
|
+
{ label: 'Content personalization', value: 'content-personalization' },
|
|
416
|
+
{ label: 'Risk assessment', value: 'risk-assessment' },
|
|
417
|
+
{ label: 'Churn prediction', value: 'churn-prediction' },
|
|
418
|
+
{ label: 'Inventory tracking', value: 'inventory-tracking' },
|
|
419
|
+
{ label: 'Semantic search', value: 'semantic-search' },
|
|
420
|
+
{ label: 'Digital twin', value: 'digital-twin' },
|
|
421
|
+
];
|
|
422
|
+
|
|
423
|
+
const Component = () => {
|
|
424
|
+
return (
|
|
425
|
+
<div className="n-flex n-flex-col n-gap-token-4">
|
|
426
|
+
<Combobox
|
|
427
|
+
label="Project"
|
|
428
|
+
placeholder="Select project"
|
|
429
|
+
searchPlaceholder="Search projects"
|
|
430
|
+
options={options}
|
|
431
|
+
// An action never becomes part of the value. It runs, the panel closes,
|
|
432
|
+
// and the selection is left as it was, which is what makes it usable for
|
|
433
|
+
// leaving the combobox entirely.
|
|
434
|
+
actions={[
|
|
435
|
+
{
|
|
436
|
+
label: 'All projects',
|
|
437
|
+
onSelect: () => console.info('all projects'),
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
// A divider above the action separates it from the actions before
|
|
441
|
+
// it. The first action never needs one: it already sits below the
|
|
442
|
+
// divider that separates actions from options.
|
|
443
|
+
hasDivider: true,
|
|
444
|
+
label: 'Create project',
|
|
445
|
+
leadingVisual: <PlusIconOutline />,
|
|
446
|
+
// The search value comes along, so an unmatched query can prefill
|
|
447
|
+
// the flow the action leads to.
|
|
448
|
+
onSelect: ({ searchValue }) =>
|
|
449
|
+
console.info('create project with search value', searchValue),
|
|
450
|
+
},
|
|
451
|
+
]}
|
|
452
|
+
helpText="actions stay pinned below the scrolling options, and are exempt from searching"
|
|
453
|
+
/>
|
|
454
|
+
</div>
|
|
455
|
+
);
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
export default Component;
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Async
|
|
462
|
+
|
|
463
|
+
```tsx
|
|
464
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
465
|
+
|
|
466
|
+
import { Combobox, Typography } from '@neo4j-ndl/react';
|
|
467
|
+
import { useEffect, useState } from 'react';
|
|
468
|
+
|
|
469
|
+
const catalog = [
|
|
470
|
+
{ label: 'Neo4j', value: 'neo4j' },
|
|
471
|
+
{ label: 'Aura', value: 'aura' },
|
|
472
|
+
{ label: 'Bloom', value: 'bloom' },
|
|
473
|
+
{ label: 'Browser', value: 'browser' },
|
|
474
|
+
{ label: 'Cypher', value: 'cypher' },
|
|
475
|
+
{ label: 'Data Importer', value: 'data-importer' },
|
|
476
|
+
{ label: 'GraphQL', value: 'graphql' },
|
|
477
|
+
];
|
|
478
|
+
|
|
479
|
+
const SEARCH_DELAY_MS = 600;
|
|
480
|
+
|
|
481
|
+
const Component = () => {
|
|
482
|
+
const [searchValue, setSearchValue] = useState('');
|
|
483
|
+
const [options, setOptions] = useState<typeof catalog>([]);
|
|
484
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
485
|
+
const [hasOpened, setHasOpened] = useState(false);
|
|
486
|
+
|
|
487
|
+
useEffect(() => {
|
|
488
|
+
if (!hasOpened) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
let isCancelled = false;
|
|
493
|
+
const query = searchValue.trim().toLowerCase();
|
|
494
|
+
|
|
495
|
+
setIsLoading(true);
|
|
496
|
+
setOptions([]);
|
|
497
|
+
|
|
498
|
+
const timeoutId = window.setTimeout(() => {
|
|
499
|
+
if (isCancelled) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
setOptions(
|
|
504
|
+
query === ''
|
|
505
|
+
? catalog
|
|
506
|
+
: catalog.filter((option) =>
|
|
507
|
+
option.label.toLowerCase().includes(query),
|
|
508
|
+
),
|
|
509
|
+
);
|
|
510
|
+
setIsLoading(false);
|
|
511
|
+
}, SEARCH_DELAY_MS);
|
|
512
|
+
|
|
513
|
+
return () => {
|
|
514
|
+
isCancelled = true;
|
|
515
|
+
window.clearTimeout(timeoutId);
|
|
516
|
+
};
|
|
517
|
+
}, [hasOpened, searchValue]);
|
|
518
|
+
|
|
519
|
+
return (
|
|
520
|
+
<div className="n-flex n-flex-col n-gap-token-4">
|
|
521
|
+
<Combobox
|
|
522
|
+
label="Products"
|
|
523
|
+
// The consumer supplies already-filtered options, so internal
|
|
524
|
+
// filtering must be disabled.
|
|
525
|
+
filterOption={false}
|
|
526
|
+
searchValue={searchValue}
|
|
527
|
+
onSearchChange={setSearchValue}
|
|
528
|
+
onOpenChange={(isOpen) => {
|
|
529
|
+
if (isOpen) {
|
|
530
|
+
setIsLoading(true);
|
|
531
|
+
setHasOpened(true);
|
|
532
|
+
}
|
|
533
|
+
}}
|
|
534
|
+
options={options}
|
|
535
|
+
isLoading={isLoading}
|
|
536
|
+
searchPlaceholder="Search products"
|
|
537
|
+
/>
|
|
538
|
+
<Typography variant="body-small" className="n-text-neutral-text-weak">
|
|
539
|
+
The consumer clears, fetches, and filters options after a short delay.
|
|
540
|
+
The loading state is shown while each request is in flight.
|
|
541
|
+
</Typography>
|
|
542
|
+
</div>
|
|
543
|
+
);
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
export default Component;
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### Clean
|
|
550
|
+
|
|
551
|
+
```tsx
|
|
552
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
553
|
+
|
|
554
|
+
import { Combobox } from '@neo4j-ndl/react';
|
|
555
|
+
|
|
556
|
+
const Component = () => {
|
|
557
|
+
const options = [
|
|
558
|
+
{ label: 'Neo4j', value: 'neo4j' },
|
|
559
|
+
{ label: 'Aura', value: 'aura' },
|
|
560
|
+
{ label: 'Bloom', value: 'bloom' },
|
|
561
|
+
{ isDisabled: true, label: 'Unavailable', value: 'unavailable' },
|
|
562
|
+
];
|
|
563
|
+
|
|
564
|
+
return (
|
|
565
|
+
<Combobox
|
|
566
|
+
label="Product"
|
|
567
|
+
// A clean combobox sits inline, next to whatever already names it.
|
|
568
|
+
hasVisuallyHiddenLabel
|
|
569
|
+
isClean
|
|
570
|
+
options={options}
|
|
571
|
+
defaultValue={options[0]}
|
|
572
|
+
/>
|
|
573
|
+
);
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
export default Component;
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### Creatable
|
|
580
|
+
|
|
581
|
+
```tsx
|
|
582
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
583
|
+
|
|
584
|
+
import { Combobox } from '@neo4j-ndl/react';
|
|
585
|
+
import { useState } from 'react';
|
|
586
|
+
|
|
587
|
+
type Option = {
|
|
588
|
+
label: string;
|
|
589
|
+
value: string;
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const INITIAL_OPTIONS: Option[] = [
|
|
593
|
+
{ label: 'Recommendations', value: 'recommendations' },
|
|
594
|
+
{ label: 'Fraud detection', value: 'fraud-detection' },
|
|
595
|
+
{ label: 'Knowledge graph', value: 'knowledge-graph' },
|
|
596
|
+
];
|
|
597
|
+
|
|
598
|
+
const Component = () => {
|
|
599
|
+
const [options, setOptions] = useState(INITIAL_OPTIONS);
|
|
600
|
+
const [value, setValue] = useState<Option[]>([]);
|
|
601
|
+
|
|
602
|
+
return (
|
|
603
|
+
<Combobox
|
|
604
|
+
label="Use cases"
|
|
605
|
+
placeholder="Select use cases"
|
|
606
|
+
searchPlaceholder="Search or add a use case"
|
|
607
|
+
isMulti
|
|
608
|
+
isCreatable
|
|
609
|
+
isClearable
|
|
610
|
+
options={options}
|
|
611
|
+
value={value}
|
|
612
|
+
onChange={(nextValue) => setValue(nextValue)}
|
|
613
|
+
// Created options are not added to `options` by the Combobox, so they are
|
|
614
|
+
// appended here to keep them available for later selections.
|
|
615
|
+
onCreateOption={(option) => setOptions((current) => [...current, option])}
|
|
616
|
+
/>
|
|
617
|
+
);
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
export default Component;
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### Custom Multi
|
|
624
|
+
|
|
625
|
+
```tsx
|
|
626
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
627
|
+
|
|
628
|
+
import { tokens } from '@neo4j-ndl/base';
|
|
629
|
+
import { Combobox, GraphLabel } from '@neo4j-ndl/react';
|
|
630
|
+
import { useState } from 'react';
|
|
631
|
+
|
|
632
|
+
// The option type is free to carry fields of its own. Passing these options to
|
|
633
|
+
// the combobox is what types `option` inside the overrides below.
|
|
634
|
+
type NodeLabelOption = {
|
|
635
|
+
label: string;
|
|
636
|
+
value: string;
|
|
637
|
+
color: string;
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
const options: NodeLabelOption[] = [
|
|
641
|
+
{ color: tokens.graph['1'], label: 'Person', value: 'person' },
|
|
642
|
+
{ color: tokens.graph['2'], label: 'Movie', value: 'movie' },
|
|
643
|
+
{ color: tokens.graph['3'], label: 'Director', value: 'director' },
|
|
644
|
+
{ color: tokens.graph['4'], label: 'Actor', value: 'actor' },
|
|
645
|
+
{ color: tokens.graph['7'], label: 'Genre', value: 'genre' },
|
|
646
|
+
{ color: tokens.graph['12'], label: 'Studio', value: 'studio' },
|
|
647
|
+
];
|
|
648
|
+
|
|
649
|
+
// One presentation for the selected values and for the options, so both read
|
|
650
|
+
// the same. `as="span"` keeps it non-interactive, which leaves the list and the
|
|
651
|
+
// clear button as the ways to deselect.
|
|
652
|
+
const NodeLabel = ({
|
|
653
|
+
option,
|
|
654
|
+
isDisabled,
|
|
655
|
+
}: {
|
|
656
|
+
option: NodeLabelOption;
|
|
657
|
+
isDisabled: boolean;
|
|
658
|
+
}) => (
|
|
659
|
+
<GraphLabel
|
|
660
|
+
as="span"
|
|
661
|
+
type="node"
|
|
662
|
+
size="small"
|
|
663
|
+
color={option.color}
|
|
664
|
+
isDisabled={isDisabled}
|
|
665
|
+
>
|
|
666
|
+
{option.label}
|
|
667
|
+
</GraphLabel>
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
const Component = () => {
|
|
671
|
+
const [value, setValue] = useState<NodeLabelOption[]>(options.slice(0, 2));
|
|
672
|
+
|
|
673
|
+
return (
|
|
674
|
+
<Combobox
|
|
675
|
+
label="Node labels"
|
|
676
|
+
helpText="Deselect a label in the list to remove it."
|
|
677
|
+
isMulti
|
|
678
|
+
isClearable
|
|
679
|
+
options={options}
|
|
680
|
+
value={value}
|
|
681
|
+
onChange={setValue}
|
|
682
|
+
components={{ MultiValue: NodeLabel, OptionLabel: NodeLabel }}
|
|
683
|
+
/>
|
|
684
|
+
);
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
export default Component;
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
### Custom Single
|
|
691
|
+
|
|
692
|
+
```tsx
|
|
693
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
694
|
+
|
|
695
|
+
import { tokens } from '@neo4j-ndl/base';
|
|
696
|
+
import { Combobox, GraphLabel } from '@neo4j-ndl/react';
|
|
697
|
+
import { useState } from 'react';
|
|
698
|
+
|
|
699
|
+
// The option type is free to carry fields of its own. Passing these options to
|
|
700
|
+
// the combobox is what types `option` inside the overrides below.
|
|
701
|
+
type NodeLabelOption = {
|
|
702
|
+
label: string;
|
|
703
|
+
value: string;
|
|
704
|
+
color: string;
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
const options: NodeLabelOption[] = [
|
|
708
|
+
{ color: tokens.graph['1'], label: 'Person', value: 'person' },
|
|
709
|
+
{ color: tokens.graph['2'], label: 'Movie', value: 'movie' },
|
|
710
|
+
{ color: tokens.graph['3'], label: 'Director', value: 'director' },
|
|
711
|
+
{ color: tokens.graph['4'], label: 'Actor', value: 'actor' },
|
|
712
|
+
{ color: tokens.graph['7'], label: 'Genre', value: 'genre' },
|
|
713
|
+
{ color: tokens.graph['12'], label: 'Studio', value: 'studio' },
|
|
714
|
+
];
|
|
715
|
+
|
|
716
|
+
// One presentation for the selected value and for the options, so both read
|
|
717
|
+
// the same. `as="span"` keeps the label non-interactive.
|
|
718
|
+
const NodeLabel = ({
|
|
719
|
+
option,
|
|
720
|
+
isDisabled,
|
|
721
|
+
}: {
|
|
722
|
+
option: NodeLabelOption;
|
|
723
|
+
isDisabled: boolean;
|
|
724
|
+
}) => (
|
|
725
|
+
<GraphLabel
|
|
726
|
+
as="span"
|
|
727
|
+
type="node"
|
|
728
|
+
size="small"
|
|
729
|
+
color={option.color}
|
|
730
|
+
isDisabled={isDisabled}
|
|
731
|
+
>
|
|
732
|
+
{option.label}
|
|
733
|
+
</GraphLabel>
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
const Component = () => {
|
|
737
|
+
const [value, setValue] = useState<NodeLabelOption | null>(options[0]);
|
|
738
|
+
|
|
739
|
+
return (
|
|
740
|
+
<Combobox
|
|
741
|
+
label="Node label"
|
|
742
|
+
isClearable
|
|
743
|
+
options={options}
|
|
744
|
+
value={value}
|
|
745
|
+
onChange={setValue}
|
|
746
|
+
components={{ OptionLabel: NodeLabel, SingleValue: NodeLabel }}
|
|
747
|
+
/>
|
|
748
|
+
);
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
export default Component;
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
### Custom Trigger
|
|
755
|
+
|
|
756
|
+
```tsx
|
|
757
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
758
|
+
|
|
759
|
+
import { Combobox, OutlinedButton } from '@neo4j-ndl/react';
|
|
760
|
+
import { AdjustmentsHorizontalIconOutline } from '@neo4j-ndl/react/icons';
|
|
761
|
+
|
|
762
|
+
const Component = () => {
|
|
763
|
+
const options = [
|
|
764
|
+
{ label: 'Neo4j', value: 'neo4j' },
|
|
765
|
+
{ label: 'Aura', value: 'aura' },
|
|
766
|
+
{ label: 'Bloom', value: 'bloom' },
|
|
767
|
+
];
|
|
768
|
+
|
|
769
|
+
return (
|
|
770
|
+
<Combobox
|
|
771
|
+
// The custom trigger renders no label of its own, so the label reaches it
|
|
772
|
+
// as its accessible name only.
|
|
773
|
+
label="Product"
|
|
774
|
+
options={options}
|
|
775
|
+
searchPlaceholder="Search products"
|
|
776
|
+
// The panel anchors to whichever element the props land on, and that
|
|
777
|
+
// element becomes the combobox, so it has to be focusable.
|
|
778
|
+
renderTrigger={({ ref, ...triggerProps }, state) => (
|
|
779
|
+
<OutlinedButton
|
|
780
|
+
ref={ref as React.Ref<HTMLButtonElement>}
|
|
781
|
+
htmlAttributes={triggerProps}
|
|
782
|
+
leadingVisual={<AdjustmentsHorizontalIconOutline />}
|
|
783
|
+
>
|
|
784
|
+
{state.selectedOptions[0]?.label ?? 'Pick a product'}
|
|
785
|
+
</OutlinedButton>
|
|
786
|
+
)}
|
|
787
|
+
/>
|
|
788
|
+
);
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
export default Component;
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
### Descriptions
|
|
795
|
+
|
|
796
|
+
```tsx
|
|
797
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
798
|
+
|
|
799
|
+
import { Combobox } from '@neo4j-ndl/react';
|
|
800
|
+
|
|
801
|
+
const options = [
|
|
802
|
+
{
|
|
803
|
+
description: 'A fully managed graph database service',
|
|
804
|
+
label: 'AuraDB',
|
|
805
|
+
value: 'auradb',
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
description: 'Graph analytics and data science tools',
|
|
809
|
+
label: 'Graph Data Science',
|
|
810
|
+
value: 'graph-data-science',
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
description: 'Explore and visualize connected data',
|
|
814
|
+
label: 'Neo4j Bloom',
|
|
815
|
+
value: 'neo4j-bloom',
|
|
816
|
+
},
|
|
817
|
+
];
|
|
818
|
+
|
|
819
|
+
const Component = () => (
|
|
820
|
+
<Combobox label="Products" options={options} placeholder="Select product" />
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
export default Component;
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
### Disabled
|
|
827
|
+
|
|
828
|
+
```tsx
|
|
829
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
830
|
+
|
|
831
|
+
import { Combobox } from '@neo4j-ndl/react';
|
|
832
|
+
|
|
833
|
+
const options = [
|
|
834
|
+
{ label: 'Neo4j', value: 'neo4j' },
|
|
835
|
+
{ label: 'Aura', value: 'aura' },
|
|
836
|
+
{ label: 'Bloom', value: 'bloom' },
|
|
837
|
+
{ label: 'Data Importer', value: 'data-importer' },
|
|
838
|
+
];
|
|
839
|
+
|
|
840
|
+
const Component = () => (
|
|
841
|
+
<div className="n-flex n-flex-col n-items-start n-gap-token-16">
|
|
842
|
+
<Combobox
|
|
843
|
+
label="Disabled single select"
|
|
844
|
+
defaultValue={options[0]}
|
|
845
|
+
isDisabled
|
|
846
|
+
options={options}
|
|
847
|
+
/>
|
|
848
|
+
<Combobox
|
|
849
|
+
label="Disabled multi select"
|
|
850
|
+
defaultValue={options.slice(0, 3)}
|
|
851
|
+
isDisabled
|
|
852
|
+
isMulti
|
|
853
|
+
options={options}
|
|
854
|
+
/>
|
|
855
|
+
</div>
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
export default Component;
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
### Fluid
|
|
862
|
+
|
|
863
|
+
```tsx
|
|
864
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
865
|
+
|
|
866
|
+
import { Combobox } from '@neo4j-ndl/react';
|
|
867
|
+
|
|
868
|
+
const options = [
|
|
869
|
+
{ label: 'Neo4j', value: 'neo4j' },
|
|
870
|
+
{ label: 'Aura', value: 'aura' },
|
|
871
|
+
{ label: 'Bloom', value: 'bloom' },
|
|
872
|
+
{ label: 'Data Importer', value: 'data-importer' },
|
|
873
|
+
];
|
|
874
|
+
|
|
875
|
+
const Component = () => (
|
|
876
|
+
<div className="n-w-full">
|
|
877
|
+
<Combobox
|
|
878
|
+
label="Product"
|
|
879
|
+
helpText="This combobox fills the width of its parent."
|
|
880
|
+
isClearable
|
|
881
|
+
isFluid
|
|
882
|
+
options={options}
|
|
883
|
+
/>
|
|
884
|
+
</div>
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
export default Component;
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
### Groups
|
|
891
|
+
|
|
892
|
+
```tsx
|
|
893
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
894
|
+
|
|
895
|
+
import { Combobox } from '@neo4j-ndl/react';
|
|
896
|
+
|
|
897
|
+
const Component = () => {
|
|
898
|
+
let options = [
|
|
899
|
+
{
|
|
900
|
+
label: 'Group 1',
|
|
901
|
+
options: [
|
|
902
|
+
{ label: 'Option 1', value: 'option-1' },
|
|
903
|
+
{ label: 'Option 2', value: 'option-2' },
|
|
904
|
+
{ label: 'Option 3', value: 'option-3' },
|
|
905
|
+
],
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
label: 'Group 2',
|
|
909
|
+
options: [
|
|
910
|
+
{ label: 'Option 4', value: 'option-4' },
|
|
911
|
+
{ isDisabled: true, label: 'Option 5', value: 'option-5' },
|
|
912
|
+
{ label: 'Option 6', value: 'option-6' },
|
|
913
|
+
{ label: 'Option 7', value: 'option-7' },
|
|
914
|
+
],
|
|
915
|
+
},
|
|
916
|
+
];
|
|
917
|
+
|
|
918
|
+
return <Combobox label="Products by category" options={options} />;
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
export default Component;
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
### Hidden Label
|
|
925
|
+
|
|
926
|
+
```tsx
|
|
927
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
928
|
+
|
|
929
|
+
import { Combobox, Typography } from '@neo4j-ndl/react';
|
|
930
|
+
|
|
931
|
+
const options = [
|
|
932
|
+
{ label: 'Neo4j', value: 'neo4j' },
|
|
933
|
+
{ label: 'Aura', value: 'aura' },
|
|
934
|
+
{ label: 'Bloom', value: 'bloom' },
|
|
935
|
+
];
|
|
936
|
+
|
|
937
|
+
const Component = () => (
|
|
938
|
+
<div className="n-flex n-flex-col n-gap-token-4">
|
|
939
|
+
<Typography variant="body-medium">Filter results by product</Typography>
|
|
940
|
+
<Combobox
|
|
941
|
+
label="Product"
|
|
942
|
+
hasVisuallyHiddenLabel
|
|
943
|
+
options={options}
|
|
944
|
+
placeholder="All products"
|
|
945
|
+
/>
|
|
946
|
+
</div>
|
|
947
|
+
);
|
|
948
|
+
|
|
949
|
+
export default Component;
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
### Icons
|
|
953
|
+
|
|
954
|
+
```tsx
|
|
955
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
956
|
+
|
|
957
|
+
import { Combobox } from '@neo4j-ndl/react';
|
|
958
|
+
import {
|
|
959
|
+
BeakerIconOutline,
|
|
960
|
+
BoltIconOutline,
|
|
961
|
+
CloudIconOutline,
|
|
962
|
+
} from '@neo4j-ndl/react/icons';
|
|
963
|
+
|
|
964
|
+
const options = [
|
|
965
|
+
{
|
|
966
|
+
label: 'AuraDB',
|
|
967
|
+
leadingVisual: <CloudIconOutline />,
|
|
968
|
+
value: 'auradb',
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
label: 'Graph Data Science',
|
|
972
|
+
leadingVisual: <BeakerIconOutline />,
|
|
973
|
+
value: 'graph-data-science',
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
label: 'Neo4j Bloom',
|
|
977
|
+
leadingVisual: <BoltIconOutline />,
|
|
978
|
+
value: 'neo4j-bloom',
|
|
979
|
+
},
|
|
980
|
+
];
|
|
981
|
+
|
|
982
|
+
const Component = () => (
|
|
983
|
+
<Combobox label="Products" options={options} placeholder="Select product" />
|
|
984
|
+
);
|
|
985
|
+
|
|
986
|
+
export default Component;
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
### In Dialog
|
|
990
|
+
|
|
991
|
+
```tsx
|
|
992
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
993
|
+
|
|
994
|
+
import { Combobox, Dialog, FilledButton } from '@neo4j-ndl/react';
|
|
995
|
+
import { useState } from 'react';
|
|
996
|
+
|
|
997
|
+
const options = [
|
|
998
|
+
{ label: 'Neo4j', value: 'neo4j' },
|
|
999
|
+
{ label: 'Aura', value: 'aura' },
|
|
1000
|
+
{ label: 'Bloom', value: 'bloom' },
|
|
1001
|
+
{ label: 'Data Importer', value: 'data-importer' },
|
|
1002
|
+
{ label: 'GraphQL Library', value: 'graphql-library' },
|
|
1003
|
+
{ label: 'Cypher', value: 'cypher' },
|
|
1004
|
+
];
|
|
1005
|
+
|
|
1006
|
+
const Component = () => {
|
|
1007
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
1008
|
+
|
|
1009
|
+
return (
|
|
1010
|
+
<>
|
|
1011
|
+
<FilledButton onClick={() => setIsOpen(true)}>Open dialog</FilledButton>
|
|
1012
|
+
<Dialog isOpen={isOpen} onClose={() => setIsOpen(false)} size="medium">
|
|
1013
|
+
<Dialog.Header>Combobox in a dialog</Dialog.Header>
|
|
1014
|
+
<Dialog.Description>
|
|
1015
|
+
The panel automatically uses fixed positioning and stays inside the
|
|
1016
|
+
dialog rather than being portaled.
|
|
1017
|
+
</Dialog.Description>
|
|
1018
|
+
<Dialog.Content>
|
|
1019
|
+
<Combobox
|
|
1020
|
+
label="Product"
|
|
1021
|
+
helpText="Open the list to inspect clipping and stacking."
|
|
1022
|
+
isFluid
|
|
1023
|
+
options={options}
|
|
1024
|
+
searchPlaceholder="Search products"
|
|
1025
|
+
/>
|
|
1026
|
+
</Dialog.Content>
|
|
1027
|
+
</Dialog>
|
|
1028
|
+
</>
|
|
1029
|
+
);
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
export default Component;
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
### In Popover
|
|
1036
|
+
|
|
1037
|
+
```tsx
|
|
1038
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
1039
|
+
|
|
1040
|
+
import { Combobox, FilledButton, Popover, Typography } from '@neo4j-ndl/react';
|
|
1041
|
+
|
|
1042
|
+
const options = [
|
|
1043
|
+
{ label: 'Neo4j', value: 'neo4j' },
|
|
1044
|
+
{ label: 'Aura', value: 'aura' },
|
|
1045
|
+
{ label: 'Bloom', value: 'bloom' },
|
|
1046
|
+
{ label: 'Data Importer', value: 'data-importer' },
|
|
1047
|
+
{ label: 'GraphQL Library', value: 'graphql-library' },
|
|
1048
|
+
{ label: 'Cypher', value: 'cypher' },
|
|
1049
|
+
];
|
|
1050
|
+
|
|
1051
|
+
const Component = () => (
|
|
1052
|
+
<Popover isPortaled>
|
|
1053
|
+
<Popover.Trigger hasButtonWrapper>
|
|
1054
|
+
<FilledButton>Open popover</FilledButton>
|
|
1055
|
+
</Popover.Trigger>
|
|
1056
|
+
<Popover.Content className="n-max-h-[240px] n-w-[400px] n-overflow-y-auto n-p-token-16">
|
|
1057
|
+
<div className="n-flex n-flex-col n-gap-token-16">
|
|
1058
|
+
<Typography variant="body-medium">
|
|
1059
|
+
Scroll this popover while the combobox panel is open to verify that it
|
|
1060
|
+
remains anchored and is not clipped by the overflow container.
|
|
1061
|
+
</Typography>
|
|
1062
|
+
<div className="n-h-[120px] n-shrink-0 n-rounded-sm n-border n-border-neutral-border-strong n-bg-neutral-bg-weak" />
|
|
1063
|
+
<Combobox
|
|
1064
|
+
label="Product"
|
|
1065
|
+
helpText="The combobox panel is independently portaled above this popover."
|
|
1066
|
+
isFluid
|
|
1067
|
+
options={options}
|
|
1068
|
+
searchPlaceholder="Search products"
|
|
1069
|
+
/>
|
|
1070
|
+
<div className="n-h-[120px] n-shrink-0 n-rounded-sm n-border n-border-neutral-border-strong n-bg-neutral-bg-weak" />
|
|
1071
|
+
</div>
|
|
1072
|
+
</Popover.Content>
|
|
1073
|
+
</Popover>
|
|
1074
|
+
);
|
|
1075
|
+
|
|
1076
|
+
export default Component;
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
### In Popover Dialog
|
|
1080
|
+
|
|
1081
|
+
```tsx
|
|
1082
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
1083
|
+
|
|
1084
|
+
import { Combobox, Dialog, FilledButton, Popover } from '@neo4j-ndl/react';
|
|
1085
|
+
import { useState } from 'react';
|
|
1086
|
+
|
|
1087
|
+
const options = [
|
|
1088
|
+
{ label: 'Neo4j', value: 'neo4j' },
|
|
1089
|
+
{ label: 'Aura', value: 'aura' },
|
|
1090
|
+
{ label: 'Bloom', value: 'bloom' },
|
|
1091
|
+
{ label: 'Data Importer', value: 'data-importer' },
|
|
1092
|
+
{ label: 'GraphQL Library', value: 'graphql-library' },
|
|
1093
|
+
{ label: 'Cypher', value: 'cypher' },
|
|
1094
|
+
];
|
|
1095
|
+
|
|
1096
|
+
const Component = () => {
|
|
1097
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
1098
|
+
|
|
1099
|
+
return (
|
|
1100
|
+
<>
|
|
1101
|
+
<FilledButton onClick={() => setIsOpen(true)}>Open dialog</FilledButton>
|
|
1102
|
+
<Dialog isOpen={isOpen} onClose={() => setIsOpen(false)} size="medium">
|
|
1103
|
+
<Dialog.Header>Combobox in a popover in a dialog</Dialog.Header>
|
|
1104
|
+
<Dialog.Description>
|
|
1105
|
+
This exercises nested portals, fixed positioning, focus handling, and
|
|
1106
|
+
stacking across all three components.
|
|
1107
|
+
</Dialog.Description>
|
|
1108
|
+
<Dialog.Content>
|
|
1109
|
+
<Popover isPortaled hasAnchorPortal strategy="fixed">
|
|
1110
|
+
<Popover.Trigger hasButtonWrapper>
|
|
1111
|
+
<FilledButton>Open popover</FilledButton>
|
|
1112
|
+
</Popover.Trigger>
|
|
1113
|
+
<Popover.Content className="n-w-[400px] n-p-token-16">
|
|
1114
|
+
<Combobox
|
|
1115
|
+
label="Product"
|
|
1116
|
+
errorText="This error styling should inherit the dialog theme."
|
|
1117
|
+
isFluid
|
|
1118
|
+
options={options}
|
|
1119
|
+
searchPlaceholder="Search products"
|
|
1120
|
+
/>
|
|
1121
|
+
</Popover.Content>
|
|
1122
|
+
</Popover>
|
|
1123
|
+
</Dialog.Content>
|
|
1124
|
+
</Dialog>
|
|
1125
|
+
</>
|
|
1126
|
+
);
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
export default Component;
|
|
1130
|
+
```
|
|
1131
|
+
|
|
1132
|
+
### Large Data Set
|
|
1133
|
+
|
|
1134
|
+
```tsx
|
|
1135
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
1136
|
+
|
|
1137
|
+
import { Combobox } from '@neo4j-ndl/react';
|
|
1138
|
+
import { useState } from 'react';
|
|
1139
|
+
|
|
1140
|
+
type Option = {
|
|
1141
|
+
label: string;
|
|
1142
|
+
value: string;
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
const options: Option[] = Array.from({ length: 10000 }, (_, index) => ({
|
|
1146
|
+
label: `Node ${index + 1}`,
|
|
1147
|
+
value: `node-${index + 1}`,
|
|
1148
|
+
}));
|
|
1149
|
+
|
|
1150
|
+
const Component = () => {
|
|
1151
|
+
const [value, setValue] = useState(options.slice(0, 1500));
|
|
1152
|
+
|
|
1153
|
+
return (
|
|
1154
|
+
<Combobox
|
|
1155
|
+
label="Nodes"
|
|
1156
|
+
isClearable
|
|
1157
|
+
isMulti
|
|
1158
|
+
options={options}
|
|
1159
|
+
value={value}
|
|
1160
|
+
onChange={(nextValue) => setValue(nextValue)}
|
|
1161
|
+
helpText="10,000 options with 1,500 selected: the list renders only its visible rows and the counter rounds to thousands."
|
|
1162
|
+
/>
|
|
1163
|
+
);
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1166
|
+
export default Component;
|
|
1167
|
+
```
|
|
1168
|
+
|
|
1169
|
+
### Max Rows
|
|
1170
|
+
|
|
1171
|
+
```tsx
|
|
1172
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
1173
|
+
|
|
1174
|
+
import { Combobox } from '@neo4j-ndl/react';
|
|
1175
|
+
import { useState } from 'react';
|
|
1176
|
+
|
|
1177
|
+
type Option = {
|
|
1178
|
+
label: string;
|
|
1179
|
+
value: string;
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
const Component = () => {
|
|
1183
|
+
const options: Option[] = [
|
|
1184
|
+
{ label: 'Neo4j', value: 'neo4j' },
|
|
1185
|
+
{ label: 'Aura', value: 'aura' },
|
|
1186
|
+
{ label: 'Bloom', value: 'bloom' },
|
|
1187
|
+
{ label: 'Data Importer', value: 'data-importer' },
|
|
1188
|
+
{ label: 'GraphQL Library', value: 'graphql-library' },
|
|
1189
|
+
{ label: 'Cypher', value: 'cypher' },
|
|
1190
|
+
{ label: 'Operations Manager', value: 'operations-manager' },
|
|
1191
|
+
{ label: 'Graph Data Science', value: 'graph-data-science' },
|
|
1192
|
+
];
|
|
1193
|
+
const [value, setValue] = useState(options);
|
|
1194
|
+
|
|
1195
|
+
return (
|
|
1196
|
+
<Combobox
|
|
1197
|
+
label="Products"
|
|
1198
|
+
isClearable
|
|
1199
|
+
isMulti
|
|
1200
|
+
maxRows={2}
|
|
1201
|
+
options={options}
|
|
1202
|
+
value={value}
|
|
1203
|
+
onChange={(nextValue) => setValue(nextValue)}
|
|
1204
|
+
helpText="Set maxRows to 2 to limit the number of rows to 2."
|
|
1205
|
+
/>
|
|
1206
|
+
);
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
export default Component;
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
### Multi
|
|
1213
|
+
|
|
1214
|
+
```tsx
|
|
1215
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
1216
|
+
|
|
1217
|
+
import { Combobox } from '@neo4j-ndl/react';
|
|
1218
|
+
import { type ReactNode, useState } from 'react';
|
|
1219
|
+
|
|
1220
|
+
type Option = {
|
|
1221
|
+
label: string;
|
|
1222
|
+
value: string;
|
|
1223
|
+
leadingVisual?: ReactNode;
|
|
1224
|
+
description?: string;
|
|
1225
|
+
isDisabled?: boolean;
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
const Component = () => {
|
|
1229
|
+
const options: Option[] = [
|
|
1230
|
+
{ label: 'Neo4j', value: 'neo4j' },
|
|
1231
|
+
{ label: 'Aura', value: 'aura' },
|
|
1232
|
+
{ label: 'Bloom', value: 'bloom' },
|
|
1233
|
+
{ label: 'Data Importer', value: 'data-importer' },
|
|
1234
|
+
{
|
|
1235
|
+
label: 'GraphQL Library has a really really long label',
|
|
1236
|
+
value: 'graphql-library',
|
|
1237
|
+
},
|
|
1238
|
+
{ label: 'Cypher', value: 'cypher' },
|
|
1239
|
+
{ isDisabled: true, label: 'Unavailable', value: 'unavailable' },
|
|
1240
|
+
{ label: 'Operations Manager', value: 'operations-manager' },
|
|
1241
|
+
];
|
|
1242
|
+
const [value, setValue] = useState(options.slice(0, 5));
|
|
1243
|
+
|
|
1244
|
+
return (
|
|
1245
|
+
<Combobox
|
|
1246
|
+
label="Products"
|
|
1247
|
+
isClearable
|
|
1248
|
+
isMulti
|
|
1249
|
+
options={options}
|
|
1250
|
+
value={value}
|
|
1251
|
+
onChange={(nextValue) => setValue(nextValue)}
|
|
1252
|
+
/>
|
|
1253
|
+
);
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
export default Component;
|
|
1257
|
+
```
|
|
1258
|
+
|
|
1259
|
+
### Native Form
|
|
1260
|
+
|
|
1261
|
+
```tsx
|
|
1262
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
1263
|
+
|
|
1264
|
+
import {
|
|
1265
|
+
Combobox,
|
|
1266
|
+
FilledButton,
|
|
1267
|
+
OutlinedButton,
|
|
1268
|
+
TextButton,
|
|
1269
|
+
Typography,
|
|
1270
|
+
} from '@neo4j-ndl/react';
|
|
1271
|
+
import { type FormEvent, useRef, useState } from 'react';
|
|
1272
|
+
|
|
1273
|
+
const products = [
|
|
1274
|
+
{ label: 'Neo4j', value: 'neo4j' },
|
|
1275
|
+
{ label: 'Aura', value: 'aura' },
|
|
1276
|
+
{ label: 'Bloom', value: 'bloom' },
|
|
1277
|
+
];
|
|
1278
|
+
|
|
1279
|
+
const environments = [
|
|
1280
|
+
{ label: 'Development', value: 'development' },
|
|
1281
|
+
{ label: 'Staging', value: 'staging' },
|
|
1282
|
+
{ label: 'Production', value: 'production' },
|
|
1283
|
+
];
|
|
1284
|
+
|
|
1285
|
+
const Component = () => {
|
|
1286
|
+
const triggerRef = useRef<HTMLElement | null>(null);
|
|
1287
|
+
const [submittedValues, setSubmittedValues] = useState<string[]>([]);
|
|
1288
|
+
|
|
1289
|
+
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
|
1290
|
+
event.preventDefault();
|
|
1291
|
+
setSubmittedValues(
|
|
1292
|
+
Array.from(new FormData(event.currentTarget).entries()).map(
|
|
1293
|
+
([name, value]) => `${name}: ${String(value)}`,
|
|
1294
|
+
),
|
|
1295
|
+
);
|
|
1296
|
+
};
|
|
1297
|
+
|
|
1298
|
+
return (
|
|
1299
|
+
<form
|
|
1300
|
+
className="n-flex n-max-w-xl n-flex-col n-gap-token-6"
|
|
1301
|
+
onSubmit={handleSubmit}
|
|
1302
|
+
>
|
|
1303
|
+
<Combobox
|
|
1304
|
+
label="Product"
|
|
1305
|
+
name="product"
|
|
1306
|
+
options={products}
|
|
1307
|
+
defaultValue={products[0]}
|
|
1308
|
+
isRequired
|
|
1309
|
+
hasRequiredIndicator
|
|
1310
|
+
triggerRef={triggerRef}
|
|
1311
|
+
/>
|
|
1312
|
+
<Combobox
|
|
1313
|
+
label="Environments"
|
|
1314
|
+
name="environment"
|
|
1315
|
+
options={environments}
|
|
1316
|
+
defaultValue={[environments[0], environments[1]]}
|
|
1317
|
+
isMulti
|
|
1318
|
+
/>
|
|
1319
|
+
<div className="n-flex n-flex-wrap n-gap-token-2">
|
|
1320
|
+
<FilledButton type="submit">Submit</FilledButton>
|
|
1321
|
+
<OutlinedButton type="reset">Reset</OutlinedButton>
|
|
1322
|
+
<TextButton type="button" onClick={() => triggerRef.current?.focus()}>
|
|
1323
|
+
Focus product
|
|
1324
|
+
</TextButton>
|
|
1325
|
+
</div>
|
|
1326
|
+
<div aria-live="polite">
|
|
1327
|
+
<Typography variant="body-medium">Submitted form data</Typography>
|
|
1328
|
+
<pre className="n-mt-token-2">
|
|
1329
|
+
{submittedValues.length > 0
|
|
1330
|
+
? submittedValues.join('\n')
|
|
1331
|
+
: 'Submit the form to inspect its values.'}
|
|
1332
|
+
</pre>
|
|
1333
|
+
</div>
|
|
1334
|
+
</form>
|
|
1335
|
+
);
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
export default Component;
|
|
1339
|
+
```
|
|
1340
|
+
|
|
1341
|
+
### Select Only
|
|
1342
|
+
|
|
1343
|
+
```tsx
|
|
1344
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
1345
|
+
|
|
1346
|
+
import { Combobox } from '@neo4j-ndl/react';
|
|
1347
|
+
|
|
1348
|
+
const Component = () => {
|
|
1349
|
+
const options = [
|
|
1350
|
+
{ label: 'Neo4j', value: 'neo4j' },
|
|
1351
|
+
{ label: 'Aura', value: 'aura' },
|
|
1352
|
+
{ label: 'Bloom', value: 'bloom' },
|
|
1353
|
+
{ isDisabled: true, label: 'Cypher', value: 'cypher' },
|
|
1354
|
+
];
|
|
1355
|
+
|
|
1356
|
+
// A list this short has nothing to filter, so the search input is turned
|
|
1357
|
+
// off and the combobox follows the select-only pattern.
|
|
1358
|
+
return <Combobox label="Products" isSearchable={false} options={options} />;
|
|
1359
|
+
};
|
|
1360
|
+
|
|
1361
|
+
export default Component;
|
|
1362
|
+
```
|
|
1363
|
+
|
|
1364
|
+
### Sizes
|
|
1365
|
+
|
|
1366
|
+
```tsx
|
|
1367
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
1368
|
+
|
|
1369
|
+
import { Combobox } from '@neo4j-ndl/react';
|
|
1370
|
+
|
|
1371
|
+
const options = [
|
|
1372
|
+
{ label: 'Neo4j', value: 'neo4j' },
|
|
1373
|
+
{ label: 'Aura', value: 'aura' },
|
|
1374
|
+
{ label: 'Bloom', value: 'bloom' },
|
|
1375
|
+
];
|
|
1376
|
+
|
|
1377
|
+
const Component = () => (
|
|
1378
|
+
<div className="n-flex n-flex-col n-gap-token-16">
|
|
1379
|
+
{(['small', 'medium', 'large'] as const).map((size) => (
|
|
1380
|
+
<div className="n-flex n-flex-col n-gap-token-4" key={size}>
|
|
1381
|
+
<p className="n-capitalize">{size}</p>
|
|
1382
|
+
<div className="n-flex n-flex-wrap n-items-start n-gap-token-16">
|
|
1383
|
+
<Combobox
|
|
1384
|
+
label={`${size} single-select`}
|
|
1385
|
+
defaultValue={options[0]}
|
|
1386
|
+
options={options}
|
|
1387
|
+
size={size}
|
|
1388
|
+
/>
|
|
1389
|
+
<Combobox
|
|
1390
|
+
label={`${size} multi-select`}
|
|
1391
|
+
defaultValue={options.slice(0, 2)}
|
|
1392
|
+
isMulti
|
|
1393
|
+
options={options}
|
|
1394
|
+
size={size}
|
|
1395
|
+
/>
|
|
1396
|
+
</div>
|
|
1397
|
+
</div>
|
|
1398
|
+
))}
|
|
1399
|
+
</div>
|
|
1400
|
+
);
|
|
1401
|
+
|
|
1402
|
+
export default Component;
|
|
1403
|
+
```
|
|
1404
|
+
|
|
1405
|
+
### Unlimited Rows
|
|
1406
|
+
|
|
1407
|
+
```tsx
|
|
1408
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
1409
|
+
|
|
1410
|
+
import { Combobox } from '@neo4j-ndl/react';
|
|
1411
|
+
import { useState } from 'react';
|
|
1412
|
+
|
|
1413
|
+
type Option = {
|
|
1414
|
+
label: string;
|
|
1415
|
+
value: string;
|
|
1416
|
+
};
|
|
1417
|
+
|
|
1418
|
+
const Component = () => {
|
|
1419
|
+
const options: Option[] = [
|
|
1420
|
+
{ label: 'Neo4j', value: 'neo4j' },
|
|
1421
|
+
{ label: 'Aura', value: 'aura' },
|
|
1422
|
+
{ label: 'Bloom', value: 'bloom' },
|
|
1423
|
+
{ label: 'Data Importer', value: 'data-importer' },
|
|
1424
|
+
{ label: 'GraphQL Library', value: 'graphql-library' },
|
|
1425
|
+
{ label: 'Cypher', value: 'cypher' },
|
|
1426
|
+
{ label: 'Operations Manager', value: 'operations-manager' },
|
|
1427
|
+
{ label: 'Graph Data Science', value: 'graph-data-science' },
|
|
1428
|
+
];
|
|
1429
|
+
const [value, setValue] = useState(options);
|
|
1430
|
+
|
|
1431
|
+
return (
|
|
1432
|
+
<Combobox
|
|
1433
|
+
label="Products"
|
|
1434
|
+
isClearable
|
|
1435
|
+
isMulti
|
|
1436
|
+
maxRows={null}
|
|
1437
|
+
options={options}
|
|
1438
|
+
value={value}
|
|
1439
|
+
onChange={(nextValue) => setValue(nextValue)}
|
|
1440
|
+
helpText="Set maxRows to null to let the control grow with the selection."
|
|
1441
|
+
/>
|
|
1442
|
+
);
|
|
1443
|
+
};
|
|
1444
|
+
|
|
1445
|
+
export default Component;
|
|
1446
|
+
```
|
|
1447
|
+
|
|
1448
|
+
### Validation
|
|
1449
|
+
|
|
1450
|
+
```tsx
|
|
1451
|
+
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
|
|
1452
|
+
|
|
1453
|
+
import { Combobox } from '@neo4j-ndl/react';
|
|
1454
|
+
|
|
1455
|
+
const options = [
|
|
1456
|
+
{ label: 'Neo4j', value: 'neo4j' },
|
|
1457
|
+
{ label: 'Aura', value: 'aura' },
|
|
1458
|
+
{ label: 'Bloom', value: 'bloom' },
|
|
1459
|
+
];
|
|
1460
|
+
|
|
1461
|
+
const Component = () => (
|
|
1462
|
+
<div className="n-flex n-flex-col n-gap-token-16">
|
|
1463
|
+
<Combobox
|
|
1464
|
+
label="Product"
|
|
1465
|
+
helpText="Pick the product the ticket is about."
|
|
1466
|
+
isRequired
|
|
1467
|
+
hasRequiredIndicator
|
|
1468
|
+
options={options}
|
|
1469
|
+
/>
|
|
1470
|
+
<Combobox
|
|
1471
|
+
label="Product"
|
|
1472
|
+
errorText="Select a product to continue."
|
|
1473
|
+
isRequired
|
|
1474
|
+
hasRequiredIndicator
|
|
1475
|
+
options={options}
|
|
1476
|
+
/>
|
|
1477
|
+
<Combobox
|
|
1478
|
+
label="Environment"
|
|
1479
|
+
helpText="Leave empty to include every environment."
|
|
1480
|
+
hasRequiredIndicator
|
|
1481
|
+
options={options}
|
|
1482
|
+
/>
|
|
1483
|
+
</div>
|
|
1484
|
+
);
|
|
1485
|
+
|
|
1486
|
+
export default Component;
|
|
1487
|
+
```
|