@featherk/ui 0.11.2 → 0.12.1

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.
@@ -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.11.2",
3
+ "version": "0.12.1",
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"