@synerise/ds-list-item 1.6.1 → 1.6.2
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/CHANGELOG.md +4 -0
- package/CLAUDE.md +187 -0
- package/package.json +9 -8
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [1.6.2](https://github.com/Synerise/synerise-design/compare/@synerise/ds-list-item@1.6.1...@synerise/ds-list-item@1.6.2) (2026-07-23)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @synerise/ds-list-item
|
|
9
|
+
|
|
6
10
|
## [1.6.1](https://github.com/Synerise/synerise-design/compare/@synerise/ds-list-item@1.6.0...@synerise/ds-list-item@1.6.1) (2026-07-16)
|
|
7
11
|
|
|
8
12
|
**Note:** Version bump only for package @synerise/ds-list-item
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# ListItem (`@synerise/ds-list-item`)
|
|
2
|
+
> A polymorphic list row component that renders one of five visual variants (default text, danger, select, divider, header) and supports prefixes, suffixes, submenus, copy-to-clipboard, hover tooltips, and keyboard navigation via `@floating-ui/react`.
|
|
3
|
+
|
|
4
|
+
## Package structure
|
|
5
|
+
```
|
|
6
|
+
src/
|
|
7
|
+
ListItem.tsx — main component; type-switch dispatcher
|
|
8
|
+
ListItem.types.ts — all public types, itemTypes/itemSizes consts
|
|
9
|
+
ListItem.const.ts — LIST_ITEM_SIZE_MAPPING (default=32px, large=50px)
|
|
10
|
+
ListItem.styles.ts — (empty placeholder)
|
|
11
|
+
components/
|
|
12
|
+
Danger/Danger.tsx — renders DangerItem styled variant
|
|
13
|
+
Divider/Divider.tsx — renders ds-divider with side margin + dashed
|
|
14
|
+
Header/Header.tsx — section header row with optional tooltip icon
|
|
15
|
+
Select/Select.tsx — select-style row (uuid key bug present)
|
|
16
|
+
Text/Text.tsx — default variant; all interactive logic lives here
|
|
17
|
+
Text/ItemLabel.tsx — rendered <div role="menuitem"> with prefix/suffix/check slots
|
|
18
|
+
Text/DynamicLabel.tsx — swaps normal label for copiedLabel via CSS transition
|
|
19
|
+
Text/utils.ts — renderAddon, removeHandlerProps, getCopyConfig
|
|
20
|
+
Text/ItemLabel.const.ts — INDENT_WIDTH = 20 (px per indentLevel)
|
|
21
|
+
HoverTooltip/HoverTooltip.tsx — wraps item in @synerise/ds-popover on hover
|
|
22
|
+
HoverTooltip/HoverTooltip.const.ts — offset 8px, open 100ms / close 400ms delays
|
|
23
|
+
GroupItem/GroupItem.tsx — renders a title + array of ListItems or children
|
|
24
|
+
SubMenu/SubMenu.tsx — collapsible inline sub-list, indented by level
|
|
25
|
+
ListContext/ListContext.tsx — React context + useListContext hook
|
|
26
|
+
ListContext/ListContextProvider.tsx — provider wrapping FloatingDelayGroup
|
|
27
|
+
ListWrapper/ListWrapper.tsx — styled container that installs ListContextProvider
|
|
28
|
+
hooks/
|
|
29
|
+
useTemporaryLabel.ts — shows copiedLabel for a configurable duration
|
|
30
|
+
modules.d.ts — declares *.less module
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Public exports
|
|
34
|
+
|
|
35
|
+
### Default export: `ListItem`
|
|
36
|
+
|
|
37
|
+
`forwardRef<HTMLDivElement, ListItemProps>` — dispatches to sub-component based on `type`.
|
|
38
|
+
|
|
39
|
+
| Prop | Type | Default | Description |
|
|
40
|
+
|------|------|---------|-------------|
|
|
41
|
+
| `type` | `'default' \| 'danger' \| 'divider' \| 'select' \| 'header'` | `'default'` | Visual/behavioural variant |
|
|
42
|
+
| `text` | `ReactNode` | — | Label alternative to `children` (`text` wins if both provided) |
|
|
43
|
+
| `children` | `ReactNode` | — | Label content |
|
|
44
|
+
| `size` | `'default' \| 'large'` | `'default'` | Row height: 32px or 50px |
|
|
45
|
+
| `checked` | `boolean` | — | Shows green check icon in suffix slot; adds `ds-list-item-selected` class |
|
|
46
|
+
| `selected` | `boolean` | — | Additional selected state (typed but not rendered by built-in variants) |
|
|
47
|
+
| `disabled` | `boolean` | — | Prevents click/key events; sets `tabIndex=-1` |
|
|
48
|
+
| `noHover` | `boolean` | — | Disables hover background |
|
|
49
|
+
| `featured` | `boolean` | — | Blue label + blue prefix/suffix icon tint |
|
|
50
|
+
| `ordered` | `boolean` | — | Renders ordinal counter before content |
|
|
51
|
+
| `parent` | `boolean` | — | Shows `AngleRightS` arrow in content area |
|
|
52
|
+
| `selectableParent` | `boolean` | — | Only affects items with a `subMenu`. When `true`, clicking the row fires `onClick` (e.g. to select it) and expand/collapse moves to the suffix arrow (`role="button"`, keyboard-accessible). When omitted, the whole row toggles the sub-menu and `onClick` is not called. |
|
|
53
|
+
| `prefixel` | `ReactNode \| AddonRenderer` | — | Left slot; `AddonRenderer = (hovered: boolean) => ReactNode` |
|
|
54
|
+
| `prefixVisibilityTrigger` | `'hover' \| 'default'` | — | When `'hover'`, prefix only visible while item is hovered |
|
|
55
|
+
| `suffixel` | `ReactNode \| AddonRenderer` | — | Right slot |
|
|
56
|
+
| `suffixVisibilityTrigger` | `'hover' \| 'default'` | — | When `'hover'`, suffix only visible while item is hovered |
|
|
57
|
+
| `description` | `ReactNode` | — | Second line; only rendered when `size === 'large'` |
|
|
58
|
+
| `highlight` | `string` | — | Substring to highlight in string children |
|
|
59
|
+
| `copyable` | `boolean \| Copyable` | — | Enables copy-to-clipboard on mousedown/Enter |
|
|
60
|
+
| `copyValue` | `string` | — | **Deprecated** — use `copyable: { copyValue }` |
|
|
61
|
+
| `copyHint` | `ReactNode` | — | **Deprecated** — no longer rendered |
|
|
62
|
+
| `copyTooltip` | `ReactNode` | — | **Deprecated** — no longer rendered |
|
|
63
|
+
| `timeToHideTooltip` | `number` | — | **Deprecated** — no longer rendered |
|
|
64
|
+
| `tooltipProps` | `TooltipProps` | — | **Deprecated** — no longer rendered |
|
|
65
|
+
| `subMenu` | `ListItemProps[]` | — | Inline collapsible sub-list; toggled on row click/Enter by default, or via the suffix arrow when `selectableParent` is set |
|
|
66
|
+
| `defaultSubMenuOpen` | `boolean` | `false` | Initial open state of `subMenu` (uncontrolled, read once on mount). Ignored when `subMenuOpen` is provided |
|
|
67
|
+
| `subMenuOpen` | `boolean` | — | Controlled open state of `subMenu`; when set, the component stops managing it internally — pair with `onSubMenuToggle` |
|
|
68
|
+
| `onSubMenuToggle` | `(open: boolean) => void` | — | Fires with the next open state on every `subMenu` toggle (row click in default mode, or the suffix arrow when `selectableParent`) |
|
|
69
|
+
| `indentLevel` | `number` | — | Left indent = `indentLevel * 20px` |
|
|
70
|
+
| `renderHoverTooltip` | `() => JSX.Element` | — | Popover rendered on hover via `HoverTooltip`; wraps item in a `PropagationStopper` |
|
|
71
|
+
| `popoverProps` | `{ placement?, getPopupContainer?, offsetConfig?, flipConfig?, shiftConfig?, initialOpen? }` | — | Passed to `HoverTooltip`'s `Popover`; only used when `renderHoverTooltip` is set |
|
|
72
|
+
| `onClick` | `ListItemEventHandler<MouseEvent \| KeyboardEvent>` | — | Receives `{ key, item, domEvent }` |
|
|
73
|
+
| `onItemHover` | `ListItemEventHandler<MouseEvent>` | — | Fires on `mouseOver` |
|
|
74
|
+
| `key` | `Key` | — | React key |
|
|
75
|
+
| `itemKey` | `Key` | — | Stable key used in `ItemData`; falls back to a stable UUID |
|
|
76
|
+
| `level` | `number` | — | Passed to `Divider` only |
|
|
77
|
+
| `higher` | `boolean` | — | Passed to `Divider` only |
|
|
78
|
+
| `direction` | `'ltr' \| 'rtl'` | — | Typed; not visually implemented in default variant |
|
|
79
|
+
|
|
80
|
+
### `Copyable` type
|
|
81
|
+
```ts
|
|
82
|
+
type Copyable = {
|
|
83
|
+
copyValue: string;
|
|
84
|
+
copiedLabel?: ReactNode; // label shown after copy (replaces content temporarily)
|
|
85
|
+
timeToReset?: number; // ms to show copiedLabel (default 1000)
|
|
86
|
+
delayClickEvent?: number | false; // ms to delay onClick after copy (default 700); false to disable
|
|
87
|
+
};
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### `ListWrapper`
|
|
91
|
+
Container `<div>` that installs `ListContextProvider` (shared `onClick`, `FloatingDelayGroup`). Accepts all `ListContextProps` + HTML div attributes + `children`.
|
|
92
|
+
|
|
93
|
+
| Prop | Type | Default | Description |
|
|
94
|
+
|------|------|---------|-------------|
|
|
95
|
+
| `onClick` | `ListItemEventHandler` | — | Shared click handler for all descendant ListItems |
|
|
96
|
+
| `children` | `ReactNode` | — | ListItem nodes |
|
|
97
|
+
|
|
98
|
+
### `HoverTooltip` (named export)
|
|
99
|
+
Public re-export. Usually consumed indirectly through `renderHoverTooltip`; can be used standalone to wrap any content.
|
|
100
|
+
|
|
101
|
+
### `GroupItem` (named export)
|
|
102
|
+
Groups a title + items array.
|
|
103
|
+
|
|
104
|
+
| Prop | Type | Default | Description |
|
|
105
|
+
|------|------|---------|-------------|
|
|
106
|
+
| `title` | `ReactNode` | **required** | Section label |
|
|
107
|
+
| `items` | `ListItemProps[]` | — | Rendered as `<ListItem>` array |
|
|
108
|
+
| `children` | `ReactNode` | — | Alternative/additional items |
|
|
109
|
+
|
|
110
|
+
### `ListContextProvider` (named export)
|
|
111
|
+
Provides `ListContext` + `FloatingDelayGroup`. Use when building custom list containers.
|
|
112
|
+
|
|
113
|
+
| Prop | Type | Default | Description |
|
|
114
|
+
|------|------|---------|-------------|
|
|
115
|
+
| `onClick` | `ListItemEventHandler` | — | Shared click handler |
|
|
116
|
+
| `popoverDelay` | `DelayConfig` | `{ open: 100, close: 400 }` | HoverTooltip delay for all items |
|
|
117
|
+
| `children` | `ReactNode` | — | |
|
|
118
|
+
|
|
119
|
+
### `useListContext` (named export)
|
|
120
|
+
`() => ListContextProps | undefined` — reads `ListContext`; returns `undefined` outside a provider.
|
|
121
|
+
|
|
122
|
+
### Const/type exports
|
|
123
|
+
- `itemTypes` — `{ DEFAULT, DANGER, DIVIDER, SELECT, HEADER }`
|
|
124
|
+
- `itemSizes` — `{ DEFAULT, LARGE }`
|
|
125
|
+
- `LIST_ITEM_SIZE_MAPPING` — `Record<ItemSize, number>`
|
|
126
|
+
- `type ListItemProps`, `type BasicItemProps`, `type ItemSize`, `type ItemType`, `type ItemData`, `type StyledListItem`, `type ListItemEventHandler`, `type ListWrapperProps`
|
|
127
|
+
|
|
128
|
+
## Usage patterns
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import ListItem, { ListWrapper, ListContextProvider, useListContext } from '@synerise/ds-list-item';
|
|
132
|
+
|
|
133
|
+
// Basic
|
|
134
|
+
<ListItem>Label</ListItem>
|
|
135
|
+
|
|
136
|
+
// With shared click handler (preferred for lists)
|
|
137
|
+
<ListWrapper onClick={(itemData) => console.log(itemData)}>
|
|
138
|
+
<ListItem itemKey="a">Option A</ListItem>
|
|
139
|
+
<ListItem itemKey="b" type="danger">Delete</ListItem>
|
|
140
|
+
<ListItem type="divider" />
|
|
141
|
+
<ListItem type="header">Section</ListItem>
|
|
142
|
+
</ListWrapper>
|
|
143
|
+
|
|
144
|
+
// Copy-to-clipboard
|
|
145
|
+
<ListItem copyable={{ copyValue: 'value', copiedLabel: 'Copied!', timeToReset: 1500 }}>
|
|
146
|
+
Copy me
|
|
147
|
+
</ListItem>
|
|
148
|
+
|
|
149
|
+
// Hover tooltip (renders a Popover card on hover)
|
|
150
|
+
<ListItem renderHoverTooltip={() => <div>Details</div>} popoverProps={{ placement: 'right' }}>
|
|
151
|
+
Hover me
|
|
152
|
+
</ListItem>
|
|
153
|
+
|
|
154
|
+
// Sub-menu (inline, collapsible)
|
|
155
|
+
<ListItem subMenu={[{ children: 'Sub A', itemKey: 'sub-a' }]}>Parent</ListItem>
|
|
156
|
+
|
|
157
|
+
// Prefix/suffix with hover visibility
|
|
158
|
+
<ListItem
|
|
159
|
+
prefixel={<Icon component={<StarM />} />}
|
|
160
|
+
prefixVisibilityTrigger="hover"
|
|
161
|
+
suffixel={(hovered) => hovered ? <Button>Delete</Button> : null}
|
|
162
|
+
>
|
|
163
|
+
Row
|
|
164
|
+
</ListItem>
|
|
165
|
+
|
|
166
|
+
// Large size with description
|
|
167
|
+
<ListItem size="large" description="Secondary text">
|
|
168
|
+
Primary label
|
|
169
|
+
</ListItem>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Styling / Key dependencies / Implementation notes
|
|
173
|
+
|
|
174
|
+
- **`@floating-ui/react` `useListItem`** — `Text` registers each item with floating-ui's list context via `useListItem()`. The merged ref (`useMergeRefs`) handles both the forwarded ref and the floating-ui ref. This is required for keyboard navigation in dropdowns.
|
|
175
|
+
- **`useDropdown` integration** — if an item is rendered inside `@synerise/ds-core`'s `DropdownProvider`, clicking an item calls `setIsOpen(false)` when `hideOnItemClick === true`.
|
|
176
|
+
- **`description` only renders at `size="large"`** — setting `description` on a default-size item silently does nothing.
|
|
177
|
+
- **`checked` in `Text` type** — renders a `CheckS` icon in the suffix slot. In `select` type, the styled component handles its own checked appearance independently via `S.SelectItem`.
|
|
178
|
+
- **`Select.tsx` bug** — `uuid()` is called as the `key` prop on every render, generating a new key on each re-render, causing unnecessary unmount/remount cycles. (TODO comment present in source.)
|
|
179
|
+
- **`copyable` boolean legacy** — when `copyable` is a plain `boolean`, it requires `copyValue` (deprecated string prop) to be set separately. Use the object form `copyable: { copyValue: '..' }` instead.
|
|
180
|
+
- **`useTemporaryLabel` missing cleanup** — the `useEffect` in `useTemporaryLabel` runs on every render (no dependency array), creating a new `setTimeout` on each render. This is a bug: it resets the timer unnecessarily when other state changes.
|
|
181
|
+
- **`GroupItem` uses `items?.map(ListItem)`** — passes `ListItemProps` objects directly as React elements via `.map(Component)`. This relies on all `ListItemProps` being valid props AND each item object having a `key` property (which is typed as optional). Missing `key` will produce React warnings.
|
|
182
|
+
- **`indentLevel`** — each sub-menu level adds 20px indent (`INDENT_WIDTH`). The `SubMenu` component automatically increments `indentLevel` by 1.
|
|
183
|
+
- **`selectableParent`** — by default a `subMenu` parent owns its whole row for expand/collapse and swallows `onClick`. Setting `selectableParent` flips this: `Text.tsx` routes the row click/Enter to `onClick` (the `if (subMenu && !selectableParent)` guard), and `ItemLabel` renders the suffix arrow as a focusable `S.SubMenuToggle` (`role="button"`, Enter/Space) whose handler calls `stopPropagation()` so toggling never selects.
|
|
184
|
+
- **Sub-menu open state** — uncontrolled by default (internal `useState`, starts closed). Seed the initial state with `defaultSubMenuOpen`, or fully control it with `subMenuOpen` + `onSubMenuToggle` (mirrors `Dropdown`'s `open`/`onOpenChange`). When `subMenuOpen` is provided, `Text.tsx` derives the open state from the prop and `onSubMenuToggle` fires the next state on each toggle without mutating internal state. Omitting all three preserves the legacy behaviour.
|
|
185
|
+
- **Sub-menu nesting** — `subMenu` is fully recursive (each `SubMenu` renders items via `ItemComponent`, so a nested item with its own `subMenu` nests again); there is **no hard depth limit**. The practical cap is layout-driven: each open `SubMenuContainer` is `max-height: 999px; overflow: hidden` (`SubMenu.styles.ts`), so any single level whose expanded content exceeds 999px is clipped, and indent grows 20px per level without bound.
|
|
186
|
+
- **`popoverDelay` is not forwarded** through `ListWrapper` — `ListWrapper` only forwards `onClick`; callers needing custom delay must use `ListContextProvider` directly.
|
|
187
|
+
- **Uses Vitest** — `jest.config.js` present; only 2 smoke tests exist, well below 80% coverage requirement.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synerise/ds-list-item",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.2",
|
|
4
4
|
"description": "ListItem UI Component for the Synerise Design System",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"repository": "Synerise/synerise-design",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
"/dist",
|
|
19
19
|
"CHANGELOG.md",
|
|
20
|
+
"CLAUDE.md",
|
|
20
21
|
"README.md",
|
|
21
22
|
"package.json",
|
|
22
23
|
"LICENSE.md"
|
|
@@ -42,12 +43,12 @@
|
|
|
42
43
|
"types": "dist/index.d.ts",
|
|
43
44
|
"dependencies": {
|
|
44
45
|
"@floating-ui/react": "^0.27.16",
|
|
45
|
-
"@synerise/ds-button": "^1.5.
|
|
46
|
-
"@synerise/ds-divider": "^1.3.
|
|
47
|
-
"@synerise/ds-icon": "^1.18.
|
|
48
|
-
"@synerise/ds-popover": "^1.6.
|
|
49
|
-
"@synerise/ds-tooltip": "^1.5.
|
|
50
|
-
"@synerise/ds-utils": "^1.10.
|
|
46
|
+
"@synerise/ds-button": "^1.5.35",
|
|
47
|
+
"@synerise/ds-divider": "^1.3.16",
|
|
48
|
+
"@synerise/ds-icon": "^1.18.5",
|
|
49
|
+
"@synerise/ds-popover": "^1.6.2",
|
|
50
|
+
"@synerise/ds-tooltip": "^1.5.4",
|
|
51
|
+
"@synerise/ds-utils": "^1.10.2",
|
|
51
52
|
"classnames": "^2.5.1"
|
|
52
53
|
},
|
|
53
54
|
"peerDependencies": {
|
|
@@ -60,5 +61,5 @@
|
|
|
60
61
|
"@testing-library/react": "^14.0.0",
|
|
61
62
|
"vitest": "^4.0.0"
|
|
62
63
|
},
|
|
63
|
-
"gitHead": "
|
|
64
|
+
"gitHead": "d0a43cc43d8528a36f105aceea52ab470edb71d9"
|
|
64
65
|
}
|