@featherk/ui 0.11.1 → 0.12.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.md +4 -0
- package/dist/__tests__/withSetupHarness.d.ts +9 -0
- package/dist/featherk-ui.es.js +249 -124
- package/dist/featherk-ui.es.js.map +1 -1
- package/dist/featherk-ui.umd.js +1 -1
- package/dist/featherk-ui.umd.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/table/ActionCell.test.d.ts +1 -0
- package/dist/table/ActionCell.vue.d.ts +40 -0
- package/dist/table/index.d.ts +4 -0
- package/dist/table/types.d.ts +40 -0
- package/dist/table/useActionCellMenu.d.ts +71 -0
- package/dist/table/useActionCellMenu.test.d.ts +1 -0
- package/dist/ui.css +1 -1
- package/docs/table/ActionCell.md +118 -0
- package/docs/table/useActionCellMenu.md +175 -0
- package/package.json +18 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# ActionCell
|
|
2
|
+
|
|
3
|
+
[Back to UI README](https://github.com/NantHealth/featherk/blob/integration/packages/ui/README.md)
|
|
4
|
+
|
|
5
|
+
`ActionCell` renders a configured set of repeatable Kendo action buttons. The parent owns one `useActiveIdRegistry`, one `usePopupMenu`, and one shared Kendo `Popup` and `Menu`; ActionCell registers every button with that parent-owned lifecycle.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
1. Define an `ActionCellConfig` with the triggers and item factories used by every row.
|
|
10
|
+
2. Create one `useActiveIdRegistry` and one `usePopupMenu` in the parent.
|
|
11
|
+
3. Pass a controller that creates deterministic trigger IDs and delegates button events to the shared popup lifecycle.
|
|
12
|
+
4. Render one shared Popup/Menu from the parent using the registry's active element and active ID.
|
|
13
|
+
|
|
14
|
+
```vue
|
|
15
|
+
<template>
|
|
16
|
+
<table>
|
|
17
|
+
<tbody>
|
|
18
|
+
<tr v-for="row in rows" :key="row.id">
|
|
19
|
+
<td>
|
|
20
|
+
<!-- Step 3: repeat the trigger container for each data item. -->
|
|
21
|
+
<ActionCell
|
|
22
|
+
:data-item="row"
|
|
23
|
+
:config="config"
|
|
24
|
+
:menu-controller="menuController"
|
|
25
|
+
/>
|
|
26
|
+
</td>
|
|
27
|
+
</tr>
|
|
28
|
+
</tbody>
|
|
29
|
+
</table>
|
|
30
|
+
|
|
31
|
+
<!-- Step 4: exactly one shared popup/menu for the collection. -->
|
|
32
|
+
<Popup :show="registry.isActive.value" :offset="popupMenu.offset.value">
|
|
33
|
+
<Menu ref="menuRef" :items="activeMenuItems" :vertical="true" />
|
|
34
|
+
</Popup>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script setup lang="ts">
|
|
38
|
+
import { computed, ref, type ComponentPublicInstance } from "vue";
|
|
39
|
+
import { Popup } from "@progress/kendo-vue-popup";
|
|
40
|
+
import { Menu } from "@progress/kendo-vue-layout";
|
|
41
|
+
import {
|
|
42
|
+
useActiveIdRegistry,
|
|
43
|
+
usePopupMenu,
|
|
44
|
+
} from "@featherk/composables";
|
|
45
|
+
import {
|
|
46
|
+
ActionCell,
|
|
47
|
+
type ActionCellConfig,
|
|
48
|
+
type ActionCellMenuController,
|
|
49
|
+
} from "@featherk/ui";
|
|
50
|
+
|
|
51
|
+
type Row = { id: number; name: string };
|
|
52
|
+
const rows = ref<Row[]>([]);
|
|
53
|
+
|
|
54
|
+
// Step 1: describe every trigger rendered by a repeated ActionCell.
|
|
55
|
+
const config: ActionCellConfig<Row> = {
|
|
56
|
+
triggers: [
|
|
57
|
+
{
|
|
58
|
+
key: "patient",
|
|
59
|
+
icon: patientIcon,
|
|
60
|
+
ariaLabel: "Patient actions",
|
|
61
|
+
items: (row) => [{ id: "view", text: `View ${row.name}` }],
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Step 2: own one registry and popup lifecycle for the entire collection.
|
|
67
|
+
const registry = useActiveIdRegistry<string>();
|
|
68
|
+
const menuRef = ref<ComponentPublicInstance | null>(null);
|
|
69
|
+
const popupMenu = usePopupMenu({
|
|
70
|
+
isOpen: registry.isActive,
|
|
71
|
+
triggerRef: registry.activeElement,
|
|
72
|
+
menuRef,
|
|
73
|
+
triggerMode: "button",
|
|
74
|
+
anchor: true,
|
|
75
|
+
requestShow: (id) => {
|
|
76
|
+
if (typeof id === "string") registry.activate(id);
|
|
77
|
+
},
|
|
78
|
+
requestHide: () => registry.deactivate(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const menuController: ActionCellMenuController<Row> = {
|
|
82
|
+
registry,
|
|
83
|
+
getTriggerId: (trigger, row) => `${trigger.key}:${row.id}`,
|
|
84
|
+
handleTriggerClick: (id) => popupMenu.handleActionButtonClick(id),
|
|
85
|
+
handleTriggerKeydown: (id, ...args) =>
|
|
86
|
+
popupMenu.handleActionButtonKeydown(args[0] as KeyboardEvent, id),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const activeMenuItems = computed(() => []);
|
|
90
|
+
</script>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`useCompositeId()` supplies component-local DOM IDs for ActionCell buttons. Do not use those IDs for the shared registry: `menuController.getTriggerId` must return a deterministic identity derived from the trigger and row, such as `patient:42`.
|
|
94
|
+
|
|
95
|
+
## Keyboard Navigation
|
|
96
|
+
|
|
97
|
+
`ActionCell` defaults each trigger button to `tabindex="-1"`, preserving the roving-focus
|
|
98
|
+
model used by Kendo UI for Vue Grid with `useGridA11y`. When using `ActionCell` in a standard
|
|
99
|
+
HTML table or another context where the action buttons should participate in sequential Tab
|
|
100
|
+
navigation, pass `:tab-index="0"`:
|
|
101
|
+
|
|
102
|
+
```vue
|
|
103
|
+
<ActionCell
|
|
104
|
+
:data-item="row"
|
|
105
|
+
:tab-index="0"
|
|
106
|
+
:config="config"
|
|
107
|
+
:menu-controller="menuController"
|
|
108
|
+
/>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
This is a component-level focus policy and is intentionally separate from
|
|
112
|
+
`ActionCellConfig`, which describes the actions themselves.
|
|
113
|
+
|
|
114
|
+
## Types
|
|
115
|
+
|
|
116
|
+
- `ActionCellConfig<TData>`: stable configuration object containing `triggers`.
|
|
117
|
+
- `ActionCellTrigger<TData>`: defines `key`, `icon`, `ariaLabel`, optional `items(dataItem)`, and optional `disabled(dataItem)`.
|
|
118
|
+
- `ActionCellMenuController<TData>`: parent-owned registry, trigger-ID resolver, and popup click/keyboard delegation contract.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# useActionCellMenu
|
|
2
|
+
|
|
3
|
+
[Back to UI README](https://github.com/NantHealth/featherk/blob/integration/packages/ui/README.md)
|
|
4
|
+
|
|
5
|
+
Bundles the shared-popup wiring a parent needs to drive `ActionCell`: one `useActiveIdRegistry`,
|
|
6
|
+
one `usePopupMenu`, and the derived active row/trigger/menu items. The parent still owns the
|
|
7
|
+
`<Popup>`/`<Menu>` template elements and any business logic for the selected action.
|
|
8
|
+
|
|
9
|
+
## Why This Lives in `@featherk/ui`, Not `@featherk/composables`
|
|
10
|
+
|
|
11
|
+
Every other composable in `@featherk/composables` is generic: it improves or augments Kendo UI
|
|
12
|
+
for Vue behavior without being tied to any specific `@featherk/ui` component. `useActionCellMenu`
|
|
13
|
+
is the one exception, and deliberately so — it exists *only* to drive `ActionCell`. Its
|
|
14
|
+
`actionCellProps` return value is shaped exactly as `ActionCell`'s props (`config`,
|
|
15
|
+
`menuController`), and it has no purpose without that component.
|
|
16
|
+
|
|
17
|
+
`useActionCellMenu` was first prototyped inside `@featherk/composables` (see
|
|
18
|
+
[docs/design/action-cell-dx-follow-up.md](https://github.com/NantHealth/featherk/blob/integration/docs/design/action-cell-dx-follow-up.md)),
|
|
19
|
+
because at the time `@featherk/composables` could not depend on `@featherk/ui` — the
|
|
20
|
+
dependency direction is `@featherk/ui` → `@featherk/composables`, never the reverse. That meant
|
|
21
|
+
the composable had to re-declare structurally-compatible `Like` types
|
|
22
|
+
(`ActionCellConfigLike`/`ActionCellTriggerLike`/`ActionCellMenuControllerLike`) instead of
|
|
23
|
+
importing `ActionCell`'s real `ActionCellConfig`/`ActionCellTrigger`/`ActionCellMenuController`
|
|
24
|
+
types, relying on TypeScript's structural typing to keep them assignable. That duplication was a
|
|
25
|
+
real maintenance risk: adding a field to the real type that this composable's logic needed to
|
|
26
|
+
read did not fail to compile if the `Like` type wasn't updated to match — it would just silently
|
|
27
|
+
be ignored.
|
|
28
|
+
|
|
29
|
+
Moving `useActionCellMenu` here removes that risk entirely. `@featherk/ui` already depends on
|
|
30
|
+
`@featherk/composables` (for `useActiveIdRegistry` and `usePopupMenu`), so this composable can
|
|
31
|
+
import the real `ActionCellConfig`/`ActionCellTrigger`/`ActionCellMenuController`/
|
|
32
|
+
`ActionCellMenuItem` types from [`./types`](https://github.com/NantHealth/featherk/blob/integration/packages/ui/src/table/types.ts)
|
|
33
|
+
directly — no duplicated types, no drift risk, and the coupling to `ActionCell` is now explicit
|
|
34
|
+
in the package boundary instead of implicit in a doc comment.
|
|
35
|
+
|
|
36
|
+
`@featherk/composables` keeps `useActiveIdRegistry` and `usePopupMenu` as its own generic,
|
|
37
|
+
component-agnostic building blocks; this composable is the `ActionCell`-specific convenience
|
|
38
|
+
layer built on top of them, and belongs next to the component it drives.
|
|
39
|
+
|
|
40
|
+
## Why use it
|
|
41
|
+
|
|
42
|
+
`ActionCell` renders repeatable trigger buttons but intentionally owns no popup state itself
|
|
43
|
+
— the parent supplies a `config` and a `menuController`. Wiring that controller by hand means
|
|
44
|
+
authoring a `useActiveIdRegistry` registry, a `usePopupMenu` instance, `getTriggerId`, active row/trigger/menu-item
|
|
45
|
+
computeds, click/keydown delegation, and menu select/escape handlers for every consumer.
|
|
46
|
+
`useActionCellMenu` packages all of that into a single call, while still leaving the actual
|
|
47
|
+
`<Popup>`/`<Menu>` markup and any row-specific business logic (`onActionSelected`) with the
|
|
48
|
+
parent.
|
|
49
|
+
|
|
50
|
+
This does not replace `useActiveIdRegistry` or `usePopupMenu` — it wraps them for the
|
|
51
|
+
specific `ActionCell` shared-instance shape. Reach for the lower-level composables directly
|
|
52
|
+
when driving a popup/menu that isn't backed by `ActionCell` (e.g. a row-level context menu).
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
1. Define an `ActionCellConfig` and pass it, your row list, and a `getRowId` to
|
|
57
|
+
`useActionCellMenu`.
|
|
58
|
+
2. Create a `menuRef` for the single shared Kendo `Menu` instance.
|
|
59
|
+
3. Spread `actionCellProps` onto every `<ActionCell>` instance.
|
|
60
|
+
4. Spread `popupProps`/`menuProps` onto the single shared `<Popup>`/`<Menu>`.
|
|
61
|
+
5. Handle the selected action in `onActionSelected`, which already receives the resolved row
|
|
62
|
+
and trigger.
|
|
63
|
+
|
|
64
|
+
```vue
|
|
65
|
+
<template>
|
|
66
|
+
<!-- Step 3 -->
|
|
67
|
+
<ActionCell
|
|
68
|
+
v-for="row in rows"
|
|
69
|
+
:key="row.id"
|
|
70
|
+
:data-item="row"
|
|
71
|
+
v-bind="actionCellMenu.actionCellProps"
|
|
72
|
+
/>
|
|
73
|
+
|
|
74
|
+
<!-- Step 4 -->
|
|
75
|
+
<Popup v-bind="actionCellMenu.popupProps.value">
|
|
76
|
+
<strong v-if="actionCellMenu.activeTitle.value">
|
|
77
|
+
{{ actionCellMenu.activeTitle.value }}
|
|
78
|
+
</strong>
|
|
79
|
+
<Menu ref="menuRef" v-bind="actionCellMenu.menuProps.value" />
|
|
80
|
+
</Popup>
|
|
81
|
+
</template>
|
|
82
|
+
|
|
83
|
+
<script setup lang="ts">
|
|
84
|
+
import { ref } from "vue";
|
|
85
|
+
import { ActionCell, useActionCellMenu, type ActionCellConfig } from "@featherk/ui";
|
|
86
|
+
|
|
87
|
+
// Step 2
|
|
88
|
+
const menuRef = ref(null);
|
|
89
|
+
|
|
90
|
+
// Step 1
|
|
91
|
+
const actionCellMenu = useActionCellMenu({
|
|
92
|
+
config: actionCellConfig, // ActionCellConfig<Row>
|
|
93
|
+
rows,
|
|
94
|
+
getRowId: (row) => row.id,
|
|
95
|
+
menuRef,
|
|
96
|
+
onActionSelected: (action, row, trigger) => {
|
|
97
|
+
// Step 5: row-specific business logic
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
</script>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## API
|
|
104
|
+
|
|
105
|
+
| Option | Required | Description |
|
|
106
|
+
| -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
107
|
+
| `config` | Yes | `ActionCellConfig`-shaped trigger configuration, passed straight through to every `ActionCell`. |
|
|
108
|
+
| `rows` | Yes | Current row list (ref, getter, or plain array); used to resolve the active row from its registry id. |
|
|
109
|
+
| `getRowId` | Yes | Derives a stable row id used to build the default `"<trigger.key>:<rowId>"` registry id. |
|
|
110
|
+
| `getTriggerId` | No | Overrides registry ID formatting, but must preserve `<trigger.key>:<rowId>` semantics because active row and trigger resolution parse the ID. |
|
|
111
|
+
| `menuRef` | Yes | Ref to the single shared Kendo `Menu` instance. |
|
|
112
|
+
| `triggerMode` | No | Forwarded to `usePopupMenu`. Defaults to `"button"`. |
|
|
113
|
+
| `anchor` | No | Forwarded to `usePopupMenu` for offset positioning. Defaults to `true` because the shared popup targets dynamically resolved ActionCell triggers. |
|
|
114
|
+
| `resolveFocusTarget` | No | Forwarded to `usePopupMenu` for keyboard-close focus restoration. |
|
|
115
|
+
| `coexistsWith` | No | Sibling `useActiveIdRegistry` instances (e.g. a hand-rolled row-context menu) to deactivate whenever a trigger in this collection opens. |
|
|
116
|
+
| `onActionSelected` | Yes | Runs after a menu item is selected, with the resolved `(action, row, trigger)`. |
|
|
117
|
+
|
|
118
|
+
Each trigger in `config.triggers` may also define an optional `title?: (dataItem) => string`.
|
|
119
|
+
When set, the resolved string is exposed via the returned `activeTitle` — it is *not* mixed
|
|
120
|
+
into `menuProps.items`, since `useActionCellMenu` never renders markup and can't dictate how
|
|
121
|
+
a consumer's `<Menu>` implementation would render a fake "title" item. Render `activeTitle`
|
|
122
|
+
yourself as real markup (e.g. `<strong v-if="activeTitle">{{ activeTitle }}</strong>`) as a
|
|
123
|
+
sibling of `<Menu>` inside the `<Popup>`.
|
|
124
|
+
|
|
125
|
+
## Return shape
|
|
126
|
+
|
|
127
|
+
| Property | Description |
|
|
128
|
+
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
129
|
+
| `actionCellProps` | `{ config, menuController }` — spread onto every `<ActionCell>`. |
|
|
130
|
+
| `popupProps` | Computed props (`show`, `offset`, `onClose`) — spread onto the shared `<Popup>`. |
|
|
131
|
+
| `menuProps` | Computed props (`items`, `vertical`, `onKeydown`, `onSelect`) — spread onto the shared `<Menu>`. |
|
|
132
|
+
| `activeRow` | The row resolved from the currently active trigger, or `null`. |
|
|
133
|
+
| `activeTrigger` | The trigger config resolved from the currently active trigger, or `null`. |
|
|
134
|
+
| `activeTitle` | Resolved `trigger.title(activeRow)`, or `null` when unset/nothing is active. Render it yourself — it is not part of `menuProps.items`. |
|
|
135
|
+
|
|
136
|
+
### Trigger ID Constraint
|
|
137
|
+
|
|
138
|
+
The default registry ID is `${trigger.key}:${getRowId(row)}`. A custom `getTriggerId` may
|
|
139
|
+
change the formatting around that value, but it must preserve the same meaning: the first
|
|
140
|
+
colon-separated segment must be the trigger key, and the remaining segments must reconstruct
|
|
141
|
+
the row ID. `useActionCellMenu` parses the active ID to resolve `activeTrigger` and `activeRow`.
|
|
142
|
+
|
|
143
|
+
For example, this is compatible:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
getTriggerId: (trigger, row) => `${trigger.key}:row:${row.id}`
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
because the composable resolves `trigger.key` from the first segment and `row:${row.id}` as
|
|
150
|
+
the row ID. An opaque format such as `action-${row.id}-${trigger.key}` is not compatible with
|
|
151
|
+
the current implementation and will cause `activeRow`/`activeTrigger` to resolve to `null`.
|
|
152
|
+
|
|
153
|
+
Supporting genuinely opaque custom IDs would require a future implementation change: maintain
|
|
154
|
+
a lookup from each generated registry ID to its `{ row, trigger }` pair rather than deriving
|
|
155
|
+
those values by parsing the ID. That would remove this formatting constraint, but would also
|
|
156
|
+
require defining lookup lifecycle/re-render behavior before changing the public contract.
|
|
157
|
+
|
|
158
|
+
## Notes
|
|
159
|
+
|
|
160
|
+
- Reuses `@featherk/composables`'s `usePopupMenu`/`useActiveIdRegistry` mechanics as a
|
|
161
|
+
hand-rolled shared instance would; it does not introduce a new popup/menu implementation.
|
|
162
|
+
- Only manages `ActionCell`'s button-mode triggers. A row-level context menu alongside an
|
|
163
|
+
`ActionCell` column still needs its own `useActiveIdRegistry` + `usePopupMenu` call with
|
|
164
|
+
`triggerMode: "row"` — see
|
|
165
|
+
[ActionCellGridDemo.vue](https://github.com/NantHealth/featherk/blob/integration/demos/src/views/ui/ActionCellGridDemo.vue)
|
|
166
|
+
for a worked example of the two coexisting.
|
|
167
|
+
- **Coexisting with a sibling menu (e.g. a row-level context menu) does not give you mutual
|
|
168
|
+
exclusivity for free.** Each `usePopupMenu` instance only closes on outside clicks or its
|
|
169
|
+
own trigger; a click on this row's `ActionCell` button is *inside* a sibling row-menu's
|
|
170
|
+
ignored trigger element (the `<tr>`), so the sibling's own outside-click handling never
|
|
171
|
+
fires for it, and vice versa. Pass the sibling's registry via `coexistsWith` to close it
|
|
172
|
+
automatically whenever an `ActionCell` trigger opens — this covers one direction only. The
|
|
173
|
+
sibling must still deactivate *this* composable's registry (exposed via
|
|
174
|
+
`actionCellProps.menuController.registry`) when it activates; see `ActionCellGridDemo.vue`
|
|
175
|
+
linked above for that remaining call site.
|
package/package.json
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@featherk/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"main": "dist/featherk-ui.umd.js",
|
|
5
5
|
"module": "dist/featherk-ui.es.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/featherk-ui.es.js",
|
|
11
|
+
"require": "./dist/featherk-ui.umd.js"
|
|
12
|
+
},
|
|
13
|
+
"./table": {
|
|
14
|
+
"types": "./dist/table/index.d.ts",
|
|
15
|
+
"import": "./dist/featherk-ui.es.js",
|
|
16
|
+
"require": "./dist/featherk-ui.umd.js"
|
|
17
|
+
},
|
|
18
|
+
"./address": {
|
|
19
|
+
"types": "./dist/address/index.d.ts",
|
|
20
|
+
"import": "./dist/featherk-ui.es.js",
|
|
21
|
+
"require": "./dist/featherk-ui.umd.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
7
24
|
"files": [
|
|
8
25
|
"dist",
|
|
9
26
|
"docs"
|