@bitforgehq/ui 0.3.0 → 0.3.2
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/CHANGELOG.md +598 -0
- package/README.md +18 -11
- package/ai/MANIFEST.index.json +306 -0
- package/ai/MANIFEST.json +8915 -3788
- package/ai/REFERENCE.index.json +476 -0
- package/ai/REFERENCE.json +5810 -2905
- package/ai/UI_GUIDE.md +212 -58
- package/ai/components/app-frame.md +328 -0
- package/ai/components/back-button.md +42 -2
- package/ai/components/badge.md +17 -7
- package/ai/components/bar-chart.md +188 -0
- package/ai/components/breadcrumb-item.md +121 -0
- package/ai/components/breadcrumb.md +277 -0
- package/ai/components/button.md +82 -15
- package/ai/components/calendar.md +300 -0
- package/ai/components/card.md +119 -4
- package/ai/components/chart-card.md +283 -0
- package/ai/components/combobox.md +411 -0
- package/ai/components/date-input.md +396 -0
- package/ai/components/datetime-input.md +215 -0
- package/ai/components/description-item.md +111 -0
- package/ai/components/description-list.md +160 -0
- package/ai/components/donut-chart.md +169 -0
- package/ai/components/empty-state.md +99 -6
- package/ai/components/header.md +71 -2
- package/ai/components/icon-button.md +74 -16
- package/ai/components/icon.md +63 -7
- package/ai/components/line-chart.md +234 -0
- package/ai/components/list.md +70 -6
- package/ai/components/menu.md +5 -0
- package/ai/components/multi-combobox.md +257 -0
- package/ai/components/nav-group.md +178 -0
- package/ai/components/nav-item.md +210 -0
- package/ai/components/page-header.md +237 -0
- package/ai/components/pagination.md +173 -0
- package/ai/components/popover.md +250 -0
- package/ai/components/side-panel.md +391 -0
- package/ai/components/sidebar-toggle.md +287 -0
- package/ai/components/sidebar.md +541 -0
- package/ai/components/skeleton.md +215 -0
- package/ai/components/sparkline.md +188 -0
- package/ai/components/stat.md +297 -0
- package/ai/components/tab-group.md +226 -0
- package/ai/components/tab-panel.md +163 -0
- package/ai/components/table.md +697 -0
- package/ai/components/tabs.md +46 -2
- package/ai/components/theme-menu-button.md +182 -0
- package/ai/components/theme-switch.md +170 -0
- package/ai/components/time-input.md +240 -0
- package/ai/components/toolbar.md +354 -0
- package/ai/components/tooltip.md +230 -0
- package/ai/components/top-nav.md +159 -0
- package/ai/reference/bf-app-frame-breakpoint.md +27 -0
- package/ai/reference/bf-app-frame-content-width.md +29 -0
- package/ai/reference/bf-badge-appearance.md +36 -0
- package/ai/reference/bf-badge-variant.md +1 -1
- package/ai/reference/bf-bar-chart-orientation.md +27 -0
- package/ai/reference/bf-button-appearance.md +36 -0
- package/ai/reference/bf-button-variant.md +1 -1
- package/ai/reference/bf-calendar-disabled-date.md +23 -0
- package/ai/reference/bf-calendar-harness.md +51 -0
- package/ai/reference/bf-calendar-weekday.md +31 -0
- package/ai/reference/bf-chart-card-heading-level.md +26 -0
- package/ai/reference/bf-chart-curve.md +28 -0
- package/ai/reference/bf-chart-harness.md +51 -0
- package/ai/reference/bf-chart-legend-position.md +30 -0
- package/ai/reference/bf-chart-series.md +23 -0
- package/ai/reference/bf-chart-value-format.md +23 -0
- package/ai/reference/bf-color-scheme-service.md +30 -0
- package/ai/reference/bf-color-scheme.md +31 -0
- package/ai/reference/bf-combobox-filter.md +27 -0
- package/ai/reference/bf-combobox-harness.md +89 -0
- package/ai/reference/bf-combobox-option.md +33 -0
- package/ai/reference/bf-compare-with.md +2 -0
- package/ai/reference/bf-date-input-harness.md +67 -0
- package/ai/reference/bf-date-input-picker.md +27 -0
- package/ai/reference/bf-date-time-input-harness.md +76 -0
- package/ai/reference/bf-date-time-input-picker.md +27 -0
- package/ai/reference/bf-description-list-layout.md +26 -0
- package/ai/reference/bf-donut-slice.md +22 -0
- package/ai/reference/bf-empty-state-heading-level.md +1 -1
- package/ai/reference/bf-empty-state-heading.md +33 -0
- package/ai/reference/bf-labels.md +73 -20
- package/ai/reference/bf-menu-button-variant.md +1 -0
- package/ai/reference/bf-menu-placement.md +1 -0
- package/ai/reference/bf-overlay-options.md +28 -27
- package/ai/reference/bf-overlay-placement.md +34 -0
- package/ai/reference/bf-overlay-position.md +1 -1
- package/ai/reference/bf-overlay-service.md +5 -3
- package/ai/reference/bf-page-header-heading-level.md +26 -0
- package/ai/reference/bf-pagination-harness.md +61 -0
- package/ai/reference/bf-platform-service.md +17 -15
- package/ai/reference/bf-popover-harness.md +42 -0
- package/ai/reference/bf-popover-placement.md +28 -0
- package/ai/reference/bf-popover-trigger-directive.md +56 -0
- package/ai/reference/bf-side-panel-config.md +35 -0
- package/ai/reference/bf-side-panel-harness.md +34 -0
- package/ai/reference/bf-side-panel-ref.md +44 -0
- package/ai/reference/bf-side-panel-service.md +43 -0
- package/ai/reference/bf-side-panel-template-context.md +32 -0
- package/ai/reference/bf-side-panel-width.md +28 -0
- package/ai/reference/bf-sidebar-state-service.md +21 -0
- package/ai/reference/bf-sidebar-toggle-harness.md +32 -0
- package/ai/reference/bf-size.md +13 -0
- package/ai/reference/bf-skeleton-shape.md +28 -0
- package/ai/reference/bf-stat-appearance.md +33 -0
- package/ai/reference/bf-stat-trend-intent.md +27 -0
- package/ai/reference/bf-stat-trend.md +27 -0
- package/ai/reference/bf-stat-variant.md +26 -0
- package/ai/reference/bf-tab-group-harness.md +44 -0
- package/ai/reference/bf-table-align.md +26 -0
- package/ai/reference/bf-table-cell-context.md +24 -0
- package/ai/reference/bf-table-cell-directive.md +22 -0
- package/ai/reference/bf-table-column.md +28 -0
- package/ai/reference/bf-table-harness.md +77 -0
- package/ai/reference/bf-table-row-key.md +21 -0
- package/ai/reference/bf-table-sort-direction.md +26 -0
- package/ai/reference/bf-table-sort.md +27 -0
- package/ai/reference/bf-table-stack.md +29 -0
- package/ai/reference/bf-theme.md +5 -4
- package/ai/reference/bf-time-input-harness.md +67 -0
- package/ai/reference/bf-time-input-hour-cycle.md +27 -0
- package/ai/reference/bf-time-input-picker.md +27 -0
- package/ai/reference/bf-tooltip-directive.md +43 -0
- package/ai/reference/bf-tooltip-harness.md +43 -0
- package/ai/reference/bf-tooltip-placement.md +28 -0
- package/ai/reference/bf_color_scheme_storage.md +20 -0
- package/ai/reference/bf_side_panel_data.md +17 -0
- package/ai/reference/bf_sidebar_state_storage.md +20 -0
- package/ai/reference/bf_theme_storage.md +3 -1
- package/ai/reference/provide-bf-color-scheme.md +20 -0
- package/ai/reference/provide-bf-labels.md +13 -4
- package/ai/reference/provide-bf-sidebar-state.md +20 -0
- package/ai/reference/slide-in-from-end.md +24 -0
- package/ai/reference/slide-out-to-end.md +24 -0
- package/bin/bf-ui.js +433 -3
- package/fesm2022/bitforgehq-ui-charts.mjs +3045 -0
- package/fesm2022/bitforgehq-ui-charts.mjs.map +1 -0
- package/fesm2022/bitforgehq-ui-icons.mjs +107 -3
- package/fesm2022/bitforgehq-ui-icons.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui-mobile.mjs +14 -59
- package/fesm2022/bitforgehq-ui-mobile.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui-overlay.mjs +6566 -38
- package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui-testing.mjs +634 -1
- package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui.mjs +5708 -137
- package/fesm2022/bitforgehq-ui.mjs.map +1 -1
- package/llms-full.txt +26766 -14470
- package/llms.txt +106 -1
- package/package.json +7 -3
- package/theme.css +101 -0
- package/types/bitforgehq-ui-charts.d.ts +967 -0
- package/types/bitforgehq-ui-icons.d.ts +13 -1
- package/types/bitforgehq-ui-mobile.d.ts +25 -18
- package/types/bitforgehq-ui-overlay.d.ts +1895 -7
- package/types/bitforgehq-ui-testing.d.ts +782 -1
- package/types/bitforgehq-ui.d.ts +2393 -45
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# bf-combobox
|
|
2
|
+
|
|
3
|
+
Text field that filters a list of options as you type and picks one
|
|
4
|
+
|
|
5
|
+
`BfComboboxComponent` from `@bitforgehq/ui/overlay` · Forms · since 0.3.1
|
|
6
|
+
|
|
7
|
+
## Example
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Component, computed, signal } from '@angular/core';
|
|
11
|
+
import { BfComboboxComponent, type BfComboboxOption } from '@bitforgehq/ui/overlay';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The bf-combobox overview.
|
|
15
|
+
*
|
|
16
|
+
* Live demo of `bf-combobox`: assign a task to a person. Typing filters the people by name
|
|
17
|
+
* (ignoring case and accents, so "zoe" finds Zoë), the arrow keys move through the list and
|
|
18
|
+
* `Enter` or a click picks one; each option shows the person's email as a second line, and one
|
|
19
|
+
* person is away (disabled). The result line reports the assignee.
|
|
20
|
+
*/
|
|
21
|
+
@Component({
|
|
22
|
+
selector: 'app-example-combobox-overview',
|
|
23
|
+
imports: [BfComboboxComponent],
|
|
24
|
+
template: `
|
|
25
|
+
<div class="flex max-w-sm flex-col gap-3">
|
|
26
|
+
<bf-combobox
|
|
27
|
+
label="Assignee"
|
|
28
|
+
placeholder="Search people"
|
|
29
|
+
helpText="Type a name, then pick with Enter or a click."
|
|
30
|
+
[options]="people"
|
|
31
|
+
[(value)]="assignee"
|
|
32
|
+
/>
|
|
33
|
+
<p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
|
|
34
|
+
</div>
|
|
35
|
+
`,
|
|
36
|
+
})
|
|
37
|
+
export class ComboboxOverviewExampleComponent {
|
|
38
|
+
protected readonly people: readonly BfComboboxOption<number>[] = [
|
|
39
|
+
{ value: 1, label: 'Ada Lovelace', description: 'ada@example.com' },
|
|
40
|
+
{ value: 2, label: 'Grace Hopper', description: 'grace@example.com' },
|
|
41
|
+
{ value: 3, label: 'Alan Turing', description: 'Away until Monday', disabled: true },
|
|
42
|
+
{ value: 4, label: 'Katherine Johnson', description: 'katherine@example.com' },
|
|
43
|
+
{ value: 5, label: 'Zoë Martin', description: 'zoe@example.com' },
|
|
44
|
+
];
|
|
45
|
+
/** The person's id, `null` while nobody is assigned. */
|
|
46
|
+
readonly assignee = signal<number | null>(null);
|
|
47
|
+
|
|
48
|
+
readonly status = computed(() => {
|
|
49
|
+
const person = this.people.find((p) => p.value === this.assignee());
|
|
50
|
+
return person ? `Assigned to ${person.label}.` : 'Nobody assigned.';
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Overview
|
|
56
|
+
|
|
57
|
+
Text field with a filtered list of options to pick one value from (the ARIA combobox pattern).
|
|
58
|
+
Typing filters the list by label, ignoring case and accents; the arrow keys move through it
|
|
59
|
+
and `Enter` picks. Generic over the option value (a string by default); `null` means nothing is
|
|
60
|
+
picked. For several values use `bf-multi-combobox`.
|
|
61
|
+
|
|
62
|
+
A text field with a list of options that filters as you type, to pick one value from more choices
|
|
63
|
+
than a dropdown can show comfortably: a person, a customer, a city. It follows the
|
|
64
|
+
[ARIA 1.2 combobox](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) pattern with a list
|
|
65
|
+
autocomplete: focus stays in the field while the arrow keys move through the options. It is a
|
|
66
|
+
Signal Forms value control like `bf-select`, with the same label, help and error handling and the
|
|
67
|
+
same field look, and generic over the option value.
|
|
68
|
+
|
|
69
|
+
Import `BfComboboxComponent` and `BfComboboxOption` from `@bitforgehq/ui/overlay` (needs
|
|
70
|
+
`@angular/cdk`): the list opens in the CDK overlay container, so it is never clipped by a scrolling
|
|
71
|
+
parent and works inside a `BfDialogService` dialog.
|
|
72
|
+
|
|
73
|
+
## When to use
|
|
74
|
+
|
|
75
|
+
Use `bf-combobox` to pick one value from a list long enough to search, or whose options come from
|
|
76
|
+
a server: an assignee, a customer, a time zone. Use [bf-select](https://bitforge-ui.web.app/components/select) instead for a
|
|
77
|
+
short list the user can scan (it opens the platform's own picker on phones),
|
|
78
|
+
[bf-radio-group](https://bitforge-ui.web.app/components/radio-group) for up to five or six options read side by side, and
|
|
79
|
+
[bf-multi-combobox](https://bitforge-ui.web.app/components/multi-combobox) to pick several values as tags. Both work in both
|
|
80
|
+
shells; in a Capacitor app set `provideBfDefaults({ fieldSize: 'lg' })` for 48 px fields.
|
|
81
|
+
|
|
82
|
+
## Typing and picking
|
|
83
|
+
|
|
84
|
+
Typing filters the options by label, ignoring case and accents (`zoe` finds "Zoë"), opens the list
|
|
85
|
+
and makes the first match active. The arrow keys open the list and move through it (wrapping at
|
|
86
|
+
the ends, skipping disabled options), `Home` and `End` jump to its ends, `Enter` or a click picks
|
|
87
|
+
the active option: the value changes, `(changed)` emits it, the field shows its label and the list
|
|
88
|
+
closes. `Escape` closes the list; a second `Escape` drops the typed text. `Tab` picks nothing.
|
|
89
|
+
|
|
90
|
+
When the field loses focus, text that names an option exactly (ignoring case and accents) picks it,
|
|
91
|
+
emptied text clears the value (`(changed)` emits `null`), and any other text reverts to the picked
|
|
92
|
+
option's label, so the field never shows text that is not its value.
|
|
93
|
+
|
|
94
|
+
Options take a `description` (a second, muted line, such as an email) and `disabled` (listed but
|
|
95
|
+
not pickable). `value` is `null` while nothing is picked; for object values pass `compareWith`, as
|
|
96
|
+
on `bf-select`.
|
|
97
|
+
|
|
98
|
+
## Groups
|
|
99
|
+
|
|
100
|
+
Give options a `group` to list them under that heading, in the order the groups first appear;
|
|
101
|
+
options without a group come first. Filtering keeps the headings of the groups that still match.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { Component, signal } from '@angular/core';
|
|
105
|
+
import { BfComboboxComponent, type BfComboboxOption } from '@bitforgehq/ui/overlay';
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Options under group headings.
|
|
109
|
+
*
|
|
110
|
+
* Each option's `group` lists it under that heading (a named `role="group"`), in the order the
|
|
111
|
+
* groups first appear; filtering keeps the headings of the groups that still have matches.
|
|
112
|
+
*/
|
|
113
|
+
@Component({
|
|
114
|
+
selector: 'app-example-combobox-groups',
|
|
115
|
+
imports: [BfComboboxComponent],
|
|
116
|
+
template: `
|
|
117
|
+
<bf-combobox
|
|
118
|
+
class="max-w-sm"
|
|
119
|
+
label="Time zone"
|
|
120
|
+
placeholder="Search cities"
|
|
121
|
+
[options]="zones"
|
|
122
|
+
[(value)]="zone"
|
|
123
|
+
/>
|
|
124
|
+
`,
|
|
125
|
+
})
|
|
126
|
+
export class ComboboxGroupsExampleComponent {
|
|
127
|
+
protected readonly zones: readonly BfComboboxOption[] = [
|
|
128
|
+
{ value: 'Europe/Zurich', label: 'Zürich', group: 'Europe' },
|
|
129
|
+
{ value: 'Europe/Lisbon', label: 'Lisbon', group: 'Europe' },
|
|
130
|
+
{ value: 'Europe/Helsinki', label: 'Helsinki', group: 'Europe' },
|
|
131
|
+
{ value: 'America/New_York', label: 'New York', group: 'Americas' },
|
|
132
|
+
{ value: 'America/Sao_Paulo', label: 'São Paulo', group: 'Americas' },
|
|
133
|
+
{ value: 'Asia/Tokyo', label: 'Tokyo', group: 'Asia' },
|
|
134
|
+
{ value: 'Asia/Singapore', label: 'Singapore', group: 'Asia' },
|
|
135
|
+
];
|
|
136
|
+
readonly zone = signal<string | null>('Europe/Zurich');
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Filtering and server search
|
|
141
|
+
|
|
142
|
+
`filter` replaces the match function: `(option, query) => boolean`, called for a query that is not
|
|
143
|
+
empty. For a server search, listen to `(queried)`, which emits the typed text on each change (`''`
|
|
144
|
+
once the text is cleared), set `loading` while the request runs (the list shows a spinner row) and
|
|
145
|
+
pass the results as `options` with `[filter]="null"`, so they are shown as the server sent them.
|
|
146
|
+
Debounce or cancel requests in your search. The field keeps the label of the picked option when
|
|
147
|
+
later results no longer hold it. With nothing to show, the list says "No results" (`emptyLabel`,
|
|
148
|
+
else the `comboboxEmpty` label).
|
|
149
|
+
|
|
150
|
+
The list is not virtualized: keep it to a few hundred options, and search on the server beyond that.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { Component, DestroyRef, computed, inject, signal } from '@angular/core';
|
|
154
|
+
import { BfComboboxComponent, type BfComboboxOption } from '@bitforgehq/ui/overlay';
|
|
155
|
+
|
|
156
|
+
interface Customer {
|
|
157
|
+
id: string;
|
|
158
|
+
name: string;
|
|
159
|
+
city: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Stands in for the server's customer table. */
|
|
163
|
+
const CUSTOMERS: readonly Customer[] = [
|
|
164
|
+
{ id: 'c1', name: 'Acme Corporation', city: 'Basel' },
|
|
165
|
+
{ id: 'c2', name: 'Globex', city: 'Bern' },
|
|
166
|
+
{ id: 'c3', name: 'Initech', city: 'Geneva' },
|
|
167
|
+
{ id: 'c4', name: 'Umbrella', city: 'Lausanne' },
|
|
168
|
+
{ id: 'c5', name: 'Hooli', city: 'Zürich' },
|
|
169
|
+
{ id: 'c6', name: 'Stark Industries', city: 'Lugano' },
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Server search with a loading row.
|
|
174
|
+
*
|
|
175
|
+
* `(queried)` hands each typed text to a search (here a fake one that answers after 400 ms and
|
|
176
|
+
* matches the name or the city); `loading` shows a spinner row while it runs, and `[filter]="null"`
|
|
177
|
+
* shows the results as the server sent them, without filtering them again by label.
|
|
178
|
+
*/
|
|
179
|
+
@Component({
|
|
180
|
+
selector: 'app-example-combobox-server',
|
|
181
|
+
imports: [BfComboboxComponent],
|
|
182
|
+
template: `
|
|
183
|
+
<div class="flex max-w-sm flex-col gap-3">
|
|
184
|
+
<bf-combobox
|
|
185
|
+
label="Customer"
|
|
186
|
+
placeholder="Search by name or city"
|
|
187
|
+
[options]="options()"
|
|
188
|
+
[filter]="null"
|
|
189
|
+
[loading]="loading()"
|
|
190
|
+
[(value)]="customer"
|
|
191
|
+
(queried)="search($event)"
|
|
192
|
+
/>
|
|
193
|
+
<p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
|
|
194
|
+
</div>
|
|
195
|
+
`,
|
|
196
|
+
})
|
|
197
|
+
export class ComboboxServerExampleComponent {
|
|
198
|
+
private timer: ReturnType<typeof setTimeout> | undefined;
|
|
199
|
+
private readonly results = signal<readonly Customer[]>(CUSTOMERS);
|
|
200
|
+
readonly loading = signal(false);
|
|
201
|
+
readonly customer = signal<string | null>(null);
|
|
202
|
+
|
|
203
|
+
protected readonly options = computed<BfComboboxOption[]>(() =>
|
|
204
|
+
this.results().map((c) => ({ value: c.id, label: c.name, description: c.city })),
|
|
205
|
+
);
|
|
206
|
+
readonly status = computed(() => {
|
|
207
|
+
const picked = CUSTOMERS.find((c) => c.id === this.customer());
|
|
208
|
+
return picked ? `Customer: ${picked.name} (${picked.city}).` : 'No customer picked.';
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
constructor() {
|
|
212
|
+
inject(DestroyRef).onDestroy(() => clearTimeout(this.timer));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Starts a search for `query`, cancelling the one still running. */
|
|
216
|
+
search(query: string): void {
|
|
217
|
+
clearTimeout(this.timer);
|
|
218
|
+
this.loading.set(true);
|
|
219
|
+
this.timer = setTimeout(() => {
|
|
220
|
+
const q = query.trim().toLowerCase();
|
|
221
|
+
this.results.set(CUSTOMERS.filter((c) => `${c.name} ${c.city}`.toLowerCase().includes(q)));
|
|
222
|
+
this.loading.set(false);
|
|
223
|
+
}, 400);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## With Signal Forms
|
|
229
|
+
|
|
230
|
+
Bind a field with `[formField]`, modelled as `T | null` and starting at `null`; `required()` treats
|
|
231
|
+
`null` as empty, and the error shows under the field once it is touched (or on submit), as on every
|
|
232
|
+
library field. `readonly` shows the value but never opens the list.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
import { Component, signal } from '@angular/core';
|
|
236
|
+
import { FormField, form, required, submit } from '@angular/forms/signals';
|
|
237
|
+
import { BfButtonComponent } from '@bitforgehq/ui';
|
|
238
|
+
import { BfComboboxComponent, type BfComboboxOption } from '@bitforgehq/ui/overlay';
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* A required combobox in a Signal Forms form.
|
|
242
|
+
*
|
|
243
|
+
* `[formField]` binds the field (modelled as `T | null`, starting at `null`); `required()` treats
|
|
244
|
+
* `null` as empty, so submitting without a project shows the error under the field.
|
|
245
|
+
*/
|
|
246
|
+
@Component({
|
|
247
|
+
selector: 'app-example-combobox-forms',
|
|
248
|
+
imports: [BfButtonComponent, BfComboboxComponent, FormField],
|
|
249
|
+
template: `
|
|
250
|
+
<form class="flex max-w-sm flex-col items-start gap-3" (submit)="onSubmit($event)">
|
|
251
|
+
<bf-combobox
|
|
252
|
+
class="self-stretch"
|
|
253
|
+
label="Project"
|
|
254
|
+
placeholder="Search projects"
|
|
255
|
+
[options]="projects"
|
|
256
|
+
[formField]="task.project"
|
|
257
|
+
/>
|
|
258
|
+
<bf-button type="submit">Create task</bf-button>
|
|
259
|
+
<p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
|
|
260
|
+
</form>
|
|
261
|
+
`,
|
|
262
|
+
})
|
|
263
|
+
export class ComboboxFormsExampleComponent {
|
|
264
|
+
protected readonly projects: readonly BfComboboxOption[] = [
|
|
265
|
+
{ value: 'atlas', label: 'Atlas relaunch' },
|
|
266
|
+
{ value: 'borealis', label: 'Borealis' },
|
|
267
|
+
{ value: 'cedar', label: 'Cedar migration' },
|
|
268
|
+
];
|
|
269
|
+
private readonly model = signal<{ project: string | null }>({ project: null });
|
|
270
|
+
protected readonly task = form(this.model, (path) => {
|
|
271
|
+
required(path.project);
|
|
272
|
+
});
|
|
273
|
+
readonly status = signal('Pick a project, then create the task.');
|
|
274
|
+
|
|
275
|
+
async onSubmit(event: Event): Promise<void> {
|
|
276
|
+
event.preventDefault();
|
|
277
|
+
await submit(this.task, () => {
|
|
278
|
+
this.status.set(`Task created in ${this.model().project}.`);
|
|
279
|
+
return Promise.resolve(undefined);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Sizes and labels
|
|
286
|
+
|
|
287
|
+
`size` follows the field steps (`sm` 32, `md` 40, `lg` 48 px; unset uses `provideBfDefaults()`'s
|
|
288
|
+
`fieldSize`). Without a visible `label`, give it `ariaLabel` or `ariaLabelledby`, as on
|
|
289
|
+
`bf-select`.
|
|
290
|
+
|
|
291
|
+
## Desktop
|
|
292
|
+
|
|
293
|
+
Built for a keyboard and a mouse: the field keeps focus while the arrow keys move through the list,
|
|
294
|
+
and the pointer moving over an option makes it the active one. A click into the field opens the
|
|
295
|
+
list; a click outside closes it. Options are 32, 36 or 44 px tall with the field's size. Under a
|
|
296
|
+
coarse pointer every option row is at least 44 px tall; the field itself gets no touch extension,
|
|
297
|
+
so touch apps use `fieldSize: 'lg'`.
|
|
298
|
+
|
|
299
|
+
## Theming
|
|
300
|
+
|
|
301
|
+
- Field: the `bf-input` / `bf-select` tokens: `border-strong` border (`danger-border` on error),
|
|
302
|
+
`primary` border and `ring` ring on focus, `surface-muted` while read-only or disabled, a
|
|
303
|
+
`text-subtle` chevron.
|
|
304
|
+
- List: `bg-surface`, a 1 px `border-border`, `rounded-card`, `shadow-floating`; the active option
|
|
305
|
+
`surface-muted`, a selected option `primary-text` with a check mark, a disabled one
|
|
306
|
+
`text-disabled`, descriptions and group headings `text-muted`.
|
|
307
|
+
- In forced colours the active option takes the system highlight.
|
|
308
|
+
|
|
309
|
+
## Accessibility
|
|
310
|
+
|
|
311
|
+
- The input is `role="combobox"` with `aria-autocomplete="list"`, `aria-expanded`, and while the
|
|
312
|
+
list shows options `aria-controls` (the listbox) and `aria-activedescendant` (the active option).
|
|
313
|
+
Focus never leaves the input.
|
|
314
|
+
- The list is a `listbox` named like the field, its options `role="option"` with `aria-selected`
|
|
315
|
+
and `aria-disabled`; grouped options sit in a `role="group"` named by its heading.
|
|
316
|
+
- The loading and "No results" rows are a `status` live region, so they are announced.
|
|
317
|
+
- `bf-field` renders the `<label for>`, the help and the error text; `aria-describedby`,
|
|
318
|
+
`aria-invalid` and `aria-errormessage` follow the same rules as on `bf-select`.
|
|
319
|
+
- `focus()` focuses the input; Signal Forms calls it from `focusBoundControl()`.
|
|
320
|
+
|
|
321
|
+
### Keyboard
|
|
322
|
+
|
|
323
|
+
| Key | Where | Action |
|
|
324
|
+
| ---------------------------------------------------- | --------- | ----------------------------------------------------------- |
|
|
325
|
+
| <kbd>ArrowDown</kbd> / <kbd>ArrowUp</kbd> | Field | Opens the list on the picked (else first / last) option |
|
|
326
|
+
| <kbd>ArrowDown</kbd> / <kbd>ArrowUp</kbd> | Open list | Next / previous enabled option, wrapping at the ends |
|
|
327
|
+
| <kbd>Home</kbd> / <kbd>End</kbd> | Open list | First / last enabled option |
|
|
328
|
+
| <kbd>Enter</kbd> | Open list | Picks the active option and closes the list |
|
|
329
|
+
| <kbd>Escape</kbd> | Open list | Closes the list |
|
|
330
|
+
| <kbd>Escape</kbd> | Field | Drops the typed text and shows the picked option's label |
|
|
331
|
+
| <kbd>Tab</kbd> | Field | Moves on without picking; the list closes |
|
|
332
|
+
| <kbd>A</kbd>–<kbd>Z</kbd>, <kbd>0</kbd>–<kbd>9</kbd> | Field | Filters the list, opens it and makes the first match active |
|
|
333
|
+
|
|
334
|
+
## Inputs
|
|
335
|
+
|
|
336
|
+
| Name | Type | Default | Description |
|
|
337
|
+
| ---------------- | ----------------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
338
|
+
| `options` | `readonly BfComboboxOption<T>[]` | required | The options to choose from; with server search, the latest results (keep the picked one). |
|
|
339
|
+
| `compareWith` | `BfCompareWith<T>` | `Object.is` | Equality for matching `value` to an option (default `Object.is`); pass one for object values. |
|
|
340
|
+
| `filter` | `BfComboboxFilter<T> \| null` | `labelContains` | Whether an option matches the typed text (default: its label contains it, ignoring case and accents); `null` shows `options` unfiltered, for results filtered by a server. |
|
|
341
|
+
| `size` | `BfSize \| undefined` (`'sm' \| 'md' \| 'lg' \| undefined`) | `undefined` | Height step: sm 32, md 40, lg 48 px; unset follows `provideBfDefaults()` (`fieldSize`, `md` by default). |
|
|
342
|
+
| `label` | `string` | `''` | Visible label above the field. |
|
|
343
|
+
| `ariaLabel` | `string` | `''` | Accessible name when no visible label is rendered. Ignored when `label` is set. |
|
|
344
|
+
| `ariaLabelledby` | `string` | `''` | Id(s) of the element(s) that name the field when no visible label is rendered. Ignored when `label` is set. |
|
|
345
|
+
| `placeholder` | `string` | `''` | Hint text shown in the empty field. |
|
|
346
|
+
| `helpText` | `string` | `''` | Helper text under the field. |
|
|
347
|
+
| `errorMessage` | `string` | `''` | Explicit error text; takes precedence over Signal Forms errors. |
|
|
348
|
+
| `emptyLabel` | `string` | `''` | Text of the list's row when no option matches; empty uses the `comboboxEmpty` label. |
|
|
349
|
+
| `loading` | `boolean` | `false` | Shows a loading row in the list, while a server search for the typed text runs. |
|
|
350
|
+
| `disabled` | `boolean` | `false` | Disables the field. Set by `[formField]`; do not bind it yourself. |
|
|
351
|
+
| `readonly` | `boolean` | `false` | Shows the value but blocks changes; the field stays focusable and the list never opens. Set by `[formField]`; do not bind it yourself. |
|
|
352
|
+
| `required` | `boolean` | `false` | Marks the field required. Set by `[formField]`; do not bind it yourself. |
|
|
353
|
+
| `invalid` | `boolean` | `false` | Field validity from Signal Forms. Set by `[formField]`; do not bind it yourself. |
|
|
354
|
+
| `touched` | `boolean` | `false` | Whether the user has left the field. Set by `[formField]`; do not bind it yourself. |
|
|
355
|
+
| `errors` | `readonly ValidationError.WithOptionalFieldTree[]` | `[]` | Validation errors from Signal Forms. Set by `[formField]`; do not bind it yourself. |
|
|
356
|
+
| `name` | `string` | `''` | Native name attribute of the text input. Set by `[formField]`; do not bind it yourself. |
|
|
357
|
+
| `id` | `string` | `generated` | Input id; generated when not set. |
|
|
358
|
+
| `value` | `T \| null` | `null` | The picked option's value, `null` while nothing is picked; two-way bindable and bound by `[formField]`. Two-way: `[(value)]`. |
|
|
359
|
+
|
|
360
|
+
## Outputs
|
|
361
|
+
|
|
362
|
+
| Name | Payload | Description |
|
|
363
|
+
| --------- | ------------ | --------------------------------------------------------------------------------------------------------- |
|
|
364
|
+
| `changed` | `T \| null` | Emitted with the new value after the user picks an option, or `null` after clearing the text. |
|
|
365
|
+
| `queried` | `string` | Emitted with the typed text each time the user changes it (`''` once it is cleared), for a server search. |
|
|
366
|
+
| `focused` | `FocusEvent` | Emitted when the input gains focus. |
|
|
367
|
+
| `blurred` | `FocusEvent` | Emitted when the input loses focus. |
|
|
368
|
+
| `touch` | `void` | Emitted on blur so Signal Forms marks the field touched. Set by `[formField]`; do not bind it yourself. |
|
|
369
|
+
|
|
370
|
+
## Methods
|
|
371
|
+
|
|
372
|
+
### `focus(options?: FocusOptions): void`
|
|
373
|
+
|
|
374
|
+
Focuses the input; Signal Forms calls it from `focusBoundControl()`.
|
|
375
|
+
|
|
376
|
+
| Parameter | Type | Description |
|
|
377
|
+
| ---------- | -------------- | --------------------------------------------------------------------- |
|
|
378
|
+
| `options?` | `FocusOptions` | Passed on to the native `focus()`, such as `{ preventScroll: true }`. |
|
|
379
|
+
|
|
380
|
+
## Notes
|
|
381
|
+
|
|
382
|
+
### Do
|
|
383
|
+
|
|
384
|
+
- Give every option a short, distinct label; put details in `description`.
|
|
385
|
+
- Keep the picked option in `options` when you replace them with server results.
|
|
386
|
+
- Make the field required when it has no sensible empty state.
|
|
387
|
+
|
|
388
|
+
### Don't
|
|
389
|
+
|
|
390
|
+
- Don't use a combobox for a handful of options; use `bf-select` or `bf-radio-group`.
|
|
391
|
+
- Don't load thousands of options into it; search on the server.
|
|
392
|
+
- Don't use it for free text that may match no option; it reverts such text to the value.
|
|
393
|
+
|
|
394
|
+
## Migration notes
|
|
395
|
+
|
|
396
|
+
### 0.3.1
|
|
397
|
+
|
|
398
|
+
New `bf-combobox` (`BfComboboxComponent<T>`) in `@bitforgehq/ui/overlay`: a text field with a
|
|
399
|
+
list of options that filters as you type, to pick one value (the ARIA 1.2 combobox pattern with a
|
|
400
|
+
list autocomplete). `[(value)]` is `T | null`; `options` are `BfComboboxOption<T>` (`value`,
|
|
401
|
+
`label`, `description`, `disabled`, `group`); `compareWith`, `label`, `ariaLabel`,
|
|
402
|
+
`ariaLabelledby`, `placeholder`, `helpText`, `errorMessage`, `size` (`fieldSize`) and the Signal
|
|
403
|
+
Forms value contract like `bf-select`. Filters by label, ignoring case and accents; `filter`
|
|
404
|
+
(`BfComboboxFilter<T>`) replaces the match, `null` turns local filtering off for server results,
|
|
405
|
+
`(queried)` emits the typed text and `loading` shows a spinner row. Arrow keys open and move
|
|
406
|
+
(wrapping), `Home`/`End`, `Enter` picks, `Escape` closes then drops the typed text, `Tab` picks
|
|
407
|
+
nothing; leaving the field reverts unmatched text to the picked label (emptied text clears the
|
|
408
|
+
value, `(changed)` emits `null`). Grouped options render in named `role="group"` sections. New
|
|
409
|
+
label key `comboboxEmpty` ("No results", or `emptyLabel`) and a `BfComboboxHarness` in
|
|
410
|
+
`@bitforgehq/ui/testing` (`open()`, `type(query)`, `getOptionLabels()`, `selectOption(label)`,
|
|
411
|
+
`getValueText()`, `getChipLabels()`, `removeChip(label)`) for both comboboxes.
|