@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,298 @@
|
|
|
1
|
+
# `<FuroDropdownMenu>` Interface Specification
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`<FuroDropdownMenu>` is an action/navigation menu molecule in the Furo-Vue component system. Its **public contract is Furo-defined**: applications pass `parcel` and `triggerParcel`, listen to Furo emits, and optionally override the trigger slot — without importing any headless menu library.
|
|
6
|
+
|
|
7
|
+
Implementation uses a headless menu primitive inside `lib/components/molecules/FuroDropdownMenu/*` only. That dependency is **not** part of the application API.
|
|
8
|
+
|
|
9
|
+
**No `v-model`:** unlike `FuroSelect`, this menu does not represent a form value. It emits selection and checkbox events; parent code owns persistent checked state for checkbox rows.
|
|
10
|
+
|
|
11
|
+
**Default trigger:** the underlying primitive renders a native `<button>` trigger (no `as-child`). Appearance uses `triggerParcel` tokens (`variant`, `size`) aligned with `FuroButton` — the `FuroButton` SFC is **not** used as trigger because `as-child` composition breaks open behavior.
|
|
12
|
+
|
|
13
|
+
**Custom trigger slot:** Label content only (text, icons). Do not nest another `<button>`.
|
|
14
|
+
|
|
15
|
+
**Source files:**
|
|
16
|
+
|
|
17
|
+
- `lib/components/molecules/FuroDropdownMenu/FuroDropdownMenu.vue`
|
|
18
|
+
- `lib/components/molecules/FuroDropdownMenu/FuroDropdownMenuOptionRows.vue`
|
|
19
|
+
- `lib/components/molecules/FuroDropdownMenu/FuroDropdownMenuContext.js`
|
|
20
|
+
- `lib/components/molecules/FuroDropdownMenu/DropdownMenuOptionSelectEmitPayload.js`
|
|
21
|
+
|
|
22
|
+
**Live samples:** `pages/samples/components/playground/FuroDropdownMenuPlayground/index.vue`
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Public API at a glance
|
|
27
|
+
|
|
28
|
+
| Area | Status | Notes |
|
|
29
|
+
| ---- | ------ | ----- |
|
|
30
|
+
| **SFC surface** | | |
|
|
31
|
+
| Props `parcel`, `triggerParcel` | **Implemented** | See tables below |
|
|
32
|
+
| **Menu** | | |
|
|
33
|
+
| `parcel.options` declarative rows | **Implemented** | Action, checkbox, submenu, separator |
|
|
34
|
+
| Custom trigger slot | **Implemented** | `#trigger`; label inside native trigger button |
|
|
35
|
+
| `option-label` scoped slot | **Implemented** | Override row label rendering |
|
|
36
|
+
| Default slot | **Implemented** | Extra menu children (advanced) |
|
|
37
|
+
| **Emits** | | |
|
|
38
|
+
| `open:change` | **Implemented** | `{ open: boolean }` |
|
|
39
|
+
| `option:select` | **Implemented** | `DropdownMenuOptionSelectEmitPayload` |
|
|
40
|
+
| `option:checked-change` | **Implemented** | `{ option, checked }` |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Props
|
|
45
|
+
|
|
46
|
+
### `parcel` — `FuroDropdownMenuParcel`
|
|
47
|
+
|
|
48
|
+
| Key | Type | Default | Description |
|
|
49
|
+
| --- | ---- | ------- | ----------- |
|
|
50
|
+
| `open` | `boolean` | uncontrolled | Controlled open state; omit for internal state |
|
|
51
|
+
| `disabled` | `boolean` | `false` | Disables trigger and menu |
|
|
52
|
+
| `side` | `'top' \| 'right' \| 'bottom' \| 'left'` | `'bottom'` | Positioning control: which edge of the trigger the menu opens against |
|
|
53
|
+
| `align` | `'start' \| 'center' \| 'end'` | `'start'` | Positioning control: how the menu aligns along that edge |
|
|
54
|
+
| `sideOffset` | `number` | `4` | Positioning control: gap between trigger and menu, in `rem`-equivalent pixels |
|
|
55
|
+
| `options` | `Array<FuroDropdownMenuOption>` | `[]` | Declarative menu rows |
|
|
56
|
+
|
|
57
|
+
### `FuroDropdownMenuOption`
|
|
58
|
+
|
|
59
|
+
| Key | Type | Description |
|
|
60
|
+
| --- | ---- | ----------- |
|
|
61
|
+
| `label` | `string` | Visible row label |
|
|
62
|
+
| `value` | `string` | Stable identifier for handlers |
|
|
63
|
+
| `disabled` | `boolean` | Non-interactive row |
|
|
64
|
+
| `separator` | `boolean` | Renders separator instead of a selectable row |
|
|
65
|
+
| `variant` | `'default' \| 'destructive'` | Destructive styling for delete actions |
|
|
66
|
+
| `type` | `'action' \| 'checkbox' \| 'submenu'` | Row kind; omit for action |
|
|
67
|
+
| `checked` | `boolean` | Checkbox row checked state (parent-owned) |
|
|
68
|
+
| `keepOpen` | `boolean` | Action row: keep menu open after select |
|
|
69
|
+
| `options` | `Array<FuroDropdownMenuOption>` | Nested rows for `type: 'submenu'` |
|
|
70
|
+
|
|
71
|
+
**Row kind resolution (priority):**
|
|
72
|
+
|
|
73
|
+
1. `separator: true` → separator
|
|
74
|
+
2. `type: 'submenu'` with `options` array → nested submenu
|
|
75
|
+
3. `type: 'checkbox'` → checkbox row (menu stays open on toggle)
|
|
76
|
+
4. default → action row (closes menu unless `keepOpen: true`)
|
|
77
|
+
|
|
78
|
+
### `triggerParcel` — trigger appearance
|
|
79
|
+
|
|
80
|
+
| Key | Type | Default | Description |
|
|
81
|
+
| --- | ---- | ------- | ----------- |
|
|
82
|
+
| `variant` | `string` | — | Default trigger appearance token, aligned with `FuroButton` |
|
|
83
|
+
| `size` | `string` | — | Default trigger size token, aligned with `FuroButton` |
|
|
84
|
+
| `asChild` | `boolean` | `false` | When `true`, the `#trigger` slot's own element becomes the trigger — no wrapper `<button>` and no forced `.unit-button` styling — so a consumer can supply their own trigger element without a nested button |
|
|
85
|
+
|
|
86
|
+
By default (`asChild: false`) the menu renders the styled native button and the
|
|
87
|
+
`#trigger` slot fills its label content. Set `asChild: true` to hand the trigger
|
|
88
|
+
element to the consumer (for example a `<FuroButton>` or an `<a>`), letting the
|
|
89
|
+
slotted element itself become the trigger. See [Consumer-owned trigger](#consumer-owned-trigger-aschild).
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Emits
|
|
94
|
+
|
|
95
|
+
| Event | Payload | When |
|
|
96
|
+
| ----- | ------- | ---- |
|
|
97
|
+
| `open:change` | `{ open: boolean }` | Menu open state changes |
|
|
98
|
+
| `option:select` | `DropdownMenuOptionSelectEmitPayload` | User selects an enabled action row |
|
|
99
|
+
| `option:checked-change` | `{ option: FuroDropdownMenuOption, checked: boolean }` | User toggles a checkbox row |
|
|
100
|
+
|
|
101
|
+
### Parent handler style (action rows)
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
runMenuAction ({
|
|
105
|
+
payload,
|
|
106
|
+
}) {
|
|
107
|
+
const value = payload.option.value
|
|
108
|
+
|
|
109
|
+
if (!value) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.runAction({
|
|
114
|
+
value,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Parent handler style (checkbox rows)
|
|
120
|
+
|
|
121
|
+
Checkbox rows do **not** close the root menu. Parent code must update `parcel.options` (including nested submenu options) when receiving `option:checked-change`:
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
syncCheckboxOption ({
|
|
125
|
+
payload,
|
|
126
|
+
}) {
|
|
127
|
+
this.menuOptions = this.applyCheckedChangeToOptions({
|
|
128
|
+
options: this.menuOptions,
|
|
129
|
+
targetValue: payload.option.value,
|
|
130
|
+
checked: payload.checked,
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Use `keepOpen: true` on action rows when the menu should stay open after a one-shot action (for example “Reset filters” inside a submenu).
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Slots
|
|
140
|
+
|
|
141
|
+
| Slot | Scoped props | Default |
|
|
142
|
+
| ---- | ------------ | ------- |
|
|
143
|
+
| `trigger` | — | Native button labeled "Menu" (FuroButton-compatible styling) |
|
|
144
|
+
| `option-label` | `option`, `index` | `option.label` text |
|
|
145
|
+
| `default` | — | Empty — append custom menu content (advanced) |
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Accessibility
|
|
150
|
+
|
|
151
|
+
- Default trigger sets `aria-haspopup="menu"`.
|
|
152
|
+
- The underlying primitive manages `aria-expanded` on the trigger.
|
|
153
|
+
- Keyboard: Arrow keys navigate, Enter selects, Escape closes (primitive baseline).
|
|
154
|
+
- Checkbox rows expose checked state through the underlying primitive's checkbox row.
|
|
155
|
+
- Disabled options are skipped via the underlying primitive plus a Furo guard on emits.
|
|
156
|
+
- Menu rows take their minimum height from `--size-option-height`, so an app raises them to a larger touch target without overriding menu CSS.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Examples
|
|
161
|
+
|
|
162
|
+
### Declarative action options
|
|
163
|
+
|
|
164
|
+
```html
|
|
165
|
+
<FuroDropdownMenu
|
|
166
|
+
:parcel="{
|
|
167
|
+
options: [
|
|
168
|
+
{ label: 'Edit', value: 'edit' },
|
|
169
|
+
{ separator: true },
|
|
170
|
+
{ label: 'Delete', value: 'delete', variant: 'destructive' },
|
|
171
|
+
],
|
|
172
|
+
}"
|
|
173
|
+
@option:select="context.runRowAction({ payload: $event })"
|
|
174
|
+
/>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Checkbox filters (multi-toggle, menu stays open)
|
|
178
|
+
|
|
179
|
+
```html
|
|
180
|
+
<FuroDropdownMenu
|
|
181
|
+
:parcel="{
|
|
182
|
+
options: context.extractCheckboxFilterMenuOptions(),
|
|
183
|
+
}"
|
|
184
|
+
@option:checked-change="context.recordOptionCheckedChange({ payload: $event })"
|
|
185
|
+
/>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
extractCheckboxFilterMenuOptions () {
|
|
190
|
+
return [
|
|
191
|
+
{
|
|
192
|
+
type: 'checkbox',
|
|
193
|
+
label: 'Show archived',
|
|
194
|
+
value: 'archived',
|
|
195
|
+
checked: this.showArchived,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
type: 'checkbox',
|
|
199
|
+
label: 'Show drafts',
|
|
200
|
+
value: 'drafts',
|
|
201
|
+
checked: this.showDrafts,
|
|
202
|
+
},
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Submenu with nested checkboxes
|
|
208
|
+
|
|
209
|
+
```html
|
|
210
|
+
<FuroDropdownMenu
|
|
211
|
+
:parcel="{
|
|
212
|
+
options: [
|
|
213
|
+
{ label: 'Export CSV', value: 'export-csv' },
|
|
214
|
+
{
|
|
215
|
+
type: 'submenu',
|
|
216
|
+
label: 'Column filters',
|
|
217
|
+
value: 'column-filters',
|
|
218
|
+
options: context.extractCheckboxFilterMenuOptions(),
|
|
219
|
+
},
|
|
220
|
+
{ separator: true },
|
|
221
|
+
{
|
|
222
|
+
label: 'Reset filters',
|
|
223
|
+
value: 'reset-filters',
|
|
224
|
+
keepOpen: true,
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
}"
|
|
228
|
+
@option:select="context.recordOptionSelect({ payload: $event })"
|
|
229
|
+
@option:checked-change="context.recordOptionCheckedChange({ payload: $event })"
|
|
230
|
+
/>
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Custom trigger
|
|
234
|
+
|
|
235
|
+
Set `triggerParcel` for variant/size, then override `#trigger` slot content:
|
|
236
|
+
|
|
237
|
+
```html
|
|
238
|
+
<FuroDropdownMenu
|
|
239
|
+
:parcel="{ options: context.extractActionMenuOptions() }"
|
|
240
|
+
:trigger-parcel="{
|
|
241
|
+
variant: 'secondary',
|
|
242
|
+
size: 'sm',
|
|
243
|
+
}"
|
|
244
|
+
@option:select="context.runRowAction({ payload: $event })"
|
|
245
|
+
>
|
|
246
|
+
<template #trigger>
|
|
247
|
+
<Icon name="ph:dots-three-outline" />
|
|
248
|
+
Actions
|
|
249
|
+
</template>
|
|
250
|
+
</FuroDropdownMenu>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Consumer-owned trigger (`asChild`)
|
|
254
|
+
|
|
255
|
+
Set `triggerParcel.asChild: true` so the slotted element itself becomes the
|
|
256
|
+
trigger — no wrapper `<button>`, no forced `.unit-button` styling:
|
|
257
|
+
|
|
258
|
+
```html
|
|
259
|
+
<FuroDropdownMenu
|
|
260
|
+
:parcel="{ options: context.extractActionMenuOptions() }"
|
|
261
|
+
:trigger-parcel="{ asChild: true }"
|
|
262
|
+
@option:select="context.runRowAction({ payload: $event })"
|
|
263
|
+
>
|
|
264
|
+
<template #trigger>
|
|
265
|
+
<FuroButton :parcel="{ variant: 'secondary' }">
|
|
266
|
+
Actions
|
|
267
|
+
</FuroButton>
|
|
268
|
+
</template>
|
|
269
|
+
</FuroDropdownMenu>
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
The default (`asChild: false`) still renders the styled native button.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Implementation status
|
|
277
|
+
|
|
278
|
+
| Deliverable | Status |
|
|
279
|
+
| ----------- | ------ |
|
|
280
|
+
| `FuroDropdownMenu.vue` + context + payloads | Shipped |
|
|
281
|
+
| `FuroDropdownMenuOptionRows.vue` (recursive rows) | Shipped |
|
|
282
|
+
| Submenus (`type: 'submenu'`) | Shipped |
|
|
283
|
+
| Checkbox options (`type: 'checkbox'`) | Shipped |
|
|
284
|
+
| `keepOpen` action rows | Shipped |
|
|
285
|
+
| Playground `FuroDropdownMenuPlayground` | Shipped |
|
|
286
|
+
| Unit tests (context + payloads) | Shipped |
|
|
287
|
+
| Default trigger via `<FuroButton>` SFC | **Deferred** | Blocked by atom polymorphic root — see below |
|
|
288
|
+
| Radio menu options | Out of scope |
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Deferred work: default trigger as `<FuroButton>`
|
|
293
|
+
|
|
294
|
+
**Status:** Intentionally not shipped. Default trigger is the primitive's native trigger button with `FuroButton`-compatible styling via `triggerParcel`.
|
|
295
|
+
|
|
296
|
+
**Why:** the primitive's `as-child` trigger + `<FuroButton>` does not open the menu — Vue component boundary and missing `ref` forward on `FuroButton`. Full analysis: `docs/furo-vue/performance-and-pitfalls.md` §11.
|
|
297
|
+
|
|
298
|
+
**When fixed on `FuroButton`:** Re-evaluate replacing duplicated `.unit-button` CSS in `FuroDropdownMenu.vue` with `<FuroButton>` as the default trigger. Until then, do not nest `<FuroButton>` under the primitive's `as-child` trigger in application code.
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
# `<FuroEditableField>` Interface Specification
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`<FuroEditableField>` is a **shipped** editable molecule in the Furo-Vue component system. Applications use `parcel`, Furo emits, documented slots, and attribute fallthrough only — no internal implementation imports.
|
|
6
|
+
|
|
7
|
+
Implementation wraps a headless inline-edit primitive (`Root`, `Area`, `Preview`, `Input`, …) inside `lib/components/molecules/FuroEditableField/*`. The underlying primitive is **not** part of the application API.
|
|
8
|
+
|
|
9
|
+
Contract style aligns with [`FuroTextField`](./furo-text-field.md) and [`FuroCheckbox`](./furo-checkbox.md):
|
|
10
|
+
|
|
11
|
+
- Furo-owned behavior via `:parcel` (`FuroEditableFieldParcel`)
|
|
12
|
+
- primitive-owned edit lifecycle keys also live on `parcel` but are **forwarded** to the root (not interpreted by Furo)
|
|
13
|
+
- HTML / form attributes via fallthrough (`name`, `required`, `id`, `aria-*`, …)
|
|
14
|
+
- Furo emit: `commit-value` with `EditableEmitPayload`
|
|
15
|
+
- default **text** editor (`FuroTextField`) when `#editor` is omitted
|
|
16
|
+
- `#editor` slot for number, boolean, select, file, and other custom controls
|
|
17
|
+
|
|
18
|
+
**Public SFC prop:** `parcel` only (+ attribute fallthrough).
|
|
19
|
+
|
|
20
|
+
**Live samples:** `pages/samples/components/playground/FuroEditableFieldPlayground/index.vue`
|
|
21
|
+
|
|
22
|
+
### Abstraction boundary
|
|
23
|
+
|
|
24
|
+
| Layer | Responsibility |
|
|
25
|
+
| ----- | -------------- |
|
|
26
|
+
| Application | `parcel`, slots, `@commit-value` listener; updates `parcel.value` after commit |
|
|
27
|
+
| Molecule (`FuroEditableField`) | Draft/commit sync, preview text, floating editor position, undo, `EditableEmitPayload` |
|
|
28
|
+
| Underlying primitive (maintainers) | Edit mode open/close, activation/submit gestures on default surface; internal only |
|
|
29
|
+
|
|
30
|
+
**Rules:**
|
|
31
|
+
|
|
32
|
+
1. Documented **Furo-owned** `parcel` keys are stripped in `extractRootAttributes()` before optional passthrough to the underlying primitive.
|
|
33
|
+
2. Do **not** import the underlying editable primitive in application code.
|
|
34
|
+
3. Sync committed value in the parent on `@commit-value` — Furo does **not** emit `update:value`.
|
|
35
|
+
|
|
36
|
+
**Source files:**
|
|
37
|
+
|
|
38
|
+
- `lib/components/molecules/FuroEditableField/FuroEditableField.vue`
|
|
39
|
+
- `lib/components/molecules/FuroEditableField/FuroEditableFieldContext.js`
|
|
40
|
+
- `lib/components/molecules/FuroEditableField/EditableEmitPayload.js`
|
|
41
|
+
- `lib/components/molecules/FuroEditableField/processors/EditorCollisionProcessor.js`
|
|
42
|
+
- `lib/components/molecules/FuroEditableField/processors/ObjectProcessor.js`
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Public API at a glance
|
|
47
|
+
|
|
48
|
+
| Area | Status | Notes |
|
|
49
|
+
| ---- | ------ | ----- |
|
|
50
|
+
| **SFC surface** | | |
|
|
51
|
+
| Public prop `parcel` only (+ attribute fallthrough) | **Implemented** | See [`FuroEditableFieldParcel`](#furoeditablefieldparcel) |
|
|
52
|
+
| **Value** | | |
|
|
53
|
+
| `parcel.value` (controlled sync) | **Implemented** | Watched; updates `committedValueRef` and `draftValueRef` |
|
|
54
|
+
| `v-model:value` | **Not implemented** | Listen to `@commit-value` and assign `parcel.value` in the parent |
|
|
55
|
+
| **Furo-owned parcel** | | |
|
|
56
|
+
| `invalid`, `disabled`, `required`, `readonly` | **Implemented** | `disabled` / `required` / `readonly` also from fallthrough attrs |
|
|
57
|
+
| `placeholder` | **Implemented** | Default text editor + forwarded placeholder on the root |
|
|
58
|
+
| `emptyPreviewText` | **Implemented** | Default `'---'` when value is empty |
|
|
59
|
+
| `showActions` | **Implemented** | Default `false`; shows Edit trigger and enables action-button row |
|
|
60
|
+
| `showEditing` | **Implemented** | Default `false`; sets internal editing flag on mount |
|
|
61
|
+
| `showUndoChange` | **Implemented** | Default `false`; shows undo control when committed value diverges from mount snapshot |
|
|
62
|
+
| `collisionBoundary` | **Implemented** | `HTMLElement`, `Ref<HTMLElement>`, or `null`; clamps floating editor position |
|
|
63
|
+
| **Primitive-forwarded parcel** | | |
|
|
64
|
+
| `activationMode`, `submitMode`, `selectOnFocus`, `startWithEditMode`, `maxLength`, `autoResize` | **Implemented** | Passed through `extractRootAttributes()` to the root; the primitive's defaults apply when omitted |
|
|
65
|
+
| `variant`, `preserveLayout`, `editOverlay` | **Not implemented** | Editor uses `position: fixed` collision positioning when editing |
|
|
66
|
+
| `previewText`, `multiline`, `editing` / `v-model:editing` | **Not implemented** | Use `#preview` slot; `#editor` + `FuroTextarea`; the primitive's state is internal |
|
|
67
|
+
| **Emits** | | |
|
|
68
|
+
| `commit-value` | **Implemented** | On successful submit and on undo |
|
|
69
|
+
| `update-editing-state` | **Implemented** | the primitive's `@update:state` → `onUpdateEditingState()`; payload is `EdittingState` |
|
|
70
|
+
| `change-value`, `cancel-edit`, `update:value` | **Not implemented** | Draft changes are local until commit |
|
|
71
|
+
| **Slots** | | |
|
|
72
|
+
| `preview`, `editor` | **Implemented** | `editor` replaces default `FuroTextField` |
|
|
73
|
+
| `edit-trigger`, `submit-trigger`, `cancel-trigger`, `undo-trigger` | **Implemented** | Optional custom triggers |
|
|
74
|
+
| `#edit` (legacy name) | **Not implemented** | Use `#editor` |
|
|
75
|
+
| **Editor overlay** | | |
|
|
76
|
+
| Fixed collision-aware editor panel | **Implemented** | `EditorCollisionProcessor`; scroll/resize listeners while editing |
|
|
77
|
+
| Teleport to `document.body`, `editOverlay: 'inset'` | **Not implemented** | |
|
|
78
|
+
| Closed `parcel` (reject unknown keys) | **Not implemented** | Unknown keys may pass through to the underlying primitive |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## `FuroEditableFieldParcel`
|
|
83
|
+
|
|
84
|
+
Behavior-controlling props passed as `:parcel`. Furo-owned values must be **JSON-serializable** (no functions, no class instances). `collisionBoundary` may be a Vue `Ref`.
|
|
85
|
+
|
|
86
|
+
### `parcel` vs fallthrough
|
|
87
|
+
|
|
88
|
+
| Mechanism | Use for |
|
|
89
|
+
| --------- | ------- |
|
|
90
|
+
| `:parcel` | Furo behavior and the root's options |
|
|
91
|
+
| Fallthrough attributes | Native HTML on the root (`name`, `required`, `id`, `aria-*`, `placeholder`, …) |
|
|
92
|
+
|
|
93
|
+
Merged defaults (Furo-owned keys only):
|
|
94
|
+
|
|
95
|
+
| Key | Default |
|
|
96
|
+
| --- | ------- |
|
|
97
|
+
| `emptyPreviewText` | `'---'` |
|
|
98
|
+
| `showActions` | `false` |
|
|
99
|
+
| `showEditing` | `false` |
|
|
100
|
+
| `showUndoChange` | `false` |
|
|
101
|
+
|
|
102
|
+
### Furo-owned keys
|
|
103
|
+
|
|
104
|
+
Stripped before forwarding to the underlying primitive. Interpreted by `FuroEditableFieldContext`.
|
|
105
|
+
|
|
106
|
+
| Key | Type | Default | Description |
|
|
107
|
+
| --- | ---- | ------- | ----------- |
|
|
108
|
+
| `value` | `FuroEditableValue` | — | Committed value. Parent should update this after `@commit-value` |
|
|
109
|
+
| `invalid` | `boolean` | `false` | Passed to default `FuroTextField`; use `#editor` for custom invalid UI |
|
|
110
|
+
| `disabled` | `boolean` | `false` | Also respected from fallthrough `disabled`, including the bare `<FuroEditableField disabled>` spelling |
|
|
111
|
+
| `required` | `boolean` | `false` | Also respected from fallthrough `required`, including the bare `<FuroEditableField required>` spelling |
|
|
112
|
+
| `readonly` | `boolean` | `false` | Also respected from fallthrough `readonly`, including the bare `<FuroEditableField readonly>` spelling |
|
|
113
|
+
| `editLabel` | `string` | `'Edit'` | Accessible name of the edit trigger |
|
|
114
|
+
| `undoLabel` | `string` | `'Undo'` | Accessible name of the undo control |
|
|
115
|
+
| `submitLabel` | `string` | `'Submit'` | Accessible name of the submit control |
|
|
116
|
+
| `cancelLabel` | `string` | `'Cancel'` | Accessible name of the cancel control |
|
|
117
|
+
| `placeholder` | `string` \| `{ edit: string, preview: string }` | fallthrough `placeholder` or `''` | Default text editor placeholder |
|
|
118
|
+
| `emptyPreviewText` | `string` | `'---'` | Shown when `hasEmptyValue()` is true (no `#preview` slot) |
|
|
119
|
+
| `showActions` | `boolean` | `false` | Show Edit icon trigger (`v-show` on the edit-trigger part) |
|
|
120
|
+
| `showEditing` | `boolean` | `false` | On mount, sets internal `isShowEditing` used for editor positioning |
|
|
121
|
+
| `showUndoChange` | `boolean` | `false` | Show undo button when committed value changed since mount |
|
|
122
|
+
| `collisionBoundary` | `HTMLElement` \| `Ref<HTMLElement \| null>` \| `null` | — | Bounding rect for editor collision; viewport when omitted |
|
|
123
|
+
|
|
124
|
+
A fallthrough `disabled` or `required` is read by presence, the way HTML reads a valueless flag: every value turns it on except `false` and an absent attribute, so `:disabled="0"` disables. Bind a comparison, as in `:disabled="selectedCount === 0"`, when a number drives the flag. See [ADR 0036](adr/0036-boolean-attribute-presence-decides.md).
|
|
125
|
+
|
|
126
|
+
### Primitive-forwarded keys
|
|
127
|
+
|
|
128
|
+
Not in `FURO_EDITABLE_PARCEL_KEYS`. Merged into `extractRootAttributes()` and passed to the root. Furo does not validate or default these — the underlying primitive's semantics apply.
|
|
129
|
+
|
|
130
|
+
| Key | Type (JSDoc) | Description |
|
|
131
|
+
| --- | ------------ | ----------- |
|
|
132
|
+
| `activationMode` | `'dblclick' \| 'focus' \| 'none'` | How preview enters edit mode |
|
|
133
|
+
| `submitMode` | `'blur' \| 'enter' \| 'both' \| 'none'` | When the primitive auto-submits the draft |
|
|
134
|
+
| `selectOnFocus` | `boolean` | Select all on focus in the primitive's default input path |
|
|
135
|
+
| `startWithEditMode` | `boolean` | Mount in edit mode (primitive) |
|
|
136
|
+
| `maxLength` | `number` | Max length (primitive default input path) |
|
|
137
|
+
| `autoResize` | `boolean` | Auto-resize (primitive default input path) |
|
|
138
|
+
|
|
139
|
+
Other unknown `parcel` keys are also forwarded to the underlying primitive unless added to `FURO_EDITABLE_PARCEL_KEYS` in a future release.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Emits
|
|
144
|
+
|
|
145
|
+
Each `@commit-value` argument is an **`EditableEmitPayload` instance**. `$event.value` is the committed value.
|
|
146
|
+
|
|
147
|
+
```html
|
|
148
|
+
<FuroEditableField
|
|
149
|
+
:parcel="{ value: context.titleRef.value, showActions: true }"
|
|
150
|
+
@commit-value="context.titleRef.value = $event.value"
|
|
151
|
+
@update-editing-state="context.onUpdateEditingState({ editingState: $event })"
|
|
152
|
+
/>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`$event` is an `EdittingState`: `'edit'` when the editor opens, `'submit'` or `'cancel'` when it closes.
|
|
156
|
+
|
|
157
|
+
| Event | Payload | When |
|
|
158
|
+
| ----- | ------- | ---- |
|
|
159
|
+
| `commit-value` | `EditableEmitPayload` | the primitive's `@submit` → `onSubmitCommittedValue()`; undo button → `onUndoChange()` |
|
|
160
|
+
| `update-editing-state` | `EdittingState` | the primitive's `@update:state` → `onUpdateEditingState()`; values: `'edit'`, `'cancel'`, `'submit'` |
|
|
161
|
+
|
|
162
|
+
Furo does **not** emit `change-value`, `cancel-edit`, or `update:value`. Draft updates via `updateDraftValue` in the `#editor` slot are internal until the primitive submits or the parent calls scoped `submit()` from the root's slot props.
|
|
163
|
+
|
|
164
|
+
### `EditableEmitPayload`
|
|
165
|
+
|
|
166
|
+
| Member | Description |
|
|
167
|
+
| ------ | ----------- |
|
|
168
|
+
| `.value` | Committed value from event `detail.value` |
|
|
169
|
+
| `.controlElement` | `detail.target` from the raw `CustomEvent`, or `null` |
|
|
170
|
+
| `.hasEmptyValue()` | `true` for `null`, `''`, `{}`, `[]` |
|
|
171
|
+
|
|
172
|
+
Not on payload (removed from earlier spec drafts): `.draftValue`, `.committedValue`, `.reason`, `.isEditing()`, `.extractTrimmedText()`.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Slots
|
|
177
|
+
|
|
178
|
+
| Slot | Scoped props | Notes |
|
|
179
|
+
| ---- | ------------- | ----- |
|
|
180
|
+
| `preview` | root props + `committedValue`, `draftValue`, `updateDraftValue`, `undoChange` | Used when `hasEmptyValue()` is false; overrides `resolvePreviewText()` |
|
|
181
|
+
| `editor` | Same as `preview` | Replaces default `FuroTextField`; hosts custom atoms |
|
|
182
|
+
| `edit-trigger` | Same | `as-child` when slot provided |
|
|
183
|
+
| `submit-trigger` | Same | Inside editor `.actions`; visible when `isEditing` |
|
|
184
|
+
| `cancel-trigger` | Same | Inside editor `.actions`; visible when `isEditing` |
|
|
185
|
+
| `undo-trigger` | Same | Shown when `showUndoChange` and value changed since mount |
|
|
186
|
+
|
|
187
|
+
The root's scoped props (`isEditing`, `submit`, `cancel`, `edit`, `modelValue`, `isEmpty`, …) are spread into every slot via `buildSlotProps()`.
|
|
188
|
+
|
|
189
|
+
### `#editor` integration contract
|
|
190
|
+
|
|
191
|
+
1. Read `draftValue` — synced from `parcel.value` when edit opens and on each `updateDraftValue`.
|
|
192
|
+
2. On each change → `updateDraftValue({ value })` — updates draft only (no Furo emit).
|
|
193
|
+
3. On save → call scoped `submit()` from the root's props, or rely on the primitive's `submitMode` (`'blur'`, `'enter'`, `'both'`) on the root.
|
|
194
|
+
4. On discard → call scoped `cancel()` from the root's props.
|
|
195
|
+
|
|
196
|
+
For commit-on-pick controls (select, checkbox, file), set `submitMode: 'none'` on `parcel` and call `submit()` from the child handler, or sync draft and let blur submit if appropriate.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Layout & action UI
|
|
201
|
+
|
|
202
|
+
| Concern | Behavior |
|
|
203
|
+
| ------- | -------- |
|
|
204
|
+
| Root layout | `.unit-form-control.editable` — flex row; preview ellipsis |
|
|
205
|
+
| Edit trigger | Shown when `parcel.showActions` |
|
|
206
|
+
| Submit / Cancel row | Shown when `showActions` **or** any of `edit-trigger` / `submit-trigger` / `cancel-trigger` slots exist; submit/cancel icons only while `isEditing` |
|
|
207
|
+
| Undo | Shown when `showUndoChange` and `hasOriginalValueChanged()` |
|
|
208
|
+
| Editor panel | `position: fixed` while editing; width matches anchor cell; `z-index: calc(var(--value-z-index-layer-overlay) + 0)` |
|
|
209
|
+
|
|
210
|
+
There is no `variant: 'inline' \| 'form'` flag. Use `showActions` and table-friendly `collisionBoundary` for inline grids.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Edit lifecycle
|
|
215
|
+
|
|
216
|
+
```mermaid
|
|
217
|
+
stateDiagram-v2
|
|
218
|
+
[*] --> Preview: mount
|
|
219
|
+
Preview --> Editing: primitive activation (click/focus/dblclick/trigger)
|
|
220
|
+
Editing --> Preview: submit() / blur+submitMode / cancel()
|
|
221
|
+
Editing --> Preview: scroll outside editor (primitive cancel)
|
|
222
|
+
Preview --> Preview: commit-value emit (parent updates parcel.value)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
| Stage | What happens |
|
|
226
|
+
| ----- | ------------ |
|
|
227
|
+
| Mount | `originalValueRef` ← `parcel.value`; `committedValueRef` / `draftValueRef` synced from `parcel.value` watch |
|
|
228
|
+
| Enter edit | the root opens; `@update:state` → `onUpdateEditingState` → `update-editing-state` emit; focus first `input` / `textarea` in editor |
|
|
229
|
+
| Draft | Default editor: `@change-value` on `FuroTextField` → `updateDraftValue`. Custom `#editor`: parent calls `updateDraftValue` |
|
|
230
|
+
| Commit | the primitive's `@submit` → `onSubmitCommittedValue` → `commit-value` with current draft |
|
|
231
|
+
| Cancel | the primitive's `cancel()` — no Furo emit |
|
|
232
|
+
| Scroll | Window scroll (capture) outside floating editor → `editableRootInstanceRef.cancel()` |
|
|
233
|
+
| Resize | Window `resize` while editing → recalculate editor position |
|
|
234
|
+
| Undo | `onUndoChange()` restores `originalValueRef` to committed + draft → `commit-value` |
|
|
235
|
+
|
|
236
|
+
`parcel.showEditing` only sets internal `isShowEditing` on mount for positioning; prefer the primitive's `startWithEditMode` to open edit on mount.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Editor collision / overlay
|
|
241
|
+
|
|
242
|
+
While `isShowEditing` is true, `generateEditorPositionStyle()` runs `EditorCollisionProcessor`:
|
|
243
|
+
|
|
244
|
+
| Input | Role |
|
|
245
|
+
| ----- | ---- |
|
|
246
|
+
| Anchor rect | `.unit-form-control.editable` root `getBoundingClientRect()` |
|
|
247
|
+
| Editor size | Floating `.editor` `offsetWidth` / `offsetHeight` |
|
|
248
|
+
| Boundary | `parcel.collisionBoundary` rect, or viewport (`document.documentElement.clientWidth` / `clientHeight`) |
|
|
249
|
+
|
|
250
|
+
| Rule | Behavior |
|
|
251
|
+
| ---- | -------- |
|
|
252
|
+
| Horizontal overflow | Prefer anchor `left`; flip to `anchor.right - editorWidth` if needed; clamp inside boundary |
|
|
253
|
+
| Vertical overflow | Prefer anchor `top`; flip to `anchor.bottom - editorHeight` if needed; clamp inside boundary |
|
|
254
|
+
| Width | Editor panel width = anchor width |
|
|
255
|
+
|
|
256
|
+
**Table usage:** pass the table element as `collisionBoundary` so dropdowns stay inside the table box (see playground).
|
|
257
|
+
|
|
258
|
+
```html
|
|
259
|
+
:parcel="{
|
|
260
|
+
value: row.title,
|
|
261
|
+
showActions: true,
|
|
262
|
+
collisionBoundary: context.tableElementRef.value,
|
|
263
|
+
}"
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Editor is **not** teleported to `document.body`; it is `position: fixed` relative to the viewport with coordinates derived from the anchor.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Value types
|
|
271
|
+
|
|
272
|
+
`FuroEditableValue` — any value except `undefined`.
|
|
273
|
+
|
|
274
|
+
| Kind | Examples | Default preview (`resolvePreviewText`) | Empty (`hasEmptyValue`) |
|
|
275
|
+
| ---- | -------- | -------------------------------------- | ------------------------ |
|
|
276
|
+
| `null` | `null` | `''` (shows `emptyPreviewText`) | yes |
|
|
277
|
+
| `string` | `'Acme'`, `''` | `String(value)`; `''` is **not** empty | only `null` for primitives |
|
|
278
|
+
| `number` | `42`, `0` | `String(value)` | no |
|
|
279
|
+
| `boolean` | `true`, `false` | `String(value)` | no |
|
|
280
|
+
| Plain object | `{ label: 'US', value: 'us' }` | `JSON.stringify(value)` | yes when `{}` |
|
|
281
|
+
| Array | `['a', 'b']` | `String(value)` (comma-separated) | yes when `[]` |
|
|
282
|
+
|
|
283
|
+
For human-readable preview (options, booleans, masked passwords), use `#preview` — there is no `previewText` parcel key.
|
|
284
|
+
|
|
285
|
+
`updateDraftValue({ value })` assigns `value ?? null` — passing `undefined` coerces to `null`.
|
|
286
|
+
|
|
287
|
+
`ObjectProcessor` provides deep equality for undo comparison on objects and arrays.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Usage examples
|
|
292
|
+
|
|
293
|
+
### Default text editor
|
|
294
|
+
|
|
295
|
+
From playground — `showActions`, `showUndoChange`, commit via payload:
|
|
296
|
+
|
|
297
|
+
```html
|
|
298
|
+
<FuroEditableField
|
|
299
|
+
:parcel="{
|
|
300
|
+
value: context.textValueRef.value,
|
|
301
|
+
showActions: true,
|
|
302
|
+
showUndoChange: true,
|
|
303
|
+
}"
|
|
304
|
+
name="playground-editable-default"
|
|
305
|
+
@commit-value="context.textValueRef.value = $event.value"
|
|
306
|
+
/>
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Table row with `#editor` (textarea)
|
|
310
|
+
|
|
311
|
+
```html
|
|
312
|
+
<FuroEditableField
|
|
313
|
+
:parcel="{
|
|
314
|
+
value: row.notes,
|
|
315
|
+
showActions: true,
|
|
316
|
+
showUndoChange: true,
|
|
317
|
+
collisionBoundary: context.tableElementRef.value,
|
|
318
|
+
}"
|
|
319
|
+
@commit-value="row.notes = $event.value"
|
|
320
|
+
>
|
|
321
|
+
<template #editor="{ draftValue, updateDraftValue }">
|
|
322
|
+
<FuroTextarea
|
|
323
|
+
:parcel="{ value: draftValue }"
|
|
324
|
+
@change-value="updateDraftValue({ value: $event.value })"
|
|
325
|
+
/>
|
|
326
|
+
</template>
|
|
327
|
+
</FuroEditableField>
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Select with custom preview
|
|
331
|
+
|
|
332
|
+
```html
|
|
333
|
+
<FuroEditableField
|
|
334
|
+
:parcel="{
|
|
335
|
+
value: row.territoryCode,
|
|
336
|
+
showActions: true,
|
|
337
|
+
collisionBoundary: context.tableElementRef.value,
|
|
338
|
+
submitMode: 'none',
|
|
339
|
+
}"
|
|
340
|
+
@commit-value="row.territoryCode = $event.value"
|
|
341
|
+
>
|
|
342
|
+
<template #preview>
|
|
343
|
+
{{ row.formatTerritoryPreview() }}
|
|
344
|
+
</template>
|
|
345
|
+
|
|
346
|
+
<template #editor="{ draftValue, updateDraftValue, submit }">
|
|
347
|
+
<FuroSelect
|
|
348
|
+
:parcel="{ value: draftValue, options: context.territoryOptions }"
|
|
349
|
+
@update:value="updateDraftValue({ value: $event })"
|
|
350
|
+
/>
|
|
351
|
+
</template>
|
|
352
|
+
</FuroEditableField>
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Checkbox — draft sync only; commit on the primitive's submit or explicit `submit()`
|
|
356
|
+
|
|
357
|
+
```html
|
|
358
|
+
<template #editor="{ draftValue, updateDraftValue }">
|
|
359
|
+
<FuroCheckbox
|
|
360
|
+
:parcel="{ value: draftValue }"
|
|
361
|
+
@update:value="updateDraftValue({ value: $event })"
|
|
362
|
+
/>
|
|
363
|
+
</template>
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Non-goals / limitations
|
|
369
|
+
|
|
370
|
+
- **No `v-model:value`** — parent owns `parcel.value`; listen to `@commit-value`.
|
|
371
|
+
- **No `v-model:editing`** — listen to `@update-editing-state` (`'edit' \| 'cancel' \| 'submit'`) if the parent needs edit-mode telemetry.
|
|
372
|
+
- **No per-keystroke Furo emit** — use child field `@change-value` inside `#editor` if the parent needs live draft telemetry.
|
|
373
|
+
- **No `cancel-edit` emit** — listen to the primitive's cancel only via future API; today cancel is silent at the Furo layer.
|
|
374
|
+
- **No built-in `variant` / `editOverlay` modes** — one floating editor positioning strategy.
|
|
375
|
+
- **Default preview is not option-aware** — objects render as JSON; use `#preview`.
|
|
376
|
+
- **Empty string is not “empty”** for preview placeholder — only `null`, `{}`, and `[]` show `emptyPreviewText`.
|
|
377
|
+
- **Do not import** `EditorCollisionProcessor`, `ObjectProcessor`, or the underlying editable primitive in application code.
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Tests
|
|
382
|
+
|
|
383
|
+
- `tests/__tests__/jsdom/lib/components/molecules/FuroEditableField/EditableEmitPayload.js`
|
|
384
|
+
- `tests/__tests__/jsdom/lib/components/molecules/FuroEditableField/FuroEditableFieldContext.js`
|
|
385
|
+
- `tests/__tests__/jsdom/lib/components/molecules/FuroEditableField/processors/EditorCollisionProcessor.js`
|
|
386
|
+
|
|
387
|
+
Run:
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
npm test -- tests/__tests__/jsdom/lib/components/molecules/FuroEditableField/
|
|
391
|
+
```
|