@usefy/use-selection 0.19.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 +164 -0
- package/dist/index.d.mts +205 -0
- package/dist/index.d.ts +205 -0
- package/dist/index.js +163 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +136 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/mirunamu00/usefy/master/assets/logo.png" alt="usefy logo" width="120" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">@usefy/use-selection</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Multi/single selection state for lists and tables — Set-based, checkbox-ready</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/@usefy/use-selection"><img src="https://img.shields.io/npm/v/@usefy/use-selection.svg?style=flat-square&color=007acc" alt="npm version" /></a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/@usefy/use-selection"><img src="https://img.shields.io/npm/dm/@usefy/use-selection.svg?style=flat-square&color=007acc" alt="npm downloads" /></a>
|
|
14
|
+
<a href="https://bundlephobia.com/package/@usefy/use-selection"><img src="https://img.shields.io/bundlephobia/minzip/@usefy/use-selection?style=flat-square&color=007acc" alt="bundle size" /></a>
|
|
15
|
+
<a href="https://github.com/mirunamu00/usefy/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/@usefy/use-selection.svg?style=flat-square&color=007acc" alt="license" /></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="#installation">Installation</a> •
|
|
20
|
+
<a href="#quick-start">Quick Start</a> •
|
|
21
|
+
<a href="#api">API</a> •
|
|
22
|
+
<a href="#testing">Testing</a> •
|
|
23
|
+
<a href="#license">License</a>
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<a href="https://mirunamu00.github.io/usefy/?path=/docs/hooks-useselection--docs" target="_blank" rel="noopener noreferrer">
|
|
28
|
+
<strong>📚 View Storybook Demo</strong>
|
|
29
|
+
</a>
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Overview
|
|
35
|
+
|
|
36
|
+
`useSelection` is part of the [@usefy](https://www.npmjs.com/org/usefy) ecosystem — a collection of production-ready, TypeScript-first, SSR-safe React hooks. It manages which items from a list or table are selected, backed by a `Set` of keys, with everything a checkbox UI needs: per-row selection, a header "select all" with indeterminate state, and an optional single-selection mode.
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **Set-backed** — stores **keys** (via `getKey`, default identity), so a selection survives new item identities across renders
|
|
41
|
+
- **Ergonomic surface** — `selected` items, `isSelected`, `toggle`, `select`, `deselect`, `selectAll`, `clear`
|
|
42
|
+
- **Header-checkbox ready** — `isAllSelected` / `isPartiallySelected` / `isNoneSelected` for the indeterminate state
|
|
43
|
+
- **Single-selection mode** — `multiple: false` replaces the selection on select (radio-like)
|
|
44
|
+
- **Auto-reconciled** — item-facing values derive from the current `items`; removed rows drop out automatically
|
|
45
|
+
- **Stable & efficient** — stable action identities; no-op `selectAll`/`clear` skip re-renders; immutable Set updates
|
|
46
|
+
- **SSR-safe & StrictMode-safe** — pure state, no mutation of prior state
|
|
47
|
+
- **TypeScript-first** — full type inference and exported types
|
|
48
|
+
- **Tiny & tree-shakeable** — zero dependencies, published as its own package
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# npm
|
|
54
|
+
npm install @usefy/use-selection
|
|
55
|
+
|
|
56
|
+
# yarn
|
|
57
|
+
yarn add @usefy/use-selection
|
|
58
|
+
|
|
59
|
+
# pnpm
|
|
60
|
+
pnpm add @usefy/use-selection
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Requires React 18 or 19 (`peerDependencies: "react": "^18.0.0 || ^19.0.0"`).
|
|
64
|
+
|
|
65
|
+
## Quick Start
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { useSelection } from "@usefy/use-selection";
|
|
69
|
+
|
|
70
|
+
type User = { id: number; name: string };
|
|
71
|
+
|
|
72
|
+
function UserTable({ users }: { users: User[] }) {
|
|
73
|
+
const {
|
|
74
|
+
selected,
|
|
75
|
+
isSelected,
|
|
76
|
+
toggle,
|
|
77
|
+
selectAll,
|
|
78
|
+
clear,
|
|
79
|
+
isAllSelected,
|
|
80
|
+
isPartiallySelected,
|
|
81
|
+
} = useSelection(users, { getKey: (u) => u.id });
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<table>
|
|
85
|
+
<thead>
|
|
86
|
+
<tr>
|
|
87
|
+
<th>
|
|
88
|
+
<input
|
|
89
|
+
type="checkbox"
|
|
90
|
+
checked={isAllSelected}
|
|
91
|
+
ref={(el) => {
|
|
92
|
+
if (el) el.indeterminate = isPartiallySelected;
|
|
93
|
+
}}
|
|
94
|
+
onChange={() => (isAllSelected ? clear() : selectAll())}
|
|
95
|
+
/>
|
|
96
|
+
</th>
|
|
97
|
+
<th>Name ({selected.length} selected)</th>
|
|
98
|
+
</tr>
|
|
99
|
+
</thead>
|
|
100
|
+
<tbody>
|
|
101
|
+
{users.map((user) => (
|
|
102
|
+
<tr key={user.id}>
|
|
103
|
+
<td>
|
|
104
|
+
<input
|
|
105
|
+
type="checkbox"
|
|
106
|
+
checked={isSelected(user)}
|
|
107
|
+
onChange={() => toggle(user)}
|
|
108
|
+
/>
|
|
109
|
+
</td>
|
|
110
|
+
<td>{user.name}</td>
|
|
111
|
+
</tr>
|
|
112
|
+
))}
|
|
113
|
+
</tbody>
|
|
114
|
+
</table>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## API
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const result = useSelection<T>(items: T[], options?: UseSelectionOptions<T>);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Options — `UseSelectionOptions<T>`
|
|
126
|
+
|
|
127
|
+
| Option | Type | Default | Description |
|
|
128
|
+
| --- | --- | --- | --- |
|
|
129
|
+
| `getKey` | `(item: T) => string \| number` | identity (`(item) => item`) | Derives the selection key for an item. The `Set` stores these keys, not items, so selection survives new object references. **Provide this for object items** (e.g. `(row) => row.id`); the identity default is only correct for primitive items. |
|
|
130
|
+
| `multiple` | `boolean` | `true` | `true` = multi-selection; `false` = single-selection (selecting replaces the current choice, and `selectAll` is a no-op). |
|
|
131
|
+
|
|
132
|
+
### Return — `UseSelectionReturn<T>`
|
|
133
|
+
|
|
134
|
+
| Property | Type | Description |
|
|
135
|
+
| --- | --- | --- |
|
|
136
|
+
| `selected` | `T[]` | The selected items, in `items` order. Derived as `items ∩ selectedKeys` — items removed from `items` drop out automatically. |
|
|
137
|
+
| `selectedKeys` | `ReadonlySet<string \| number>` | The raw set of selected keys (source of truth). May contain keys for items no longer in `items`; those are invisible in the derived values. |
|
|
138
|
+
| `isSelected` | `(item: T) => boolean` | Whether an item is selected (by key). Stable. |
|
|
139
|
+
| `toggle` | `(item: T) => void` | Flip an item's selection. In single mode, selecting a new item replaces the previous one. Stable. |
|
|
140
|
+
| `select` | `(item: T) => void` | Select an item (idempotent). In single mode, replaces the selection. Stable. |
|
|
141
|
+
| `deselect` | `(item: T) => void` | Deselect an item (idempotent). Stable. |
|
|
142
|
+
| `selectAll` | `() => void` | Select every item in `items`. No-op when all are already selected, or in single mode. Stable. |
|
|
143
|
+
| `clear` | `() => void` | Clear the whole selection (deselect all). No-op when already empty. Stable. |
|
|
144
|
+
| `isAllSelected` | `boolean` | `true` when every item is selected. **Always `false` for an empty `items` array.** |
|
|
145
|
+
| `isPartiallySelected` | `boolean` | `true` when some — but not all — items are selected (drives an indeterminate header checkbox). `false` for empty `items`. |
|
|
146
|
+
| `isNoneSelected` | `boolean` | `true` when no items are selected (including when `items` is empty). |
|
|
147
|
+
|
|
148
|
+
### Behavior notes
|
|
149
|
+
|
|
150
|
+
- **Selection key.** The `Set` stores **keys**, never items. This is what makes a selection stable when `items` is rebuilt with fresh object references each render — as long as `getKey` maps the same logical item to the same key. For primitive `items` (`string`/`number`) the identity default just works.
|
|
151
|
+
- **Items-change reconciliation.** `selectedKeys` is the source of truth; every item-facing value is **derived from the current `items`**. Removing a selected row from `items` makes it disappear from `selected` and recomputes `isAllSelected`/`isPartiallySelected` automatically — no manual pruning needed. The removed key stays in `selectedKeys` (harmless and invisible) so it re-appears if the item comes back.
|
|
152
|
+
- **Empty list.** `isAllSelected` is `false` for an empty `items` array (there is nothing to have "all" selected); `isNoneSelected` is `true`.
|
|
153
|
+
- **Single-selection mode.** With `multiple: false`, `select`/`toggle` of a new item replace the current selection; toggling the already-selected item deselects it. `selectAll` is a no-op (you cannot hold more than one selection).
|
|
154
|
+
- **StrictMode / concurrency.** Updates are immutable (a fresh `Set` on every change) and no user callback runs inside a `setState` updater, so it is safe under StrictMode and concurrent rendering.
|
|
155
|
+
|
|
156
|
+
## Testing
|
|
157
|
+
|
|
158
|
+
📊 <a href="https://mirunamu00.github.io/usefy/coverage/use-selection/src/index.html" target="_blank" rel="noopener noreferrer"><strong>View Detailed Coverage Report</strong></a> (GitHub Pages) — **24 tests**, 100% statement coverage.
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT © [mirunamu](https://github.com/mirunamu00)
|
|
163
|
+
|
|
164
|
+
This package is part of the [usefy](https://github.com/mirunamu00/usefy) monorepo.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The primitive key a selection is tracked by. Items may be objects, so
|
|
3
|
+
* {@link useSelection} never stores items directly — it stores a stable
|
|
4
|
+
* key derived from each item (see {@link UseSelectionOptions.getKey}). Keys are
|
|
5
|
+
* `string | number` so they compare by value inside the backing `Set`, which is
|
|
6
|
+
* what makes a selection survive new object identities across renders.
|
|
7
|
+
*/
|
|
8
|
+
type SelectionKey = string | number;
|
|
9
|
+
/**
|
|
10
|
+
* Options for {@link useSelection}.
|
|
11
|
+
*
|
|
12
|
+
* @template T - The item type.
|
|
13
|
+
*/
|
|
14
|
+
interface UseSelectionOptions<T> {
|
|
15
|
+
/**
|
|
16
|
+
* Derive the selection key for an item. The backing `Set` stores these keys,
|
|
17
|
+
* not the items themselves, so selection is stable even when the `items`
|
|
18
|
+
* array is rebuilt with new object references each render (as long as
|
|
19
|
+
* `getKey` returns the same key for the same logical item).
|
|
20
|
+
*
|
|
21
|
+
* Defaults to identity (`(item) => item`), which is correct when `items` are
|
|
22
|
+
* primitives (`string`/`number`). For object items you should provide a
|
|
23
|
+
* `getKey` such as `(row) => row.id`.
|
|
24
|
+
*/
|
|
25
|
+
getKey?: (item: T) => SelectionKey;
|
|
26
|
+
/**
|
|
27
|
+
* Whether more than one item can be selected at a time.
|
|
28
|
+
*
|
|
29
|
+
* - `true` (default) — multi-selection. `select`/`toggle` add to the set.
|
|
30
|
+
* - `false` — single-selection. Selecting an item replaces the current
|
|
31
|
+
* selection; toggling the already-selected item deselects it. `selectAll`
|
|
32
|
+
* is a no-op in this mode (you cannot hold more than one selection).
|
|
33
|
+
*
|
|
34
|
+
* @defaultValue true
|
|
35
|
+
*/
|
|
36
|
+
multiple?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Return value of {@link useSelection}.
|
|
40
|
+
*
|
|
41
|
+
* The backing `Set` of keys is the source of truth, but every item-facing value
|
|
42
|
+
* (`selected`, `isAllSelected`, …) is **derived from the current `items`**. This
|
|
43
|
+
* means keys for items no longer present in `items` simply stop appearing in
|
|
44
|
+
* `selected` and stop counting toward `isAllSelected` — stale selections never
|
|
45
|
+
* linger in the item-facing surface.
|
|
46
|
+
*
|
|
47
|
+
* @template T - The item type.
|
|
48
|
+
*/
|
|
49
|
+
interface UseSelectionReturn<T> {
|
|
50
|
+
/**
|
|
51
|
+
* The currently selected items, in `items` order. Derived as
|
|
52
|
+
* `items` ∩ `selectedKeys`, so items removed from `items` drop out
|
|
53
|
+
* automatically. Ideal for rendering a "selected" summary.
|
|
54
|
+
*
|
|
55
|
+
* Read-only: it is a freshly derived array — use the actions to change the
|
|
56
|
+
* selection rather than mutating this in place.
|
|
57
|
+
*/
|
|
58
|
+
selected: readonly T[];
|
|
59
|
+
/**
|
|
60
|
+
* The raw set of selected keys — the source of truth. Read-only: use the
|
|
61
|
+
* actions to mutate. May contain keys for items not currently in `items`
|
|
62
|
+
* (those are simply invisible in the derived, item-facing values).
|
|
63
|
+
*/
|
|
64
|
+
selectedKeys: ReadonlySet<SelectionKey>;
|
|
65
|
+
/**
|
|
66
|
+
* Whether a given item is currently selected (compared by key). Stable across
|
|
67
|
+
* renders and always reflects the latest state.
|
|
68
|
+
*/
|
|
69
|
+
isSelected: (item: T) => boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Toggle a single item's selection. If it is selected it becomes deselected;
|
|
72
|
+
* otherwise it becomes selected. In single-selection mode, selecting a new
|
|
73
|
+
* item replaces the previous selection. Stable identity.
|
|
74
|
+
*/
|
|
75
|
+
toggle: (item: T) => void;
|
|
76
|
+
/**
|
|
77
|
+
* Explicitly select an item (idempotent — selecting an already-selected item
|
|
78
|
+
* is a no-op). In single-selection mode this replaces the current selection.
|
|
79
|
+
* Stable identity.
|
|
80
|
+
*/
|
|
81
|
+
select: (item: T) => void;
|
|
82
|
+
/**
|
|
83
|
+
* Explicitly deselect an item (idempotent — deselecting an item that is not
|
|
84
|
+
* selected is a no-op). Stable identity.
|
|
85
|
+
*/
|
|
86
|
+
deselect: (item: T) => void;
|
|
87
|
+
/**
|
|
88
|
+
* Select every item currently in `items`. No-op when everything is already
|
|
89
|
+
* selected (no re-render). No-op in single-selection mode. Stable identity.
|
|
90
|
+
*/
|
|
91
|
+
selectAll: () => void;
|
|
92
|
+
/**
|
|
93
|
+
* Clear the entire selection (a.k.a. deselect all). No-op when the selection
|
|
94
|
+
* is already empty (no re-render). Stable identity.
|
|
95
|
+
*/
|
|
96
|
+
clear: () => void;
|
|
97
|
+
/**
|
|
98
|
+
* `true` when every item in `items` is selected. Always `false` for an empty
|
|
99
|
+
* `items` array (there is nothing to have "all" selected).
|
|
100
|
+
*/
|
|
101
|
+
isAllSelected: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* `true` when some — but not all — of the current `items` are selected. Useful
|
|
104
|
+
* to drive a header checkbox's `indeterminate` state. `false` for an empty
|
|
105
|
+
* `items` array.
|
|
106
|
+
*/
|
|
107
|
+
isPartiallySelected: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* `true` when none of the current `items` are selected (including when `items`
|
|
110
|
+
* is empty).
|
|
111
|
+
*/
|
|
112
|
+
isNoneSelected: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Manage multi- or single-selection state for a list or table, backed by a
|
|
117
|
+
* `Set` of keys.
|
|
118
|
+
*
|
|
119
|
+
* The `Set` stores **keys** (see the `getKey` option), never the items
|
|
120
|
+
* themselves, so a selection survives new object identities across renders —
|
|
121
|
+
* rebuilding the `items` array every render will not lose the selection as long
|
|
122
|
+
* as `getKey` maps the same logical item to the same key. Every item-facing
|
|
123
|
+
* value (`selected`, `isAllSelected`, `isPartiallySelected`, …) is **derived
|
|
124
|
+
* from the current `items`**, so removing a selected row from `items` makes it
|
|
125
|
+
* disappear from `selected` and recomputes the aggregate flags automatically —
|
|
126
|
+
* no manual reconciliation needed.
|
|
127
|
+
*
|
|
128
|
+
* Features:
|
|
129
|
+
* - `Set`-backed, immutable updates (a fresh `Set` on every change)
|
|
130
|
+
* - No-op skipping — `selectAll` when all are already selected, and `clear`
|
|
131
|
+
* when already empty, bail out without a re-render
|
|
132
|
+
* - Single-selection mode (`multiple: false`) that replaces the selection
|
|
133
|
+
* - Stable action identities (`toggle`/`select`/`deselect`/`selectAll`/`clear`
|
|
134
|
+
* and `isSelected`) — safe as effect dependencies
|
|
135
|
+
* - `isPartiallySelected` for a header checkbox's indeterminate state
|
|
136
|
+
* - SSR-safe (pure state) and StrictMode/concurrent-safe (no mutation of prior
|
|
137
|
+
* state, callbacks never fire from inside a `setState` updater)
|
|
138
|
+
*
|
|
139
|
+
* @template T - The item type.
|
|
140
|
+
* @param items - The current list of items the selection is scoped to.
|
|
141
|
+
* @param options - {@link UseSelectionOptions} (`getKey`, `multiple`).
|
|
142
|
+
* @returns {@link UseSelectionReturn}
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```tsx
|
|
146
|
+
* type User = { id: number; name: string };
|
|
147
|
+
*
|
|
148
|
+
* function UserTable({ users }: { users: User[] }) {
|
|
149
|
+
* const {
|
|
150
|
+
* selected,
|
|
151
|
+
* isSelected,
|
|
152
|
+
* toggle,
|
|
153
|
+
* selectAll,
|
|
154
|
+
* clear,
|
|
155
|
+
* isAllSelected,
|
|
156
|
+
* isPartiallySelected,
|
|
157
|
+
* } = useSelection(users, { getKey: (u) => u.id });
|
|
158
|
+
*
|
|
159
|
+
* return (
|
|
160
|
+
* <table>
|
|
161
|
+
* <thead>
|
|
162
|
+
* <tr>
|
|
163
|
+
* <th>
|
|
164
|
+
* <input
|
|
165
|
+
* type="checkbox"
|
|
166
|
+
* checked={isAllSelected}
|
|
167
|
+
* ref={(el) => {
|
|
168
|
+
* if (el) el.indeterminate = isPartiallySelected;
|
|
169
|
+
* }}
|
|
170
|
+
* onChange={() => (isAllSelected ? clear() : selectAll())}
|
|
171
|
+
* />
|
|
172
|
+
* </th>
|
|
173
|
+
* <th>Name ({selected.length} selected)</th>
|
|
174
|
+
* </tr>
|
|
175
|
+
* </thead>
|
|
176
|
+
* <tbody>
|
|
177
|
+
* {users.map((user) => (
|
|
178
|
+
* <tr key={user.id}>
|
|
179
|
+
* <td>
|
|
180
|
+
* <input
|
|
181
|
+
* type="checkbox"
|
|
182
|
+
* checked={isSelected(user)}
|
|
183
|
+
* onChange={() => toggle(user)}
|
|
184
|
+
* />
|
|
185
|
+
* </td>
|
|
186
|
+
* <td>{user.name}</td>
|
|
187
|
+
* </tr>
|
|
188
|
+
* ))}
|
|
189
|
+
* </tbody>
|
|
190
|
+
* </table>
|
|
191
|
+
* );
|
|
192
|
+
* }
|
|
193
|
+
* ```
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```tsx
|
|
197
|
+
* // Single-selection mode (radio-like): selecting replaces the previous choice.
|
|
198
|
+
* const { selected, isSelected, select } = useSelection(options, {
|
|
199
|
+
* multiple: false,
|
|
200
|
+
* });
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
declare function useSelection<T>(items: T[], options?: UseSelectionOptions<T>): UseSelectionReturn<T>;
|
|
204
|
+
|
|
205
|
+
export { type SelectionKey, type UseSelectionOptions, type UseSelectionReturn, useSelection };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The primitive key a selection is tracked by. Items may be objects, so
|
|
3
|
+
* {@link useSelection} never stores items directly — it stores a stable
|
|
4
|
+
* key derived from each item (see {@link UseSelectionOptions.getKey}). Keys are
|
|
5
|
+
* `string | number` so they compare by value inside the backing `Set`, which is
|
|
6
|
+
* what makes a selection survive new object identities across renders.
|
|
7
|
+
*/
|
|
8
|
+
type SelectionKey = string | number;
|
|
9
|
+
/**
|
|
10
|
+
* Options for {@link useSelection}.
|
|
11
|
+
*
|
|
12
|
+
* @template T - The item type.
|
|
13
|
+
*/
|
|
14
|
+
interface UseSelectionOptions<T> {
|
|
15
|
+
/**
|
|
16
|
+
* Derive the selection key for an item. The backing `Set` stores these keys,
|
|
17
|
+
* not the items themselves, so selection is stable even when the `items`
|
|
18
|
+
* array is rebuilt with new object references each render (as long as
|
|
19
|
+
* `getKey` returns the same key for the same logical item).
|
|
20
|
+
*
|
|
21
|
+
* Defaults to identity (`(item) => item`), which is correct when `items` are
|
|
22
|
+
* primitives (`string`/`number`). For object items you should provide a
|
|
23
|
+
* `getKey` such as `(row) => row.id`.
|
|
24
|
+
*/
|
|
25
|
+
getKey?: (item: T) => SelectionKey;
|
|
26
|
+
/**
|
|
27
|
+
* Whether more than one item can be selected at a time.
|
|
28
|
+
*
|
|
29
|
+
* - `true` (default) — multi-selection. `select`/`toggle` add to the set.
|
|
30
|
+
* - `false` — single-selection. Selecting an item replaces the current
|
|
31
|
+
* selection; toggling the already-selected item deselects it. `selectAll`
|
|
32
|
+
* is a no-op in this mode (you cannot hold more than one selection).
|
|
33
|
+
*
|
|
34
|
+
* @defaultValue true
|
|
35
|
+
*/
|
|
36
|
+
multiple?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Return value of {@link useSelection}.
|
|
40
|
+
*
|
|
41
|
+
* The backing `Set` of keys is the source of truth, but every item-facing value
|
|
42
|
+
* (`selected`, `isAllSelected`, …) is **derived from the current `items`**. This
|
|
43
|
+
* means keys for items no longer present in `items` simply stop appearing in
|
|
44
|
+
* `selected` and stop counting toward `isAllSelected` — stale selections never
|
|
45
|
+
* linger in the item-facing surface.
|
|
46
|
+
*
|
|
47
|
+
* @template T - The item type.
|
|
48
|
+
*/
|
|
49
|
+
interface UseSelectionReturn<T> {
|
|
50
|
+
/**
|
|
51
|
+
* The currently selected items, in `items` order. Derived as
|
|
52
|
+
* `items` ∩ `selectedKeys`, so items removed from `items` drop out
|
|
53
|
+
* automatically. Ideal for rendering a "selected" summary.
|
|
54
|
+
*
|
|
55
|
+
* Read-only: it is a freshly derived array — use the actions to change the
|
|
56
|
+
* selection rather than mutating this in place.
|
|
57
|
+
*/
|
|
58
|
+
selected: readonly T[];
|
|
59
|
+
/**
|
|
60
|
+
* The raw set of selected keys — the source of truth. Read-only: use the
|
|
61
|
+
* actions to mutate. May contain keys for items not currently in `items`
|
|
62
|
+
* (those are simply invisible in the derived, item-facing values).
|
|
63
|
+
*/
|
|
64
|
+
selectedKeys: ReadonlySet<SelectionKey>;
|
|
65
|
+
/**
|
|
66
|
+
* Whether a given item is currently selected (compared by key). Stable across
|
|
67
|
+
* renders and always reflects the latest state.
|
|
68
|
+
*/
|
|
69
|
+
isSelected: (item: T) => boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Toggle a single item's selection. If it is selected it becomes deselected;
|
|
72
|
+
* otherwise it becomes selected. In single-selection mode, selecting a new
|
|
73
|
+
* item replaces the previous selection. Stable identity.
|
|
74
|
+
*/
|
|
75
|
+
toggle: (item: T) => void;
|
|
76
|
+
/**
|
|
77
|
+
* Explicitly select an item (idempotent — selecting an already-selected item
|
|
78
|
+
* is a no-op). In single-selection mode this replaces the current selection.
|
|
79
|
+
* Stable identity.
|
|
80
|
+
*/
|
|
81
|
+
select: (item: T) => void;
|
|
82
|
+
/**
|
|
83
|
+
* Explicitly deselect an item (idempotent — deselecting an item that is not
|
|
84
|
+
* selected is a no-op). Stable identity.
|
|
85
|
+
*/
|
|
86
|
+
deselect: (item: T) => void;
|
|
87
|
+
/**
|
|
88
|
+
* Select every item currently in `items`. No-op when everything is already
|
|
89
|
+
* selected (no re-render). No-op in single-selection mode. Stable identity.
|
|
90
|
+
*/
|
|
91
|
+
selectAll: () => void;
|
|
92
|
+
/**
|
|
93
|
+
* Clear the entire selection (a.k.a. deselect all). No-op when the selection
|
|
94
|
+
* is already empty (no re-render). Stable identity.
|
|
95
|
+
*/
|
|
96
|
+
clear: () => void;
|
|
97
|
+
/**
|
|
98
|
+
* `true` when every item in `items` is selected. Always `false` for an empty
|
|
99
|
+
* `items` array (there is nothing to have "all" selected).
|
|
100
|
+
*/
|
|
101
|
+
isAllSelected: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* `true` when some — but not all — of the current `items` are selected. Useful
|
|
104
|
+
* to drive a header checkbox's `indeterminate` state. `false` for an empty
|
|
105
|
+
* `items` array.
|
|
106
|
+
*/
|
|
107
|
+
isPartiallySelected: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* `true` when none of the current `items` are selected (including when `items`
|
|
110
|
+
* is empty).
|
|
111
|
+
*/
|
|
112
|
+
isNoneSelected: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Manage multi- or single-selection state for a list or table, backed by a
|
|
117
|
+
* `Set` of keys.
|
|
118
|
+
*
|
|
119
|
+
* The `Set` stores **keys** (see the `getKey` option), never the items
|
|
120
|
+
* themselves, so a selection survives new object identities across renders —
|
|
121
|
+
* rebuilding the `items` array every render will not lose the selection as long
|
|
122
|
+
* as `getKey` maps the same logical item to the same key. Every item-facing
|
|
123
|
+
* value (`selected`, `isAllSelected`, `isPartiallySelected`, …) is **derived
|
|
124
|
+
* from the current `items`**, so removing a selected row from `items` makes it
|
|
125
|
+
* disappear from `selected` and recomputes the aggregate flags automatically —
|
|
126
|
+
* no manual reconciliation needed.
|
|
127
|
+
*
|
|
128
|
+
* Features:
|
|
129
|
+
* - `Set`-backed, immutable updates (a fresh `Set` on every change)
|
|
130
|
+
* - No-op skipping — `selectAll` when all are already selected, and `clear`
|
|
131
|
+
* when already empty, bail out without a re-render
|
|
132
|
+
* - Single-selection mode (`multiple: false`) that replaces the selection
|
|
133
|
+
* - Stable action identities (`toggle`/`select`/`deselect`/`selectAll`/`clear`
|
|
134
|
+
* and `isSelected`) — safe as effect dependencies
|
|
135
|
+
* - `isPartiallySelected` for a header checkbox's indeterminate state
|
|
136
|
+
* - SSR-safe (pure state) and StrictMode/concurrent-safe (no mutation of prior
|
|
137
|
+
* state, callbacks never fire from inside a `setState` updater)
|
|
138
|
+
*
|
|
139
|
+
* @template T - The item type.
|
|
140
|
+
* @param items - The current list of items the selection is scoped to.
|
|
141
|
+
* @param options - {@link UseSelectionOptions} (`getKey`, `multiple`).
|
|
142
|
+
* @returns {@link UseSelectionReturn}
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```tsx
|
|
146
|
+
* type User = { id: number; name: string };
|
|
147
|
+
*
|
|
148
|
+
* function UserTable({ users }: { users: User[] }) {
|
|
149
|
+
* const {
|
|
150
|
+
* selected,
|
|
151
|
+
* isSelected,
|
|
152
|
+
* toggle,
|
|
153
|
+
* selectAll,
|
|
154
|
+
* clear,
|
|
155
|
+
* isAllSelected,
|
|
156
|
+
* isPartiallySelected,
|
|
157
|
+
* } = useSelection(users, { getKey: (u) => u.id });
|
|
158
|
+
*
|
|
159
|
+
* return (
|
|
160
|
+
* <table>
|
|
161
|
+
* <thead>
|
|
162
|
+
* <tr>
|
|
163
|
+
* <th>
|
|
164
|
+
* <input
|
|
165
|
+
* type="checkbox"
|
|
166
|
+
* checked={isAllSelected}
|
|
167
|
+
* ref={(el) => {
|
|
168
|
+
* if (el) el.indeterminate = isPartiallySelected;
|
|
169
|
+
* }}
|
|
170
|
+
* onChange={() => (isAllSelected ? clear() : selectAll())}
|
|
171
|
+
* />
|
|
172
|
+
* </th>
|
|
173
|
+
* <th>Name ({selected.length} selected)</th>
|
|
174
|
+
* </tr>
|
|
175
|
+
* </thead>
|
|
176
|
+
* <tbody>
|
|
177
|
+
* {users.map((user) => (
|
|
178
|
+
* <tr key={user.id}>
|
|
179
|
+
* <td>
|
|
180
|
+
* <input
|
|
181
|
+
* type="checkbox"
|
|
182
|
+
* checked={isSelected(user)}
|
|
183
|
+
* onChange={() => toggle(user)}
|
|
184
|
+
* />
|
|
185
|
+
* </td>
|
|
186
|
+
* <td>{user.name}</td>
|
|
187
|
+
* </tr>
|
|
188
|
+
* ))}
|
|
189
|
+
* </tbody>
|
|
190
|
+
* </table>
|
|
191
|
+
* );
|
|
192
|
+
* }
|
|
193
|
+
* ```
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```tsx
|
|
197
|
+
* // Single-selection mode (radio-like): selecting replaces the previous choice.
|
|
198
|
+
* const { selected, isSelected, select } = useSelection(options, {
|
|
199
|
+
* multiple: false,
|
|
200
|
+
* });
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
declare function useSelection<T>(items: T[], options?: UseSelectionOptions<T>): UseSelectionReturn<T>;
|
|
204
|
+
|
|
205
|
+
export { type SelectionKey, type UseSelectionOptions, type UseSelectionReturn, useSelection };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
useSelection: () => useSelection
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/useSelection.ts
|
|
28
|
+
var import_react = require("react");
|
|
29
|
+
function useSelection(items, options = {}) {
|
|
30
|
+
const { multiple = true } = options;
|
|
31
|
+
const [selectedKeys, setSelectedKeys] = (0, import_react.useState)(
|
|
32
|
+
() => /* @__PURE__ */ new Set()
|
|
33
|
+
);
|
|
34
|
+
const getKeyOptionRef = (0, import_react.useRef)(options.getKey);
|
|
35
|
+
getKeyOptionRef.current = options.getKey;
|
|
36
|
+
const itemsRef = (0, import_react.useRef)(items);
|
|
37
|
+
itemsRef.current = items;
|
|
38
|
+
const multipleRef = (0, import_react.useRef)(multiple);
|
|
39
|
+
multipleRef.current = multiple;
|
|
40
|
+
const selectedKeysRef = (0, import_react.useRef)(selectedKeys);
|
|
41
|
+
selectedKeysRef.current = selectedKeys;
|
|
42
|
+
const getKey = (0, import_react.useCallback)((item) => {
|
|
43
|
+
const fn = getKeyOptionRef.current;
|
|
44
|
+
return fn ? fn(item) : item;
|
|
45
|
+
}, []);
|
|
46
|
+
const select = (0, import_react.useCallback)(
|
|
47
|
+
(item) => {
|
|
48
|
+
const key = getKey(item);
|
|
49
|
+
setSelectedKeys((prev) => {
|
|
50
|
+
if (multipleRef.current) {
|
|
51
|
+
if (prev.has(key)) {
|
|
52
|
+
return prev;
|
|
53
|
+
}
|
|
54
|
+
const next = new Set(prev);
|
|
55
|
+
next.add(key);
|
|
56
|
+
return next;
|
|
57
|
+
}
|
|
58
|
+
if (prev.size === 1 && prev.has(key)) {
|
|
59
|
+
return prev;
|
|
60
|
+
}
|
|
61
|
+
return /* @__PURE__ */ new Set([key]);
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
[getKey]
|
|
65
|
+
);
|
|
66
|
+
const deselect = (0, import_react.useCallback)(
|
|
67
|
+
(item) => {
|
|
68
|
+
const key = getKey(item);
|
|
69
|
+
setSelectedKeys((prev) => {
|
|
70
|
+
if (!prev.has(key)) {
|
|
71
|
+
return prev;
|
|
72
|
+
}
|
|
73
|
+
const next = new Set(prev);
|
|
74
|
+
next.delete(key);
|
|
75
|
+
return next;
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
[getKey]
|
|
79
|
+
);
|
|
80
|
+
const toggle = (0, import_react.useCallback)(
|
|
81
|
+
(item) => {
|
|
82
|
+
const key = getKey(item);
|
|
83
|
+
setSelectedKeys((prev) => {
|
|
84
|
+
if (prev.has(key)) {
|
|
85
|
+
const next = new Set(prev);
|
|
86
|
+
next.delete(key);
|
|
87
|
+
return next;
|
|
88
|
+
}
|
|
89
|
+
if (multipleRef.current) {
|
|
90
|
+
const next = new Set(prev);
|
|
91
|
+
next.add(key);
|
|
92
|
+
return next;
|
|
93
|
+
}
|
|
94
|
+
return /* @__PURE__ */ new Set([key]);
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
[getKey]
|
|
98
|
+
);
|
|
99
|
+
const selectAll = (0, import_react.useCallback)(() => {
|
|
100
|
+
if (!multipleRef.current) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
setSelectedKeys((prev) => {
|
|
104
|
+
const currentItems = itemsRef.current;
|
|
105
|
+
if (currentItems.length === 0) {
|
|
106
|
+
return prev;
|
|
107
|
+
}
|
|
108
|
+
let changed = false;
|
|
109
|
+
const next = new Set(prev);
|
|
110
|
+
for (const item of currentItems) {
|
|
111
|
+
const key = getKey(item);
|
|
112
|
+
if (!next.has(key)) {
|
|
113
|
+
next.add(key);
|
|
114
|
+
changed = true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return changed ? next : prev;
|
|
118
|
+
});
|
|
119
|
+
}, [getKey]);
|
|
120
|
+
const clear = (0, import_react.useCallback)(() => {
|
|
121
|
+
setSelectedKeys(
|
|
122
|
+
(prev) => prev.size === 0 ? prev : /* @__PURE__ */ new Set()
|
|
123
|
+
);
|
|
124
|
+
}, []);
|
|
125
|
+
const isSelected = (0, import_react.useCallback)(
|
|
126
|
+
(item) => selectedKeysRef.current.has(getKey(item)),
|
|
127
|
+
[getKey]
|
|
128
|
+
);
|
|
129
|
+
const { selected, isAllSelected, isPartiallySelected, isNoneSelected } = (0, import_react.useMemo)(() => {
|
|
130
|
+
const sel = [];
|
|
131
|
+
for (const item of items) {
|
|
132
|
+
if (selectedKeys.has(getKey(item))) {
|
|
133
|
+
sel.push(item);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const count = sel.length;
|
|
137
|
+
const total = items.length;
|
|
138
|
+
return {
|
|
139
|
+
selected: sel,
|
|
140
|
+
isAllSelected: total > 0 && count === total,
|
|
141
|
+
isPartiallySelected: count > 0 && count < total,
|
|
142
|
+
isNoneSelected: count === 0
|
|
143
|
+
};
|
|
144
|
+
}, [items, selectedKeys, getKey]);
|
|
145
|
+
return {
|
|
146
|
+
selected,
|
|
147
|
+
selectedKeys,
|
|
148
|
+
isSelected,
|
|
149
|
+
toggle,
|
|
150
|
+
select,
|
|
151
|
+
deselect,
|
|
152
|
+
selectAll,
|
|
153
|
+
clear,
|
|
154
|
+
isAllSelected,
|
|
155
|
+
isPartiallySelected,
|
|
156
|
+
isNoneSelected
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
160
|
+
0 && (module.exports = {
|
|
161
|
+
useSelection
|
|
162
|
+
});
|
|
163
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/useSelection.ts"],"sourcesContent":["export { useSelection } from \"./useSelection\";\nexport type {\n SelectionKey,\n UseSelectionOptions,\n UseSelectionReturn,\n} from \"./types\";\n","import { useCallback, useMemo, useRef, useState } from \"react\";\nimport type {\n SelectionKey,\n UseSelectionOptions,\n UseSelectionReturn,\n} from \"./types\";\n\n/**\n * Manage multi- or single-selection state for a list or table, backed by a\n * `Set` of keys.\n *\n * The `Set` stores **keys** (see the `getKey` option), never the items\n * themselves, so a selection survives new object identities across renders —\n * rebuilding the `items` array every render will not lose the selection as long\n * as `getKey` maps the same logical item to the same key. Every item-facing\n * value (`selected`, `isAllSelected`, `isPartiallySelected`, …) is **derived\n * from the current `items`**, so removing a selected row from `items` makes it\n * disappear from `selected` and recomputes the aggregate flags automatically —\n * no manual reconciliation needed.\n *\n * Features:\n * - `Set`-backed, immutable updates (a fresh `Set` on every change)\n * - No-op skipping — `selectAll` when all are already selected, and `clear`\n * when already empty, bail out without a re-render\n * - Single-selection mode (`multiple: false`) that replaces the selection\n * - Stable action identities (`toggle`/`select`/`deselect`/`selectAll`/`clear`\n * and `isSelected`) — safe as effect dependencies\n * - `isPartiallySelected` for a header checkbox's indeterminate state\n * - SSR-safe (pure state) and StrictMode/concurrent-safe (no mutation of prior\n * state, callbacks never fire from inside a `setState` updater)\n *\n * @template T - The item type.\n * @param items - The current list of items the selection is scoped to.\n * @param options - {@link UseSelectionOptions} (`getKey`, `multiple`).\n * @returns {@link UseSelectionReturn}\n *\n * @example\n * ```tsx\n * type User = { id: number; name: string };\n *\n * function UserTable({ users }: { users: User[] }) {\n * const {\n * selected,\n * isSelected,\n * toggle,\n * selectAll,\n * clear,\n * isAllSelected,\n * isPartiallySelected,\n * } = useSelection(users, { getKey: (u) => u.id });\n *\n * return (\n * <table>\n * <thead>\n * <tr>\n * <th>\n * <input\n * type=\"checkbox\"\n * checked={isAllSelected}\n * ref={(el) => {\n * if (el) el.indeterminate = isPartiallySelected;\n * }}\n * onChange={() => (isAllSelected ? clear() : selectAll())}\n * />\n * </th>\n * <th>Name ({selected.length} selected)</th>\n * </tr>\n * </thead>\n * <tbody>\n * {users.map((user) => (\n * <tr key={user.id}>\n * <td>\n * <input\n * type=\"checkbox\"\n * checked={isSelected(user)}\n * onChange={() => toggle(user)}\n * />\n * </td>\n * <td>{user.name}</td>\n * </tr>\n * ))}\n * </tbody>\n * </table>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Single-selection mode (radio-like): selecting replaces the previous choice.\n * const { selected, isSelected, select } = useSelection(options, {\n * multiple: false,\n * });\n * ```\n */\nexport function useSelection<T>(\n items: T[],\n options: UseSelectionOptions<T> = {}\n): UseSelectionReturn<T> {\n const { multiple = true } = options;\n\n const [selectedKeys, setSelectedKeys] = useState<Set<SelectionKey>>(\n () => new Set()\n );\n\n // Mirror the latest inputs so the stable actions can read fresh values\n // without being recreated each render.\n const getKeyOptionRef = useRef(options.getKey);\n getKeyOptionRef.current = options.getKey;\n\n const itemsRef = useRef(items);\n itemsRef.current = items;\n\n const multipleRef = useRef(multiple);\n multipleRef.current = multiple;\n\n const selectedKeysRef = useRef(selectedKeys);\n selectedKeysRef.current = selectedKeys;\n\n // A stable key resolver that always uses the latest `getKey`. Falls back to\n // identity (works for primitive items) when no `getKey` is provided.\n const getKey = useCallback((item: T): SelectionKey => {\n const fn = getKeyOptionRef.current;\n return fn ? fn(item) : (item as unknown as SelectionKey);\n }, []);\n\n const select = useCallback(\n (item: T) => {\n const key = getKey(item);\n setSelectedKeys((prev) => {\n if (multipleRef.current) {\n if (prev.has(key)) {\n return prev; // already selected — no-op\n }\n const next = new Set(prev);\n next.add(key);\n return next;\n }\n // single-selection: replace with just this key\n if (prev.size === 1 && prev.has(key)) {\n return prev; // already the sole selection — no-op\n }\n return new Set<SelectionKey>([key]);\n });\n },\n [getKey]\n );\n\n const deselect = useCallback(\n (item: T) => {\n const key = getKey(item);\n setSelectedKeys((prev) => {\n if (!prev.has(key)) {\n return prev; // not selected — no-op\n }\n const next = new Set(prev);\n next.delete(key);\n return next;\n });\n },\n [getKey]\n );\n\n const toggle = useCallback(\n (item: T) => {\n const key = getKey(item);\n setSelectedKeys((prev) => {\n if (prev.has(key)) {\n const next = new Set(prev);\n next.delete(key);\n return next;\n }\n if (multipleRef.current) {\n const next = new Set(prev);\n next.add(key);\n return next;\n }\n // single-selection: replace with just this key\n return new Set<SelectionKey>([key]);\n });\n },\n [getKey]\n );\n\n const selectAll = useCallback(() => {\n // Selecting \"all\" is a multi-selection concept.\n if (!multipleRef.current) {\n return;\n }\n setSelectedKeys((prev) => {\n const currentItems = itemsRef.current;\n if (currentItems.length === 0) {\n return prev; // nothing to select — no-op\n }\n let changed = false;\n const next = new Set(prev);\n for (const item of currentItems) {\n const key = getKey(item);\n if (!next.has(key)) {\n next.add(key);\n changed = true;\n }\n }\n return changed ? next : prev; // no-op when all already selected\n });\n }, [getKey]);\n\n const clear = useCallback(() => {\n setSelectedKeys((prev) =>\n prev.size === 0 ? prev : new Set<SelectionKey>()\n );\n }, []);\n\n const isSelected = useCallback(\n (item: T) => selectedKeysRef.current.has(getKey(item)),\n [getKey]\n );\n\n // Derive item-facing values from the current items ∩ selected keys. This is\n // what drops stale keys and keeps the aggregate flags in sync when `items`\n // changes. `getKey` is stable, so this recomputes only when `items` or the\n // selection changes.\n const { selected, isAllSelected, isPartiallySelected, isNoneSelected } =\n useMemo(() => {\n const sel: T[] = [];\n for (const item of items) {\n if (selectedKeys.has(getKey(item))) {\n sel.push(item);\n }\n }\n const count = sel.length;\n const total = items.length;\n return {\n selected: sel,\n isAllSelected: total > 0 && count === total,\n isPartiallySelected: count > 0 && count < total,\n isNoneSelected: count === 0,\n };\n }, [items, selectedKeys, getKey]);\n\n return {\n selected,\n selectedKeys,\n isSelected,\n toggle,\n select,\n deselect,\n selectAll,\n clear,\n isAllSelected,\n isPartiallySelected,\n isNoneSelected,\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// src/useSelection.ts
|
|
2
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
3
|
+
function useSelection(items, options = {}) {
|
|
4
|
+
const { multiple = true } = options;
|
|
5
|
+
const [selectedKeys, setSelectedKeys] = useState(
|
|
6
|
+
() => /* @__PURE__ */ new Set()
|
|
7
|
+
);
|
|
8
|
+
const getKeyOptionRef = useRef(options.getKey);
|
|
9
|
+
getKeyOptionRef.current = options.getKey;
|
|
10
|
+
const itemsRef = useRef(items);
|
|
11
|
+
itemsRef.current = items;
|
|
12
|
+
const multipleRef = useRef(multiple);
|
|
13
|
+
multipleRef.current = multiple;
|
|
14
|
+
const selectedKeysRef = useRef(selectedKeys);
|
|
15
|
+
selectedKeysRef.current = selectedKeys;
|
|
16
|
+
const getKey = useCallback((item) => {
|
|
17
|
+
const fn = getKeyOptionRef.current;
|
|
18
|
+
return fn ? fn(item) : item;
|
|
19
|
+
}, []);
|
|
20
|
+
const select = useCallback(
|
|
21
|
+
(item) => {
|
|
22
|
+
const key = getKey(item);
|
|
23
|
+
setSelectedKeys((prev) => {
|
|
24
|
+
if (multipleRef.current) {
|
|
25
|
+
if (prev.has(key)) {
|
|
26
|
+
return prev;
|
|
27
|
+
}
|
|
28
|
+
const next = new Set(prev);
|
|
29
|
+
next.add(key);
|
|
30
|
+
return next;
|
|
31
|
+
}
|
|
32
|
+
if (prev.size === 1 && prev.has(key)) {
|
|
33
|
+
return prev;
|
|
34
|
+
}
|
|
35
|
+
return /* @__PURE__ */ new Set([key]);
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
[getKey]
|
|
39
|
+
);
|
|
40
|
+
const deselect = useCallback(
|
|
41
|
+
(item) => {
|
|
42
|
+
const key = getKey(item);
|
|
43
|
+
setSelectedKeys((prev) => {
|
|
44
|
+
if (!prev.has(key)) {
|
|
45
|
+
return prev;
|
|
46
|
+
}
|
|
47
|
+
const next = new Set(prev);
|
|
48
|
+
next.delete(key);
|
|
49
|
+
return next;
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
[getKey]
|
|
53
|
+
);
|
|
54
|
+
const toggle = useCallback(
|
|
55
|
+
(item) => {
|
|
56
|
+
const key = getKey(item);
|
|
57
|
+
setSelectedKeys((prev) => {
|
|
58
|
+
if (prev.has(key)) {
|
|
59
|
+
const next = new Set(prev);
|
|
60
|
+
next.delete(key);
|
|
61
|
+
return next;
|
|
62
|
+
}
|
|
63
|
+
if (multipleRef.current) {
|
|
64
|
+
const next = new Set(prev);
|
|
65
|
+
next.add(key);
|
|
66
|
+
return next;
|
|
67
|
+
}
|
|
68
|
+
return /* @__PURE__ */ new Set([key]);
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
[getKey]
|
|
72
|
+
);
|
|
73
|
+
const selectAll = useCallback(() => {
|
|
74
|
+
if (!multipleRef.current) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
setSelectedKeys((prev) => {
|
|
78
|
+
const currentItems = itemsRef.current;
|
|
79
|
+
if (currentItems.length === 0) {
|
|
80
|
+
return prev;
|
|
81
|
+
}
|
|
82
|
+
let changed = false;
|
|
83
|
+
const next = new Set(prev);
|
|
84
|
+
for (const item of currentItems) {
|
|
85
|
+
const key = getKey(item);
|
|
86
|
+
if (!next.has(key)) {
|
|
87
|
+
next.add(key);
|
|
88
|
+
changed = true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return changed ? next : prev;
|
|
92
|
+
});
|
|
93
|
+
}, [getKey]);
|
|
94
|
+
const clear = useCallback(() => {
|
|
95
|
+
setSelectedKeys(
|
|
96
|
+
(prev) => prev.size === 0 ? prev : /* @__PURE__ */ new Set()
|
|
97
|
+
);
|
|
98
|
+
}, []);
|
|
99
|
+
const isSelected = useCallback(
|
|
100
|
+
(item) => selectedKeysRef.current.has(getKey(item)),
|
|
101
|
+
[getKey]
|
|
102
|
+
);
|
|
103
|
+
const { selected, isAllSelected, isPartiallySelected, isNoneSelected } = useMemo(() => {
|
|
104
|
+
const sel = [];
|
|
105
|
+
for (const item of items) {
|
|
106
|
+
if (selectedKeys.has(getKey(item))) {
|
|
107
|
+
sel.push(item);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const count = sel.length;
|
|
111
|
+
const total = items.length;
|
|
112
|
+
return {
|
|
113
|
+
selected: sel,
|
|
114
|
+
isAllSelected: total > 0 && count === total,
|
|
115
|
+
isPartiallySelected: count > 0 && count < total,
|
|
116
|
+
isNoneSelected: count === 0
|
|
117
|
+
};
|
|
118
|
+
}, [items, selectedKeys, getKey]);
|
|
119
|
+
return {
|
|
120
|
+
selected,
|
|
121
|
+
selectedKeys,
|
|
122
|
+
isSelected,
|
|
123
|
+
toggle,
|
|
124
|
+
select,
|
|
125
|
+
deselect,
|
|
126
|
+
selectAll,
|
|
127
|
+
clear,
|
|
128
|
+
isAllSelected,
|
|
129
|
+
isPartiallySelected,
|
|
130
|
+
isNoneSelected
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export {
|
|
134
|
+
useSelection
|
|
135
|
+
};
|
|
136
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useSelection.ts"],"sourcesContent":["import { useCallback, useMemo, useRef, useState } from \"react\";\nimport type {\n SelectionKey,\n UseSelectionOptions,\n UseSelectionReturn,\n} from \"./types\";\n\n/**\n * Manage multi- or single-selection state for a list or table, backed by a\n * `Set` of keys.\n *\n * The `Set` stores **keys** (see the `getKey` option), never the items\n * themselves, so a selection survives new object identities across renders —\n * rebuilding the `items` array every render will not lose the selection as long\n * as `getKey` maps the same logical item to the same key. Every item-facing\n * value (`selected`, `isAllSelected`, `isPartiallySelected`, …) is **derived\n * from the current `items`**, so removing a selected row from `items` makes it\n * disappear from `selected` and recomputes the aggregate flags automatically —\n * no manual reconciliation needed.\n *\n * Features:\n * - `Set`-backed, immutable updates (a fresh `Set` on every change)\n * - No-op skipping — `selectAll` when all are already selected, and `clear`\n * when already empty, bail out without a re-render\n * - Single-selection mode (`multiple: false`) that replaces the selection\n * - Stable action identities (`toggle`/`select`/`deselect`/`selectAll`/`clear`\n * and `isSelected`) — safe as effect dependencies\n * - `isPartiallySelected` for a header checkbox's indeterminate state\n * - SSR-safe (pure state) and StrictMode/concurrent-safe (no mutation of prior\n * state, callbacks never fire from inside a `setState` updater)\n *\n * @template T - The item type.\n * @param items - The current list of items the selection is scoped to.\n * @param options - {@link UseSelectionOptions} (`getKey`, `multiple`).\n * @returns {@link UseSelectionReturn}\n *\n * @example\n * ```tsx\n * type User = { id: number; name: string };\n *\n * function UserTable({ users }: { users: User[] }) {\n * const {\n * selected,\n * isSelected,\n * toggle,\n * selectAll,\n * clear,\n * isAllSelected,\n * isPartiallySelected,\n * } = useSelection(users, { getKey: (u) => u.id });\n *\n * return (\n * <table>\n * <thead>\n * <tr>\n * <th>\n * <input\n * type=\"checkbox\"\n * checked={isAllSelected}\n * ref={(el) => {\n * if (el) el.indeterminate = isPartiallySelected;\n * }}\n * onChange={() => (isAllSelected ? clear() : selectAll())}\n * />\n * </th>\n * <th>Name ({selected.length} selected)</th>\n * </tr>\n * </thead>\n * <tbody>\n * {users.map((user) => (\n * <tr key={user.id}>\n * <td>\n * <input\n * type=\"checkbox\"\n * checked={isSelected(user)}\n * onChange={() => toggle(user)}\n * />\n * </td>\n * <td>{user.name}</td>\n * </tr>\n * ))}\n * </tbody>\n * </table>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Single-selection mode (radio-like): selecting replaces the previous choice.\n * const { selected, isSelected, select } = useSelection(options, {\n * multiple: false,\n * });\n * ```\n */\nexport function useSelection<T>(\n items: T[],\n options: UseSelectionOptions<T> = {}\n): UseSelectionReturn<T> {\n const { multiple = true } = options;\n\n const [selectedKeys, setSelectedKeys] = useState<Set<SelectionKey>>(\n () => new Set()\n );\n\n // Mirror the latest inputs so the stable actions can read fresh values\n // without being recreated each render.\n const getKeyOptionRef = useRef(options.getKey);\n getKeyOptionRef.current = options.getKey;\n\n const itemsRef = useRef(items);\n itemsRef.current = items;\n\n const multipleRef = useRef(multiple);\n multipleRef.current = multiple;\n\n const selectedKeysRef = useRef(selectedKeys);\n selectedKeysRef.current = selectedKeys;\n\n // A stable key resolver that always uses the latest `getKey`. Falls back to\n // identity (works for primitive items) when no `getKey` is provided.\n const getKey = useCallback((item: T): SelectionKey => {\n const fn = getKeyOptionRef.current;\n return fn ? fn(item) : (item as unknown as SelectionKey);\n }, []);\n\n const select = useCallback(\n (item: T) => {\n const key = getKey(item);\n setSelectedKeys((prev) => {\n if (multipleRef.current) {\n if (prev.has(key)) {\n return prev; // already selected — no-op\n }\n const next = new Set(prev);\n next.add(key);\n return next;\n }\n // single-selection: replace with just this key\n if (prev.size === 1 && prev.has(key)) {\n return prev; // already the sole selection — no-op\n }\n return new Set<SelectionKey>([key]);\n });\n },\n [getKey]\n );\n\n const deselect = useCallback(\n (item: T) => {\n const key = getKey(item);\n setSelectedKeys((prev) => {\n if (!prev.has(key)) {\n return prev; // not selected — no-op\n }\n const next = new Set(prev);\n next.delete(key);\n return next;\n });\n },\n [getKey]\n );\n\n const toggle = useCallback(\n (item: T) => {\n const key = getKey(item);\n setSelectedKeys((prev) => {\n if (prev.has(key)) {\n const next = new Set(prev);\n next.delete(key);\n return next;\n }\n if (multipleRef.current) {\n const next = new Set(prev);\n next.add(key);\n return next;\n }\n // single-selection: replace with just this key\n return new Set<SelectionKey>([key]);\n });\n },\n [getKey]\n );\n\n const selectAll = useCallback(() => {\n // Selecting \"all\" is a multi-selection concept.\n if (!multipleRef.current) {\n return;\n }\n setSelectedKeys((prev) => {\n const currentItems = itemsRef.current;\n if (currentItems.length === 0) {\n return prev; // nothing to select — no-op\n }\n let changed = false;\n const next = new Set(prev);\n for (const item of currentItems) {\n const key = getKey(item);\n if (!next.has(key)) {\n next.add(key);\n changed = true;\n }\n }\n return changed ? next : prev; // no-op when all already selected\n });\n }, [getKey]);\n\n const clear = useCallback(() => {\n setSelectedKeys((prev) =>\n prev.size === 0 ? prev : new Set<SelectionKey>()\n );\n }, []);\n\n const isSelected = useCallback(\n (item: T) => selectedKeysRef.current.has(getKey(item)),\n [getKey]\n );\n\n // Derive item-facing values from the current items ∩ selected keys. This is\n // what drops stale keys and keeps the aggregate flags in sync when `items`\n // changes. `getKey` is stable, so this recomputes only when `items` or the\n // selection changes.\n const { selected, isAllSelected, isPartiallySelected, isNoneSelected } =\n useMemo(() => {\n const sel: T[] = [];\n for (const item of items) {\n if (selectedKeys.has(getKey(item))) {\n sel.push(item);\n }\n }\n const count = sel.length;\n const total = items.length;\n return {\n selected: sel,\n isAllSelected: total > 0 && count === total,\n isPartiallySelected: count > 0 && count < total,\n isNoneSelected: count === 0,\n };\n }, [items, selectedKeys, getKey]);\n\n return {\n selected,\n selectedKeys,\n isSelected,\n toggle,\n select,\n deselect,\n selectAll,\n clear,\n isAllSelected,\n isPartiallySelected,\n isNoneSelected,\n };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usefy/use-selection",
|
|
3
|
+
"version": "0.19.0",
|
|
4
|
+
"description": "A React hook for managing multi/single selection state for lists and tables, backed by a Set",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
24
|
+
"@testing-library/react": "^16.3.1",
|
|
25
|
+
"@testing-library/user-event": "^14.6.1",
|
|
26
|
+
"@types/react": "^19.0.0",
|
|
27
|
+
"jsdom": "^27.3.0",
|
|
28
|
+
"react": "^19.0.0",
|
|
29
|
+
"rimraf": "^6.0.1",
|
|
30
|
+
"tsup": "^8.0.0",
|
|
31
|
+
"typescript": "^5.0.0",
|
|
32
|
+
"vitest": "^4.0.16"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/mirunamu00/usefy.git",
|
|
40
|
+
"directory": "packages/hooks/use-selection"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"keywords": [
|
|
44
|
+
"react",
|
|
45
|
+
"hooks",
|
|
46
|
+
"selection",
|
|
47
|
+
"select",
|
|
48
|
+
"multi-select",
|
|
49
|
+
"checkbox",
|
|
50
|
+
"table",
|
|
51
|
+
"list",
|
|
52
|
+
"set",
|
|
53
|
+
"state",
|
|
54
|
+
"useSelection"
|
|
55
|
+
],
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsup",
|
|
58
|
+
"dev": "tsup --watch",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"test:watch": "vitest",
|
|
61
|
+
"typecheck": "tsc --noEmit",
|
|
62
|
+
"clean": "rimraf dist"
|
|
63
|
+
}
|
|
64
|
+
}
|