@openreachtech/furo-vue 1.6.0 → 2.0.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/README.ja.md +2 -0
- package/README.md +2 -0
- package/docs/design-tokens.md +419 -0
- package/docs/furo-accordion.md +109 -0
- package/docs/furo-alert-dialog.md +107 -0
- package/docs/furo-attachment.md +133 -0
- package/docs/furo-autocomplete-field.md +719 -0
- package/docs/furo-avatar-group.md +87 -0
- package/docs/furo-avatar.md +101 -0
- package/docs/furo-badge.md +126 -0
- package/docs/furo-breadcrumb.md +89 -0
- package/docs/furo-button.md +263 -0
- package/docs/furo-checkbox.md +384 -0
- package/docs/furo-collapsible.md +104 -0
- package/docs/furo-control-block.md +102 -0
- package/docs/furo-date-picker.md +270 -0
- package/docs/furo-date-time-picker.md +110 -0
- package/docs/furo-dialog.md +88 -0
- package/docs/furo-drawer.md +122 -0
- package/docs/furo-dropdown-menu.md +298 -0
- package/docs/furo-editable-field.md +391 -0
- package/docs/furo-editor.md +304 -0
- package/docs/furo-email-field.md +93 -0
- package/docs/furo-empty-state.md +97 -0
- package/docs/furo-error-state.md +97 -0
- package/docs/furo-file-field.md +163 -0
- package/docs/furo-icon.md +288 -0
- package/docs/furo-number-field.md +397 -0
- package/docs/furo-pagination.md +101 -0
- package/docs/furo-password-field.md +75 -0
- package/docs/furo-popover.md +114 -0
- package/docs/furo-radio.md +125 -0
- package/docs/furo-scroll-area.md +112 -0
- package/docs/furo-search-field.md +94 -0
- package/docs/furo-section-header.md +135 -0
- package/docs/furo-select.md +443 -0
- package/docs/furo-separator.md +87 -0
- package/docs/furo-sidebar.md +178 -0
- package/docs/furo-skeleton.md +92 -0
- package/docs/furo-splitter.md +103 -0
- package/docs/furo-stepper.md +127 -0
- package/docs/furo-switch.md +107 -0
- package/docs/furo-table.md +233 -0
- package/docs/furo-tabs.md +173 -0
- package/docs/furo-text-field.md +249 -0
- package/docs/furo-textarea.md +214 -0
- package/docs/furo-time-field.md +276 -0
- package/docs/furo-toast.md +217 -0
- package/docs/furo-toggle-group.md +123 -0
- package/docs/furo-toggle.md +104 -0
- package/docs/furo-tool-bar.md +88 -0
- package/docs/furo-tooltip.md +107 -0
- package/lib/components/atoms/FuroButton/FuroButtonContext.js +8 -8
- package/lib/components/atoms/FuroCheckbox/FuroCheckboxContext.js +1 -1
- package/lib/components/atoms/FuroSwitch/FuroSwitchContext.js +1 -1
- package/lib/components/atoms/FuroToggle/FuroToggleContext.js +1 -1
- package/lib/components/molecules/FuroAccordion/FuroAccordionContext.js +1 -1
- package/lib/components/molecules/FuroCollapsible/FuroCollapsibleContext.js +1 -1
- package/lib/components/molecules/FuroDropdownMenu/FuroDropdownMenuContext.js +1 -2
- package/lib/components/molecules/FuroPopover/FuroPopoverContext.js +1 -1
- package/lib/components/molecules/FuroSelect/FuroSelect.vue +8 -6
- package/lib/components/molecules/FuroSelect/FuroSelectContext.js +35 -0
- package/lib/components/molecules/FuroSelect/FuroSelectOptionText.vue +40 -0
- package/lib/components/molecules/FuroSelect/FuroSelectOptionTextContext.js +275 -0
- package/lib/components/molecules/FuroSplitter/FuroSplitterContext.js +1 -1
- package/lib/components/molecules/FuroToggleGroup/FuroToggleGroupContext.js +1 -1
- package/lib/components/molecules/FuroTooltip/FuroTooltipContext.js +1 -1
- package/lib/components/organisms/FuroTable/FuroTableContext.js +3 -3
- package/lib/components/organisms/FuroToast/FuroToastContext.js +6 -2
- package/lib/components/organisms/FuroToast/ToastQueue.js +4 -4
- package/nuxt/index.js +9 -9
- package/package.json +24 -22
- package/public/furo-vue/components.json +1 -1
- package/types/generated/components/atoms/FuroAvatar/FuroAvatar.vue.d.ts +6 -15
- package/types/generated/components/atoms/FuroBadge/FuroBadge.vue.d.ts +8 -43
- package/types/generated/components/atoms/FuroButton/FuroButton.vue.d.ts +10 -45
- package/types/generated/components/atoms/FuroButton/FuroButtonContext.d.ts +4 -4
- package/types/generated/components/atoms/FuroCheckbox/FuroCheckbox.vue.d.ts +8 -20
- package/types/generated/components/atoms/FuroNumberField/FuroNumberField.vue.d.ts +12 -12
- package/types/generated/components/atoms/FuroRadio/FuroRadio.vue.d.ts +8 -24
- package/types/generated/components/atoms/FuroSwitch/FuroSwitch.vue.d.ts +0 -13
- package/types/generated/components/atoms/FuroToggle/FuroToggle.vue.d.ts +8 -19
- package/types/generated/components/molecules/FuroAccordion/FuroAccordion.vue.d.ts +14 -30
- package/types/generated/components/molecules/FuroAttachment/FuroAttachment.vue.d.ts +8 -44
- package/types/generated/components/molecules/FuroAutocompleteField/FuroAutocompleteField.vue.d.ts +74 -105
- package/types/generated/components/molecules/FuroAvatarGroup/FuroAvatarGroup.vue.d.ts +9 -18
- package/types/generated/components/molecules/FuroBreadcrumb/FuroBreadcrumb.vue.d.ts +22 -48
- package/types/generated/components/molecules/FuroCollapsible/FuroCollapsible.vue.d.ts +12 -23
- package/types/generated/components/molecules/FuroControlBlock/FuroControlBlock.vue.d.ts +6 -4
- package/types/generated/components/molecules/FuroDatePicker/FuroDatePicker.vue.d.ts +14 -16
- package/types/generated/components/molecules/FuroDropdownMenu/FuroDropdownMenu.vue.d.ts +13 -13
- package/types/generated/components/molecules/FuroDropdownMenu/FuroDropdownMenuOptionRows.vue.d.ts +34 -47
- package/types/generated/components/molecules/FuroEditableField/FuroEditableField.vue.d.ts +93 -115
- package/types/generated/components/molecules/FuroEmptyState/FuroEmptyState.vue.d.ts +8 -7
- package/types/generated/components/molecules/FuroErrorState/FuroErrorState.vue.d.ts +8 -7
- package/types/generated/components/molecules/FuroPagination/FuroPagination.vue.d.ts +14 -15
- package/types/generated/components/molecules/FuroPopover/FuroPopover.vue.d.ts +8 -7
- package/types/generated/components/molecules/FuroScrollArea/FuroScrollArea.vue.d.ts +6 -4
- package/types/generated/components/molecules/FuroSearchField/FuroSearchField.vue.d.ts +8 -22
- package/types/generated/components/molecules/FuroSectionHeader/FuroSectionHeader.vue.d.ts +10 -25
- package/types/generated/components/molecules/FuroSelect/FuroSelect.vue.d.ts +97 -108
- package/types/generated/components/molecules/FuroSelect/FuroSelectContext.d.ts +14 -0
- package/types/generated/components/molecules/FuroSelect/FuroSelectOptionText.vue.d.ts +52 -0
- package/types/generated/components/molecules/FuroSelect/FuroSelectOptionTextContext.d.ts +141 -0
- package/types/generated/components/molecules/FuroSplitter/FuroSplitter.vue.d.ts +14 -29
- package/types/generated/components/molecules/FuroTimeField/FuroTimeField.vue.d.ts +8 -7
- package/types/generated/components/molecules/FuroToggleGroup/FuroToggleGroup.vue.d.ts +9 -24
- package/types/generated/components/molecules/FuroToolBar/FuroToolBar.vue.d.ts +6 -9
- package/types/generated/components/molecules/FuroTooltip/FuroTooltip.vue.d.ts +8 -7
- package/types/generated/components/organisms/FuroAlertDialog/FuroAlertDialog.vue.d.ts +17 -19
- package/types/generated/components/organisms/FuroDialog/FuroDialog.vue.d.ts +20 -24
- package/types/generated/components/organisms/FuroDrawer/FuroDrawer.vue.d.ts +20 -24
- package/types/generated/components/organisms/FuroEditor/FuroEditor.vue.d.ts +9 -42
- package/types/generated/components/organisms/FuroSidebar/FuroSidebar.vue.d.ts +23 -62
- package/types/generated/components/organisms/FuroStepper/FuroStepper.vue.d.ts +19 -19
- package/types/generated/components/organisms/FuroTable/FuroTable.vue.d.ts +54 -71
- package/types/generated/components/organisms/FuroTable/FuroTableContext.d.ts +2 -2
- package/types/generated/components/organisms/FuroTabs/FuroTabs.vue.d.ts +17 -17
- package/types/generated/components/organisms/FuroToast/FuroToast.vue.d.ts +25 -31
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
# `<FuroSelect>` Interface Specification
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`<FuroSelect>` is a select molecule built on a headless select primitive. It follows the same contract style as the field atoms:
|
|
6
|
+
|
|
7
|
+
- behavior via `parcel` (and a few top-level attribute props)
|
|
8
|
+
- fallthrough attributes merged onto the root where applicable
|
|
9
|
+
- Furo emits (`change-value`, `commit-value`, `update:value`)
|
|
10
|
+
- `v-model:value` support (synced through fallthrough `value` / `@update:value`)
|
|
11
|
+
|
|
12
|
+
For a select, the selection gesture is the commit point, so `change-value` and `commit-value` fire together with the same payload (honoring the Furo form-control event contract).
|
|
13
|
+
|
|
14
|
+
Read [`FuroTextField`](./furo-text-field.md) first for the shared `parcel` vs fallthrough mental model and event handler style.
|
|
15
|
+
|
|
16
|
+
**Source files:**
|
|
17
|
+
|
|
18
|
+
- `lib/components/molecules/FuroSelect/FuroSelect.vue`
|
|
19
|
+
- `lib/components/molecules/FuroSelect/FuroSelectContext.js`
|
|
20
|
+
- `lib/components/molecules/FuroSelect/SelectEmitPayload.js`
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Props
|
|
25
|
+
|
|
26
|
+
### Rule: `parcel` vs fallthrough vs top-level attributes
|
|
27
|
+
|
|
28
|
+
| Mechanism | When to use | Bound to |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| `:parcel` | Furo behavior (`value`, `options`, `invalid`, `loading`, `placeholder`, root props such as `multiple`, `defaultValue`, `disabled`) | The root (via `extractRootAttributes()`) |
|
|
31
|
+
| `triggerParcel` / `portalParcel` / `contentParcel` | Extra attributes for the trigger, portal, or content parts | Matching part |
|
|
32
|
+
| Fallthrough attributes | HTML-like and root attrs (`name`, `required`, `id`, `aria-*`, …) | Merged into the root together with `parcel` |
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<FuroSelect
|
|
36
|
+
v-model:value="context.territoryRef.value"
|
|
37
|
+
:parcel="{
|
|
38
|
+
placeholder: 'Select a territory',
|
|
39
|
+
options: context.territoryOptions,
|
|
40
|
+
invalid: context.statusReactive.isInvalidTerritory,
|
|
41
|
+
}"
|
|
42
|
+
name="territory"
|
|
43
|
+
:required="true"
|
|
44
|
+
/>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Top-level component props
|
|
48
|
+
|
|
49
|
+
| Prop | Type | Default | Description |
|
|
50
|
+
| --- | --- | --- | --- |
|
|
51
|
+
| `parcel` | root behavior object or `null` | `null` | Root behavior and Furo fields |
|
|
52
|
+
| `triggerParcel` | trigger attribute object or `null` | `null` | Extra trigger attributes |
|
|
53
|
+
| `portalParcel` | portal attribute object or `null` | `null` | Attributes of the portal the option list renders in, such as `to` or `disabled`. The list renders at `body` by default, so a transformed or clipping ancestor such as `FuroDialog` leaves it in place |
|
|
54
|
+
| `contentParcel` | content attribute object or `null` | `null` | Extra content attributes (`side-offset` defaults to `4` on the component) |
|
|
55
|
+
|
|
56
|
+
### `parcel` shape (Furo fields)
|
|
57
|
+
|
|
58
|
+
These keys live on `parcel` (not on a nested `triggerParcel` inside `parcel`):
|
|
59
|
+
|
|
60
|
+
| Key | Type | Required | Default | Description |
|
|
61
|
+
| --- | --- | --- | --- | --- |
|
|
62
|
+
| `value` | `string \| number \| object \| null` (an acceptable value) or array when `multiple` | No | `null` | Controlled value; objects are supported — see [Object values](#object-values) |
|
|
63
|
+
| `defaultValue` | same as `value` | No | — | Uncontrolled initial value (root `defaultValue`) |
|
|
64
|
+
| `options` | `Array<FuroSelectOption>` | No | `[]` | Unified list; see [Unified options](#unified-options) |
|
|
65
|
+
| `optionLabel` | `string \| ((option) => string)` | No | uses `option.label` | Key name or function that selects each option's display label; see [Custom option shape](#custom-option-shape) |
|
|
66
|
+
| `optionValue` | `string \| ((option) => value)` | No | uses `option.value` | Key name or function that selects each option's model value — what `update:value` emits and what the bound value matches against; see [Custom option shape](#custom-option-shape) |
|
|
67
|
+
| `invalid` | `boolean` | No | `false` | Invalid visual state + `aria-invalid` on trigger |
|
|
68
|
+
| `loading` | `boolean` | No | `false` | Loading state on trigger (spinner icon); suppresses empty row |
|
|
69
|
+
| `placeholder` | `string` | No | `''` | Passed to the value display |
|
|
70
|
+
| `textDirection` | `toRight` or `toLeft` | No | `toRight` | Maps to the root `dir` (`ltr` / `rtl`) |
|
|
71
|
+
| `multiple` | `boolean` | No | `false` | Multi-select (root); see [Multiple selection](#multiple-selection) |
|
|
72
|
+
| `disabled` | `boolean` | No | `false` | May be set on `parcel` or as fallthrough; forwarded to the root |
|
|
73
|
+
|
|
74
|
+
Remaining `parcel` keys are forwarded to the root (for example `name`, `required`, `defaultValue`, `multiple`). Furo-specific keys above are consumed by the context/template.
|
|
75
|
+
|
|
76
|
+
`parcel` values should be JSON-serializable.
|
|
77
|
+
|
|
78
|
+
#### Object values
|
|
79
|
+
|
|
80
|
+
An option `value` may be an object, not just a `string` / `number`. The value
|
|
81
|
+
flows through the root unchanged, and the context resolves the matching
|
|
82
|
+
`selectedOption` with deep (`JSON.stringify`) equality in `onChangeValue()`, so
|
|
83
|
+
an object `value` correctly resolves its option, index, and payload.
|
|
84
|
+
|
|
85
|
+
Caveat: `SelectEmitPayload.matchesOption({ value })` uses strict `===`, so it
|
|
86
|
+
matches object values **by reference only**. When binding objects, compare by a
|
|
87
|
+
stable id/key rather than relying on `matchesOption()` with an object literal.
|
|
88
|
+
|
|
89
|
+
#### Custom option shape
|
|
90
|
+
|
|
91
|
+
By default an option renders `option.label` and carries `option.value` as its
|
|
92
|
+
model value. When options do not follow that shape (for example `{ id, name }`),
|
|
93
|
+
map them with `optionLabel` / `optionValue`:
|
|
94
|
+
|
|
95
|
+
- `optionLabel` — a key name (`'name'`) or a function (`option => option.name`)
|
|
96
|
+
that selects the **display label** for each option. When unset, `option.label`
|
|
97
|
+
is used.
|
|
98
|
+
- `optionValue` — a key name (`'id'`) or a function that selects the **model
|
|
99
|
+
value** — what `update:value` emits and what the bound `value` matches against.
|
|
100
|
+
When unset, `option.value` (a primitive) is used.
|
|
101
|
+
|
|
102
|
+
Both keys are backward-compatible: with neither set, behavior is exactly as
|
|
103
|
+
before (`label` = `option.label`, `value` = `option.value`).
|
|
104
|
+
|
|
105
|
+
```html
|
|
106
|
+
<FuroSelect
|
|
107
|
+
:parcel="{
|
|
108
|
+
placeholder: 'Select a member',
|
|
109
|
+
optionLabel: 'name',
|
|
110
|
+
optionValue: 'id',
|
|
111
|
+
options: [
|
|
112
|
+
{ id: 50001, name: 'Ada Lovelace' },
|
|
113
|
+
{ id: 50002, name: 'Alan Turing' },
|
|
114
|
+
],
|
|
115
|
+
}"
|
|
116
|
+
@change-value="context.onChangeMember({ payload: $event })"
|
|
117
|
+
/>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`textDirection` maps to the root `dir`:
|
|
121
|
+
|
|
122
|
+
- `toRight` → `ltr`
|
|
123
|
+
- `toLeft` → `rtl`
|
|
124
|
+
|
|
125
|
+
#### Unified `options`
|
|
126
|
+
|
|
127
|
+
Applications pass a **single** `parcel.options` array. The component renders either a **group** (group label + child rows) or a **leaf** (option row) per top-level entry.
|
|
128
|
+
|
|
129
|
+
**Classification (as implemented):** An entry is rendered as a **group** when it has a non-empty `options` array (`shouldShowGroupOption()`). Otherwise it is rendered as a **leaf** row. A group entry **may** include a `value` field (used only for flattening / payload matching, not as a selectable row in the template).
|
|
130
|
+
|
|
131
|
+
Nested `options` inside a group contain leaf rows only (no nested groups in v1).
|
|
132
|
+
|
|
133
|
+
**`selectedIndex` / payload matching:** `FuroSelectContext.generateFlattenedOptions()` builds the list used to resolve `selectedOption` and indices. For grouped data it includes the **group header object** as the first entry in each group block, then each child leaf. Flat top-level leaves use `groupIndex: null` and `optionIndex` equal to their index in `parcel.options`.
|
|
134
|
+
|
|
135
|
+
#### `FuroSelectOption`
|
|
136
|
+
|
|
137
|
+
| Key | Type | Required | Default |
|
|
138
|
+
| --- | --- | --- | --- |
|
|
139
|
+
| `value` | `string \| number \| object` (an acceptable value) | Yes (leaf) | — |
|
|
140
|
+
| `label` | `string` | Yes | — |
|
|
141
|
+
| `disabled` | `boolean` | No | `false` |
|
|
142
|
+
| `separator` | `boolean` | No | `false` | Renders a separator after the row when `true` |
|
|
143
|
+
| `options` | `Array<FuroSelectOption>` | No | — | When non-empty, entry is treated as a group |
|
|
144
|
+
|
|
145
|
+
Additional properties are allowed (icons, captions, foreign keys) as long as the whole `parcel` stays JSON-serializable. Preserve full option objects for slots and `SelectEmitPayload.selectedOption`.
|
|
146
|
+
|
|
147
|
+
#### Group entries
|
|
148
|
+
|
|
149
|
+
Group rows are elements of `parcel.options` with `label` and non-empty `options`:
|
|
150
|
+
|
|
151
|
+
| Key | Type | Required |
|
|
152
|
+
| --- | --- | --- |
|
|
153
|
+
| `label` | `string` | Yes |
|
|
154
|
+
| `options` | `Array<FuroSelectOption>` | Yes (non-empty for group UI) |
|
|
155
|
+
| `disabled` | `boolean` | No |
|
|
156
|
+
| `value` | `string \| number` | No | Optional; included in flatten list |
|
|
157
|
+
|
|
158
|
+
#### Disabled precedence
|
|
159
|
+
|
|
160
|
+
Per-option **effective** disabled on each option row:
|
|
161
|
+
|
|
162
|
+
- **Leaf row:** `option.disabled` (root `disabled` is on the root)
|
|
163
|
+
- **Grouped child:** `groupChildOption.disabled || group.disabled`
|
|
164
|
+
|
|
165
|
+
Root / parcel `disabled` disables the whole control via the root.
|
|
166
|
+
|
|
167
|
+
### Fallthrough attributes
|
|
168
|
+
|
|
169
|
+
Consumed via `inheritAttrs: false` and merged in `extractRootAttributes()` (`...attrs`, `...parcel`, `dir`, `modelValue`).
|
|
170
|
+
|
|
171
|
+
Common attributes:
|
|
172
|
+
|
|
173
|
+
| Attribute | Behavior |
|
|
174
|
+
| --- | --- |
|
|
175
|
+
| `name` | Forwarded to the root |
|
|
176
|
+
| `disabled` | Forwarded to the root |
|
|
177
|
+
| `required` | Forwarded to the root |
|
|
178
|
+
| `id` | Forwarded to the root (not trigger-only) |
|
|
179
|
+
| `aria-label` / `aria-labelledby` / `aria-describedby` | Forwarded to the root |
|
|
180
|
+
| `value` | Watched with `parcel.value` for controlled sync (`v-model:value`) |
|
|
181
|
+
|
|
182
|
+
Prefer `parcel.placeholder` over a fallthrough `placeholder` (placeholder is read from `parcel` in `extractPlaceholder()`).
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Slots
|
|
187
|
+
|
|
188
|
+
| Slot | Scoped props | Default content |
|
|
189
|
+
| --- | --- | --- |
|
|
190
|
+
| `trigger-content` | — | Value display + trigger icon area |
|
|
191
|
+
| `trigger-text` | — | Inside the value display (optional extra trigger label) |
|
|
192
|
+
| `trigger-icon` | — | Caret icon when not loading |
|
|
193
|
+
| `trigger-loading-icon` | — | Spinner when `parcel.loading` |
|
|
194
|
+
| `content-before` | — | Before scroll up button |
|
|
195
|
+
| `content-after` | — | After scroll down button |
|
|
196
|
+
| `scroll-up-button` | — | Caret up icon |
|
|
197
|
+
| `scroll-down-button` | — | Caret down icon |
|
|
198
|
+
| `group` | `{ group, groupIndex }` | Group label + child options |
|
|
199
|
+
| `group-label` | `{ group, groupIndex }` | Group `label` text |
|
|
200
|
+
| `option` | `{ option, optionIndex, group?, groupIndex? }` | Option text + indicator |
|
|
201
|
+
| `option-text` | `{ option, optionIndex }` | Option `label` |
|
|
202
|
+
| `option-indicator` | `{ option, optionIndex }` | Check icon |
|
|
203
|
+
| `separator` | `{ option, optionIndex }` | Separator when `option.separator` |
|
|
204
|
+
| `empty` | — (no scoped props passed today) | Shown when `hasEmptyOptions()`; default text `No options` |
|
|
205
|
+
|
|
206
|
+
**Empty / loading:** `hasEmptyOptions()` is true when `options` is empty **and** `loading` is false. While `loading` is true, the trigger shows the loading icon and the empty row is not shown.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Emits
|
|
211
|
+
|
|
212
|
+
| Event | Payload | When |
|
|
213
|
+
| --- | --- | --- |
|
|
214
|
+
| `change-value` | `SelectEmitPayload` | On selection (root `@update:model-value`) |
|
|
215
|
+
| `commit-value` | `SelectEmitPayload` | Same gesture; selection is the commit point |
|
|
216
|
+
| `update:value` | `value \| Array<value> \| null` | Same gesture; a single value in single mode, the full array of selected values in multiple mode (values reflect `optionValue` when set) |
|
|
217
|
+
|
|
218
|
+
`@update:open` is not exposed.
|
|
219
|
+
|
|
220
|
+
### Parent handler style
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
onChangeValue ({
|
|
224
|
+
payload,
|
|
225
|
+
}) {
|
|
226
|
+
this.statusReactive.isInvalidTerritory = payload.hasEmptyValue()
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## `SelectEmitPayload` API
|
|
233
|
+
|
|
234
|
+
`SelectEmitPayload` extends [`BaseEmitPayload`](../lib/emit-payload/BaseEmitPayload.js).
|
|
235
|
+
|
|
236
|
+
| Member | Type | Description |
|
|
237
|
+
| --- | --- | --- |
|
|
238
|
+
| `value` | `string \| number \| object \| null` | Primary selected value (`selectedOption.value`) |
|
|
239
|
+
| `values` | `Array<string \| number \| object> \| null` | All selected values in multi-select |
|
|
240
|
+
| `controlElement` | `HTMLElement \| null` | Trigger element from custom event `detail.target` |
|
|
241
|
+
| `selectedOption` | `FuroSelectOption \| null` | Matched row from flatten list |
|
|
242
|
+
| `selectedIndex` | `number \| null` | `optionIndex` on matched row |
|
|
243
|
+
| `selectedOptions` | `Array<FuroSelectOption> \| null` | Multi-select matches |
|
|
244
|
+
| `selectedIndices` | `Array<number> \| null` | Multi-select indices |
|
|
245
|
+
| `hasEmptyValue()` | `boolean` | `value === null` |
|
|
246
|
+
| `hasSelectedValue()` | `boolean` | `value !== null` |
|
|
247
|
+
| `matchesOption({ value })` | `boolean` | Strict (`===`) equality with `value`. Matches object values by **reference only** — for object values compare by id/key instead |
|
|
248
|
+
|
|
249
|
+
Custom event `detail` also includes `selectedGroupIndex` / `selectedGroupIndices` (used internally; not exposed as payload getters today).
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## `v-model:value` and state handling
|
|
254
|
+
|
|
255
|
+
```html
|
|
256
|
+
<FuroSelect
|
|
257
|
+
v-model:value="context.territoryRef.value"
|
|
258
|
+
:parcel="{ options: context.territoryOptions, placeholder: 'Select a territory' }"
|
|
259
|
+
/>
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Equivalent to fallthrough `value` + `@update:value` with internal sync in `setupComponent()`.
|
|
263
|
+
|
|
264
|
+
### Controlled vs uncontrolled
|
|
265
|
+
|
|
266
|
+
| Mode | How |
|
|
267
|
+
| --- | --- |
|
|
268
|
+
| Controlled | `parcel.value` and/or `v-model:value` / fallthrough `value` |
|
|
269
|
+
| Uncontrolled | `parcel.defaultValue` (omit `value`) |
|
|
270
|
+
|
|
271
|
+
### State matrix
|
|
272
|
+
|
|
273
|
+
| State | Source |
|
|
274
|
+
| --- | --- |
|
|
275
|
+
| Selected value | `parcel.value` / `v-model:value` / `defaultValue` |
|
|
276
|
+
| Whole control `disabled` | `parcel.disabled` or fallthrough `disabled` on root |
|
|
277
|
+
| Row `disabled` | `option.disabled` and/or `group.disabled` |
|
|
278
|
+
| `invalid` | `parcel.invalid` |
|
|
279
|
+
| `placeholder` | `parcel.placeholder` |
|
|
280
|
+
| `options` | `parcel.options` |
|
|
281
|
+
| `loading` | `parcel.loading` (trigger UI; hides empty state) |
|
|
282
|
+
|
|
283
|
+
### Multiple selection
|
|
284
|
+
|
|
285
|
+
Set `parcel.multiple: true`. Use array `defaultValue` / internal value. `change-value` exposes `payload.value` as the **first** selected value; use `payload.values` / `payload.selectedOptions` for the full set. `update:value` now reliably emits the **full array** of selected values, and the selection **persists across re-open** (previously it emitted only a single value). In single mode `update:value` emits a single value.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Internal structure
|
|
290
|
+
|
|
291
|
+
### Underlying primitive and Furo option naming
|
|
292
|
+
|
|
293
|
+
The underlying headless select primitive uses item/group terminology. Furo’s public API uses **`option`** (slot and `parcel.options`); the component maps options to the primitive's item rows internally.
|
|
294
|
+
|
|
295
|
+
### Composition
|
|
296
|
+
|
|
297
|
+
```text
|
|
298
|
+
div.unit-form-control.select
|
|
299
|
+
root
|
|
300
|
+
trigger(.trigger)
|
|
301
|
+
slot trigger-content → value display + icon
|
|
302
|
+
portal
|
|
303
|
+
content(.unit-select-content)
|
|
304
|
+
slot content-before
|
|
305
|
+
scroll-up-button
|
|
306
|
+
viewport(.viewport)
|
|
307
|
+
group* / option* # group vs leaf from parcel.options
|
|
308
|
+
optional empty option
|
|
309
|
+
scroll-down-button
|
|
310
|
+
slot content-after
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Use `data-*` states in styles: `data-placeholder`, `data-disabled`, `data-highlighted`, etc.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Accessibility requirements
|
|
318
|
+
|
|
319
|
+
- Trigger remains a semantic `button`.
|
|
320
|
+
- Provide a visible label (`label` + `for` / `id`, or `aria-label` / `aria-labelledby`).
|
|
321
|
+
- `aria-invalid` is set on the trigger from `parcel.invalid`.
|
|
322
|
+
- Keyboard / focus behavior is delegated to the underlying primitive.
|
|
323
|
+
- Forward `name` and `required` on root for form integration.
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Styling convention
|
|
328
|
+
|
|
329
|
+
Inside the SFC, styles use `@layer furo`:
|
|
330
|
+
|
|
331
|
+
- Root wrapper: `.unit-form-control.select`
|
|
332
|
+
- Trigger: `.trigger` (invalid `.invalid`, loading `.loading`)
|
|
333
|
+
- Dropdown: `.unit-select-content`, `.viewport`, `.group`, `.option`, `.separator`
|
|
334
|
+
- Tokens align with other field atoms (`--color-input`, `--size-input-height`, etc.)
|
|
335
|
+
- Option rows take their minimum height from `--size-option-height`
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## Examples
|
|
340
|
+
|
|
341
|
+
### Basic (controlled)
|
|
342
|
+
|
|
343
|
+
```html
|
|
344
|
+
<FuroSelect
|
|
345
|
+
:parcel="{
|
|
346
|
+
value: context.statusReactive.territory,
|
|
347
|
+
invalid: context.statusReactive.isInvalidTerritory,
|
|
348
|
+
placeholder: 'Select a territory',
|
|
349
|
+
options: [
|
|
350
|
+
{ value: 'JP', label: 'Japan' },
|
|
351
|
+
{ value: 'VN', label: 'Vietnam' },
|
|
352
|
+
{ value: 'TW', label: 'Taiwan' },
|
|
353
|
+
{ value: 'US', label: 'United States' },
|
|
354
|
+
],
|
|
355
|
+
}"
|
|
356
|
+
name="territory"
|
|
357
|
+
:required="true"
|
|
358
|
+
@change-value="context.onChangeTerritory({ payload: $event })"
|
|
359
|
+
/>
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Grouped options
|
|
363
|
+
|
|
364
|
+
```html
|
|
365
|
+
<FuroSelect
|
|
366
|
+
:parcel="{
|
|
367
|
+
value: context.statusReactive.fruit,
|
|
368
|
+
placeholder: 'Pick a fruit',
|
|
369
|
+
options: [
|
|
370
|
+
{
|
|
371
|
+
label: 'Citrus',
|
|
372
|
+
options: [
|
|
373
|
+
{ value: 'orange', label: 'Orange' },
|
|
374
|
+
{ value: 'lemon', label: 'Lemon' },
|
|
375
|
+
],
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
label: 'Berries',
|
|
379
|
+
disabled: true,
|
|
380
|
+
options: [
|
|
381
|
+
{ value: 'strawberry', label: 'Strawberry' },
|
|
382
|
+
{ value: 'blueberry', label: 'Blueberry', disabled: true },
|
|
383
|
+
],
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
}"
|
|
387
|
+
/>
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Uncontrolled default
|
|
391
|
+
|
|
392
|
+
```html
|
|
393
|
+
<FuroSelect
|
|
394
|
+
name="territory"
|
|
395
|
+
:parcel="{
|
|
396
|
+
defaultValue: 'JP',
|
|
397
|
+
placeholder: 'Select a territory',
|
|
398
|
+
options: context.territoryOptions,
|
|
399
|
+
}"
|
|
400
|
+
/>
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Empty slot
|
|
404
|
+
|
|
405
|
+
```html
|
|
406
|
+
<FuroSelect
|
|
407
|
+
:parcel="{
|
|
408
|
+
placeholder: 'Assignee',
|
|
409
|
+
options: [],
|
|
410
|
+
}"
|
|
411
|
+
>
|
|
412
|
+
<template #empty>
|
|
413
|
+
<span>No assignees available</span>
|
|
414
|
+
</template>
|
|
415
|
+
</FuroSelect>
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Custom option row
|
|
419
|
+
|
|
420
|
+
```html
|
|
421
|
+
<FuroSelect
|
|
422
|
+
:parcel="{
|
|
423
|
+
placeholder: 'Choose row',
|
|
424
|
+
options: context.customRowOptions,
|
|
425
|
+
}"
|
|
426
|
+
>
|
|
427
|
+
<template #option-text="{ option }">
|
|
428
|
+
<strong>{{ option.label }}</strong>
|
|
429
|
+
<small>{{ option.caption }}</small>
|
|
430
|
+
</template>
|
|
431
|
+
</FuroSelect>
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Live samples: `pages/samples/components/playground/FuroSelectPlayground/index.vue`.
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## Known gaps and follow-ups
|
|
439
|
+
|
|
440
|
+
- **Multi-select:** `update:value` now emits the full array of selected values and the selection persists across re-open; `payload.value` still surfaces the primary value (use `payload.values` for the full set).
|
|
441
|
+
- **`empty` slot:** typedef allows `{ loading }` but the template does not pass scoped props yet.
|
|
442
|
+
- **Group classification:** spec previously used “no `value` on group”; implementation uses non-empty `options` only.
|
|
443
|
+
- **Open state:** `@update:open` / `open` on `parcel` not exposed.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# FuroSeparator
|
|
2
|
+
|
|
3
|
+
Presentational divider drawn between sibling content — toolbar groups, menu sections, and section boundaries. Built on a headless separator primitive that handles the correct `role` / `aria-orientation` semantics. It carries no value and emits nothing.
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
| Field | Value |
|
|
8
|
+
|---|---|
|
|
9
|
+
| **Layer** | Molecule |
|
|
10
|
+
| **Path** | `lib/components/molecules/FuroSeparator/` |
|
|
11
|
+
| **Base primitive** | Headless separator (`role="separator"` with `aria-orientation`, or `role="none"` when decorative) |
|
|
12
|
+
| **Composes** | Nothing — a single styled rule |
|
|
13
|
+
| **Emits** | None |
|
|
14
|
+
| **Orientation** | `horizontal` (default) or `vertical` |
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Public API
|
|
19
|
+
|
|
20
|
+
### Props
|
|
21
|
+
|
|
22
|
+
| Prop | Type | Default | Description |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| `parcel` | `SeparatorParcel \| null` | `null` | Reactive data object holding orientation and the decorative flag. |
|
|
25
|
+
|
|
26
|
+
### Parcel keys
|
|
27
|
+
|
|
28
|
+
| Key | Type | Default | Description |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Direction the divider runs. `horizontal` spans the full inline width; `vertical` stretches to the parent's height. |
|
|
31
|
+
| `decorative` | `boolean` | `false` | When `true`, the divider is purely visual and drops the `separator` role so assistive tech ignores it. Use for dividers that repeat content already conveyed by layout. |
|
|
32
|
+
|
|
33
|
+
### Emits
|
|
34
|
+
|
|
35
|
+
None. The separator is presentational.
|
|
36
|
+
|
|
37
|
+
### Slots
|
|
38
|
+
|
|
39
|
+
None.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Behavior
|
|
44
|
+
|
|
45
|
+
| Concern | Behavior |
|
|
46
|
+
|---|---|
|
|
47
|
+
| **Orientation** | `parcel.orientation` maps to the `horizontal` / `vertical` root class and to the primitive's `aria-orientation`; no JS branching. |
|
|
48
|
+
| **Semantics** | Non-decorative separators expose `role="separator"`. Decorative separators expose `role="none"` and no orientation semantics. |
|
|
49
|
+
| **Sizing** | Thickness is `--size-thinnest`. Horizontal fills `width: 100%`; vertical uses `align-self: stretch` so it matches a flex row's height. |
|
|
50
|
+
| **Passthrough** | Extra attributes fall through to the rule element (`inheritAttrs: false` + `v-bind="$attrs"`). |
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
```vue
|
|
57
|
+
<!-- Horizontal section divider -->
|
|
58
|
+
<FuroSeparator :parcel="{ orientation: 'horizontal' }" />
|
|
59
|
+
|
|
60
|
+
<!-- Vertical divider between toolbar groups -->
|
|
61
|
+
<div class="toolbar">
|
|
62
|
+
<FuroButton :parcel="{ label: 'Bold' }" />
|
|
63
|
+
<FuroSeparator :parcel="{ orientation: 'vertical' }" />
|
|
64
|
+
<FuroButton :parcel="{ label: 'Link' }" />
|
|
65
|
+
</div>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Composition notes
|
|
71
|
+
|
|
72
|
+
- Imports nothing from the app — no GraphQL, PrimeVue, Vue Router, or Nuxt-specific imports.
|
|
73
|
+
- All logic lives in `FuroSeparatorContext` as an extract-only getter (`orientation`) and one boolean method (`isDecorative()`).
|
|
74
|
+
- CSS is wrapped in `@layer furo`; dimensions use `rem` / `--size-thinnest` and the rule color references the semantic `--color-border` token only.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Testing
|
|
79
|
+
|
|
80
|
+
- Context logic (`orientation` getter, `isDecorative()`) is fully covered by jsdom unit tests, with the boolean method split into truthy / falsy describe blocks.
|
|
81
|
+
- No SFC mount test: behavior is declarative bindings over the tested context plus the headless primitive's own semantics. Rendered output (orientation class, role) is verified by manual QA in the playground.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Playground
|
|
86
|
+
|
|
87
|
+
`/samples/components/playground` → **Separator** tab. Demonstrates horizontal, vertical (between inline items), and decorative dividers.
|