@edux-design/chips 0.1.0 → 0.3.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/CHANGELOG.md +25 -0
- package/README.md +53 -9
- package/dist/index.d.mts +35 -3
- package/dist/index.d.ts +35 -3
- package/dist/index.js +80 -5
- package/dist/index.mjs +79 -5
- package/docs/action-chip-inventory.md +21 -0
- package/docs/action-chip.md +85 -0
- package/docs/removal.md +26 -0
- package/package.json +7 -19
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @edux-design/chips
|
|
2
|
+
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add ActionChip as a distinct native button with native ref and activation, count and adornment content, and visual selected state. Preserve the existing div-based Chip and removal contract; do not introduce nested interactive controls.
|
|
8
|
+
|
|
9
|
+
## 0.2.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- Add typed removeButtonProps for native remove-button descriptions, refs and composed handlers while preserving Chip removal, label and disabled semantics. Keep collection state and post-removal focus with the host.
|
|
14
|
+
|
|
15
|
+
## 0.1.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- Moved react / dom to peer deps
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- Updated dependencies
|
|
24
|
+
- @edux-design/icons@0.1.0
|
|
25
|
+
- @edux-design/utils@0.1.0
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @edux-design/chips
|
|
2
2
|
|
|
3
|
-
Token-driven
|
|
3
|
+
Token-driven compact chips. `Chip` renders an informational or removable div-root pill; `ActionChip` renders a native button for filter actions and overlay triggers. Both support label/count composition with existing primitives.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -14,6 +14,18 @@ npm install @edux-design/chips @edux-design/utils @edux-design/icons
|
|
|
14
14
|
|
|
15
15
|
The package ships React components and depends on `react@^19.1.0` / `react-dom@^19.1.0`.
|
|
16
16
|
|
|
17
|
+
## Token contract
|
|
18
|
+
|
|
19
|
+
Small chips use the shared `min-h-32` token and both chip sizes use the shared
|
|
20
|
+
`size-16` token for the remove control. The default chip remains `40px` tall
|
|
21
|
+
through an explicit component-owned `min-h-[40px]` value because the current
|
|
22
|
+
theme has no 40px spacing or control token; introducing a new semantic token
|
|
23
|
+
would be broader than this package change and is intentionally deferred.
|
|
24
|
+
|
|
25
|
+
Padding, gaps, borders, colours, and focus treatment use existing Edux numeric
|
|
26
|
+
and semantic tokens. Consumers should import `@edux-design/tokens/theme` so
|
|
27
|
+
those utilities resolve consistently.
|
|
28
|
+
|
|
17
29
|
---
|
|
18
30
|
|
|
19
31
|
## Usage
|
|
@@ -25,7 +37,9 @@ import { TagNumber } from "@edux-design/tags";
|
|
|
25
37
|
export function Filters({ onRemove }) {
|
|
26
38
|
return (
|
|
27
39
|
<Chip label="Label" onRemove={onRemove}>
|
|
28
|
-
<TagNumber variant="brand" tone="solid">
|
|
40
|
+
<TagNumber variant="brand" tone="solid">
|
|
41
|
+
999
|
|
42
|
+
</TagNumber>
|
|
29
43
|
</Chip>
|
|
30
44
|
);
|
|
31
45
|
}
|
|
@@ -33,13 +47,13 @@ export function Filters({ onRemove }) {
|
|
|
33
47
|
|
|
34
48
|
### Key Props
|
|
35
49
|
|
|
36
|
-
| Prop
|
|
37
|
-
|
|
|
38
|
-
| `label`
|
|
39
|
-
| `size`
|
|
40
|
-
| `onRemove`
|
|
41
|
-
| `removeLabel` | Accessible label for the close button. Defaults to `"Remove chip"`.
|
|
42
|
-
| `disabled`
|
|
50
|
+
| Prop | Description |
|
|
51
|
+
| ------------- | -------------------------------------------------------------------------- |
|
|
52
|
+
| `label` | Primary text rendered inside the chip (truncated when needed). |
|
|
53
|
+
| `size` | `"default"` (40px tall) or `"small"` (32px tall). |
|
|
54
|
+
| `onRemove` | When provided, renders a close icon button that calls the handler. |
|
|
55
|
+
| `removeLabel` | Accessible label for the close button. Defaults to `"Remove chip"`. |
|
|
56
|
+
| `disabled` | Mutes colors, disables the close button, and removes pointer interactions. |
|
|
43
57
|
|
|
44
58
|
`children` are rendered before the label, so you can compose `TagNumber` (or any other primitive) directly inside the chip without a dedicated prop.
|
|
45
59
|
|
|
@@ -62,3 +76,33 @@ Stories live under `src/demos/Chip.stories.jsx` with size + state examples pulle
|
|
|
62
76
|
- Focus, hover, pressed, dragged, and inactive visuals are all handled via Tailwind utility classes wired to the shared design tokens.
|
|
63
77
|
- The chip container uses `focus-within` so the focus ring appears when the remove button is tabbed to.
|
|
64
78
|
- Feel free to slot other components ahead of the label; the layout simply spaces whatever you supply, so TagNumber composition stays entirely declarative.
|
|
79
|
+
|
|
80
|
+
## Native action/filter trigger
|
|
81
|
+
|
|
82
|
+
```jsx
|
|
83
|
+
import { ActionChip } from "@edux-design/chips";
|
|
84
|
+
import { TagNumber } from "@edux-design/tags";
|
|
85
|
+
import { Chevron } from "@edux-design/icons";
|
|
86
|
+
|
|
87
|
+
<ActionChip
|
|
88
|
+
label="Stages"
|
|
89
|
+
selected={selectedStages.length > 0}
|
|
90
|
+
count={
|
|
91
|
+
selectedStages.length > 0 ? (
|
|
92
|
+
<TagNumber>{selectedStages.length}</TagNumber>
|
|
93
|
+
) : null
|
|
94
|
+
}
|
|
95
|
+
endAdornment={<Chevron aria-hidden="true" />}
|
|
96
|
+
onClick={openStageFilter}
|
|
97
|
+
/>;
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
ActionChip forwards native button props and an `HTMLButtonElement` ref, defaults
|
|
101
|
+
to `type="button"`, and composes with Menu/Popover triggers. `selected` is visual
|
|
102
|
+
filter state; popup expansion comes from `aria-expanded` or `data-state="open"`.
|
|
103
|
+
Set native `aria-pressed` only for actual toggle actions. Labels and slots must
|
|
104
|
+
contain noninteractive content; removal remains on the div-root Chip.
|
|
105
|
+
|
|
106
|
+
Its `default` and `small` minimum heights retain existing 40px/32px chip geometry.
|
|
107
|
+
See [ActionChip contract and examples](./docs/action-chip.md) and the
|
|
108
|
+
[read-only host inventory](./docs/action-chip-inventory.md).
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import React from 'react';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Token-driven chip that composes existing primitives (i.e. TagNumber) inside.
|
|
@@ -9,8 +9,40 @@ import * as react from 'react';
|
|
|
9
9
|
* onRemove?: (event: React.MouseEvent<HTMLButtonElement>) => void,
|
|
10
10
|
* removeLabel?: string,
|
|
11
11
|
* disabled?: boolean,
|
|
12
|
+
* removeButtonProps?: Omit<React.ComponentPropsWithRef<"button">, "type"|"disabled"|"aria-label"|"aria-labelledby"|"children">,
|
|
12
13
|
* }} ChipProps
|
|
13
14
|
*/
|
|
14
|
-
|
|
15
|
+
/** @type {React.ForwardRefExoticComponent<ChipProps & React.RefAttributes<HTMLDivElement>>} */
|
|
16
|
+
declare const Chip: React.ForwardRefExoticComponent<ChipProps & React.RefAttributes<HTMLDivElement>>;
|
|
17
|
+
/**
|
|
18
|
+
* Token-driven chip that composes existing primitives (i.e. TagNumber) inside.
|
|
19
|
+
*/
|
|
20
|
+
type ChipProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
21
|
+
label?: React.ReactNode;
|
|
22
|
+
size?: "default" | "small";
|
|
23
|
+
onRemove?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
24
|
+
removeLabel?: string;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
removeButtonProps?: Omit<React.ComponentPropsWithRef<"button">, "type" | "disabled" | "aria-label" | "aria-labelledby" | "children">;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
interface ActionChipProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
30
|
+
/** Visible label; children are used when label is undefined. Supply noninteractive content. */
|
|
31
|
+
label?: React.ReactNode;
|
|
32
|
+
/** Chip-specific geometry, matching Chip's existing 40px/32px minimum heights. */
|
|
33
|
+
size?: "default" | "small";
|
|
34
|
+
/** Visual filter state only. Does not imply aria-pressed or popup expansion. */
|
|
35
|
+
selected?: boolean;
|
|
36
|
+
/** Noninteractive count content before the label. The host owns its visibility and value. */
|
|
37
|
+
count?: React.ReactNode;
|
|
38
|
+
startAdornment?: React.ReactNode;
|
|
39
|
+
endAdornment?: React.ReactNode;
|
|
40
|
+
/** Use the separate div-root Chip for removal. */
|
|
41
|
+
onRemove?: never;
|
|
42
|
+
removeLabel?: never;
|
|
43
|
+
removeButtonProps?: never;
|
|
44
|
+
}
|
|
45
|
+
/** Native action/overlay trigger. The host owns filter state and popup behaviour. */
|
|
46
|
+
declare const ActionChip: React.ForwardRefExoticComponent<ActionChipProps & React.RefAttributes<HTMLButtonElement>>;
|
|
15
47
|
|
|
16
|
-
export { Chip };
|
|
48
|
+
export { ActionChip, Chip };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import React from 'react';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Token-driven chip that composes existing primitives (i.e. TagNumber) inside.
|
|
@@ -9,8 +9,40 @@ import * as react from 'react';
|
|
|
9
9
|
* onRemove?: (event: React.MouseEvent<HTMLButtonElement>) => void,
|
|
10
10
|
* removeLabel?: string,
|
|
11
11
|
* disabled?: boolean,
|
|
12
|
+
* removeButtonProps?: Omit<React.ComponentPropsWithRef<"button">, "type"|"disabled"|"aria-label"|"aria-labelledby"|"children">,
|
|
12
13
|
* }} ChipProps
|
|
13
14
|
*/
|
|
14
|
-
|
|
15
|
+
/** @type {React.ForwardRefExoticComponent<ChipProps & React.RefAttributes<HTMLDivElement>>} */
|
|
16
|
+
declare const Chip: React.ForwardRefExoticComponent<ChipProps & React.RefAttributes<HTMLDivElement>>;
|
|
17
|
+
/**
|
|
18
|
+
* Token-driven chip that composes existing primitives (i.e. TagNumber) inside.
|
|
19
|
+
*/
|
|
20
|
+
type ChipProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
21
|
+
label?: React.ReactNode;
|
|
22
|
+
size?: "default" | "small";
|
|
23
|
+
onRemove?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
24
|
+
removeLabel?: string;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
removeButtonProps?: Omit<React.ComponentPropsWithRef<"button">, "type" | "disabled" | "aria-label" | "aria-labelledby" | "children">;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
interface ActionChipProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
30
|
+
/** Visible label; children are used when label is undefined. Supply noninteractive content. */
|
|
31
|
+
label?: React.ReactNode;
|
|
32
|
+
/** Chip-specific geometry, matching Chip's existing 40px/32px minimum heights. */
|
|
33
|
+
size?: "default" | "small";
|
|
34
|
+
/** Visual filter state only. Does not imply aria-pressed or popup expansion. */
|
|
35
|
+
selected?: boolean;
|
|
36
|
+
/** Noninteractive count content before the label. The host owns its visibility and value. */
|
|
37
|
+
count?: React.ReactNode;
|
|
38
|
+
startAdornment?: React.ReactNode;
|
|
39
|
+
endAdornment?: React.ReactNode;
|
|
40
|
+
/** Use the separate div-root Chip for removal. */
|
|
41
|
+
onRemove?: never;
|
|
42
|
+
removeLabel?: never;
|
|
43
|
+
removeButtonProps?: never;
|
|
44
|
+
}
|
|
45
|
+
/** Native action/overlay trigger. The host owns filter state and popup behaviour. */
|
|
46
|
+
declare const ActionChip: React.ForwardRefExoticComponent<ActionChipProps & React.RefAttributes<HTMLButtonElement>>;
|
|
15
47
|
|
|
16
|
-
export { Chip };
|
|
48
|
+
export { ActionChip, Chip };
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
29
29
|
// src/index.js
|
|
30
30
|
var index_exports = {};
|
|
31
31
|
__export(index_exports, {
|
|
32
|
+
ActionChip: () => ActionChip,
|
|
32
33
|
Chip: () => Chip
|
|
33
34
|
});
|
|
34
35
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -41,11 +42,11 @@ var import_jsx_runtime = require("react/jsx-runtime");
|
|
|
41
42
|
var SIZE_MAP = {
|
|
42
43
|
default: {
|
|
43
44
|
container: "text-sm min-h-[40px] px-8 py-6 gap-8",
|
|
44
|
-
close: "
|
|
45
|
+
close: "size-16"
|
|
45
46
|
},
|
|
46
47
|
small: {
|
|
47
|
-
container: "text-xs min-h-
|
|
48
|
-
close: "
|
|
48
|
+
container: "text-xs min-h-32 px-8 py-4 gap-6",
|
|
49
|
+
close: "size-16"
|
|
49
50
|
}
|
|
50
51
|
};
|
|
51
52
|
var BASE_STYLES = (0, import_utils.cx)(
|
|
@@ -64,9 +65,15 @@ var Chip = (0, import_react.forwardRef)(function Chip2({
|
|
|
64
65
|
onRemove,
|
|
65
66
|
removeLabel = "Remove chip",
|
|
66
67
|
disabled = false,
|
|
68
|
+
removeButtonProps = {},
|
|
67
69
|
className,
|
|
68
70
|
...rest
|
|
69
71
|
}, ref) {
|
|
72
|
+
const {
|
|
73
|
+
className: removeClassName,
|
|
74
|
+
onClick: onRemoveClick,
|
|
75
|
+
...buttonProps
|
|
76
|
+
} = removeButtonProps;
|
|
70
77
|
const sizeStyles = SIZE_MAP[size] ?? SIZE_MAP.default;
|
|
71
78
|
const containerClass = (0, import_utils.cx)(
|
|
72
79
|
BASE_STYLES,
|
|
@@ -86,6 +93,7 @@ var Chip = (0, import_react.forwardRef)(function Chip2({
|
|
|
86
93
|
className: containerClass,
|
|
87
94
|
"data-disabled": disabled ? "true" : void 0,
|
|
88
95
|
...rest,
|
|
96
|
+
"aria-disabled": disabled ? true : rest["aria-disabled"],
|
|
89
97
|
children: [
|
|
90
98
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "flex min-w-0 flex-1 items-center gap-6", children: [
|
|
91
99
|
shouldRenderChildren && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "flex items-center gap-4 shrink-0 [&>*]:shrink-0", children: normalizedChildren.map(
|
|
@@ -96,12 +104,16 @@ var Chip = (0, import_react.forwardRef)(function Chip2({
|
|
|
96
104
|
onRemove && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
97
105
|
"button",
|
|
98
106
|
{
|
|
107
|
+
...buttonProps,
|
|
99
108
|
type: "button",
|
|
100
109
|
disabled,
|
|
101
110
|
"aria-label": removeLabel,
|
|
111
|
+
"aria-labelledby": void 0,
|
|
102
112
|
onClick: (event) => {
|
|
103
113
|
event.stopPropagation();
|
|
104
|
-
|
|
114
|
+
if (disabled) return;
|
|
115
|
+
onRemoveClick?.(event);
|
|
116
|
+
if (!event.defaultPrevented) onRemove(event);
|
|
105
117
|
},
|
|
106
118
|
className: (0, import_utils.cx)(
|
|
107
119
|
"ml-4 inline-flex items-center justify-center rounded-full text-fg-subtle",
|
|
@@ -110,7 +122,8 @@ var Chip = (0, import_react.forwardRef)(function Chip2({
|
|
|
110
122
|
"focus-visible:shadow-focus focus-visible:outline-none",
|
|
111
123
|
"active:bg-bg-primary-subtle-90",
|
|
112
124
|
disabled && "opacity-60 pointer-events-none bg-transparent text-fg-inactive",
|
|
113
|
-
sizeStyles.close
|
|
125
|
+
sizeStyles.close,
|
|
126
|
+
removeClassName
|
|
114
127
|
),
|
|
115
128
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.Close, {})
|
|
116
129
|
}
|
|
@@ -119,7 +132,69 @@ var Chip = (0, import_react.forwardRef)(function Chip2({
|
|
|
119
132
|
}
|
|
120
133
|
);
|
|
121
134
|
});
|
|
135
|
+
|
|
136
|
+
// src/elements/ActionChip.tsx
|
|
137
|
+
var import_react2 = require("react");
|
|
138
|
+
var import_utils2 = require("@edux-design/utils");
|
|
139
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
140
|
+
var hasSlot = (node) => node != null && typeof node !== "boolean";
|
|
141
|
+
var sizes = {
|
|
142
|
+
default: "text-sm min-h-[40px] px-8 py-6 gap-8",
|
|
143
|
+
small: "text-xs min-h-32 px-8 py-4 gap-6"
|
|
144
|
+
};
|
|
145
|
+
var ActionChip = (0, import_react2.forwardRef)(
|
|
146
|
+
function ActionChip2({
|
|
147
|
+
label,
|
|
148
|
+
children,
|
|
149
|
+
size = "default",
|
|
150
|
+
selected = false,
|
|
151
|
+
count,
|
|
152
|
+
startAdornment,
|
|
153
|
+
endAdornment,
|
|
154
|
+
disabled = false,
|
|
155
|
+
type = "button",
|
|
156
|
+
className,
|
|
157
|
+
// Do not leak unsupported removal props onto the native button in JS hosts.
|
|
158
|
+
onRemove: _onRemove,
|
|
159
|
+
removeLabel: _removeLabel,
|
|
160
|
+
removeButtonProps: _removeButtonProps,
|
|
161
|
+
...props
|
|
162
|
+
}, ref) {
|
|
163
|
+
const expanded = props["aria-expanded"] === true || props["aria-expanded"] === "true";
|
|
164
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
165
|
+
"button",
|
|
166
|
+
{
|
|
167
|
+
...props,
|
|
168
|
+
ref,
|
|
169
|
+
type,
|
|
170
|
+
disabled,
|
|
171
|
+
"data-selected": selected ? "true" : void 0,
|
|
172
|
+
className: (0, import_utils2.cx)(
|
|
173
|
+
"inline-flex w-fit max-w-full min-w-0 items-center rounded-full border border-border-minimal bg-bg-base text-fg-base shadow-sm",
|
|
174
|
+
"transition-all duration-200 ease-out cursor-pointer",
|
|
175
|
+
"hover:border-border-primary-base hover:bg-bg-primary-subtle",
|
|
176
|
+
"active:border-border-primary-emphasis active:bg-bg-primary-subtle-90",
|
|
177
|
+
"focus-visible:shadow-focus focus-visible:border-border-emphasis focus-visible:outline-none",
|
|
178
|
+
"data-[state=open]:border-border-primary-base data-[state=open]:bg-bg-primary-subtle",
|
|
179
|
+
sizes[size],
|
|
180
|
+
(selected || expanded) && "border-border-primary-base bg-bg-primary-subtle",
|
|
181
|
+
disabled && "bg-bg-inactive text-fg-inactive border-border-subtle cursor-not-allowed pointer-events-none",
|
|
182
|
+
className
|
|
183
|
+
),
|
|
184
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "flex min-w-0 flex-1 items-center gap-6", children: [
|
|
185
|
+
hasSlot(startAdornment) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "inline-flex shrink-0 items-center", children: startAdornment }),
|
|
186
|
+
hasSlot(count) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "inline-flex shrink-0 items-center", children: count }),
|
|
187
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "flex min-w-0 items-center gap-8", children: [
|
|
188
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "min-w-0 truncate text-left", children: label === void 0 ? children : label }),
|
|
189
|
+
hasSlot(endAdornment) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "inline-flex shrink-0 items-center", children: endAdornment })
|
|
190
|
+
] })
|
|
191
|
+
] })
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
);
|
|
122
196
|
// Annotate the CommonJS export names for ESM import in node:
|
|
123
197
|
0 && (module.exports = {
|
|
198
|
+
ActionChip,
|
|
124
199
|
Chip
|
|
125
200
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -6,11 +6,11 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
var SIZE_MAP = {
|
|
7
7
|
default: {
|
|
8
8
|
container: "text-sm min-h-[40px] px-8 py-6 gap-8",
|
|
9
|
-
close: "
|
|
9
|
+
close: "size-16"
|
|
10
10
|
},
|
|
11
11
|
small: {
|
|
12
|
-
container: "text-xs min-h-
|
|
13
|
-
close: "
|
|
12
|
+
container: "text-xs min-h-32 px-8 py-4 gap-6",
|
|
13
|
+
close: "size-16"
|
|
14
14
|
}
|
|
15
15
|
};
|
|
16
16
|
var BASE_STYLES = cx(
|
|
@@ -29,9 +29,15 @@ var Chip = forwardRef(function Chip2({
|
|
|
29
29
|
onRemove,
|
|
30
30
|
removeLabel = "Remove chip",
|
|
31
31
|
disabled = false,
|
|
32
|
+
removeButtonProps = {},
|
|
32
33
|
className,
|
|
33
34
|
...rest
|
|
34
35
|
}, ref) {
|
|
36
|
+
const {
|
|
37
|
+
className: removeClassName,
|
|
38
|
+
onClick: onRemoveClick,
|
|
39
|
+
...buttonProps
|
|
40
|
+
} = removeButtonProps;
|
|
35
41
|
const sizeStyles = SIZE_MAP[size] ?? SIZE_MAP.default;
|
|
36
42
|
const containerClass = cx(
|
|
37
43
|
BASE_STYLES,
|
|
@@ -51,6 +57,7 @@ var Chip = forwardRef(function Chip2({
|
|
|
51
57
|
className: containerClass,
|
|
52
58
|
"data-disabled": disabled ? "true" : void 0,
|
|
53
59
|
...rest,
|
|
60
|
+
"aria-disabled": disabled ? true : rest["aria-disabled"],
|
|
54
61
|
children: [
|
|
55
62
|
/* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-6", children: [
|
|
56
63
|
shouldRenderChildren && /* @__PURE__ */ jsx("span", { className: "flex items-center gap-4 shrink-0 [&>*]:shrink-0", children: normalizedChildren.map(
|
|
@@ -61,12 +68,16 @@ var Chip = forwardRef(function Chip2({
|
|
|
61
68
|
onRemove && /* @__PURE__ */ jsx(
|
|
62
69
|
"button",
|
|
63
70
|
{
|
|
71
|
+
...buttonProps,
|
|
64
72
|
type: "button",
|
|
65
73
|
disabled,
|
|
66
74
|
"aria-label": removeLabel,
|
|
75
|
+
"aria-labelledby": void 0,
|
|
67
76
|
onClick: (event) => {
|
|
68
77
|
event.stopPropagation();
|
|
69
|
-
|
|
78
|
+
if (disabled) return;
|
|
79
|
+
onRemoveClick?.(event);
|
|
80
|
+
if (!event.defaultPrevented) onRemove(event);
|
|
70
81
|
},
|
|
71
82
|
className: cx(
|
|
72
83
|
"ml-4 inline-flex items-center justify-center rounded-full text-fg-subtle",
|
|
@@ -75,7 +86,8 @@ var Chip = forwardRef(function Chip2({
|
|
|
75
86
|
"focus-visible:shadow-focus focus-visible:outline-none",
|
|
76
87
|
"active:bg-bg-primary-subtle-90",
|
|
77
88
|
disabled && "opacity-60 pointer-events-none bg-transparent text-fg-inactive",
|
|
78
|
-
sizeStyles.close
|
|
89
|
+
sizeStyles.close,
|
|
90
|
+
removeClassName
|
|
79
91
|
),
|
|
80
92
|
children: /* @__PURE__ */ jsx(Close, {})
|
|
81
93
|
}
|
|
@@ -84,6 +96,68 @@ var Chip = forwardRef(function Chip2({
|
|
|
84
96
|
}
|
|
85
97
|
);
|
|
86
98
|
});
|
|
99
|
+
|
|
100
|
+
// src/elements/ActionChip.tsx
|
|
101
|
+
import { forwardRef as forwardRef2 } from "react";
|
|
102
|
+
import { cx as cx2 } from "@edux-design/utils";
|
|
103
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
104
|
+
var hasSlot = (node) => node != null && typeof node !== "boolean";
|
|
105
|
+
var sizes = {
|
|
106
|
+
default: "text-sm min-h-[40px] px-8 py-6 gap-8",
|
|
107
|
+
small: "text-xs min-h-32 px-8 py-4 gap-6"
|
|
108
|
+
};
|
|
109
|
+
var ActionChip = forwardRef2(
|
|
110
|
+
function ActionChip2({
|
|
111
|
+
label,
|
|
112
|
+
children,
|
|
113
|
+
size = "default",
|
|
114
|
+
selected = false,
|
|
115
|
+
count,
|
|
116
|
+
startAdornment,
|
|
117
|
+
endAdornment,
|
|
118
|
+
disabled = false,
|
|
119
|
+
type = "button",
|
|
120
|
+
className,
|
|
121
|
+
// Do not leak unsupported removal props onto the native button in JS hosts.
|
|
122
|
+
onRemove: _onRemove,
|
|
123
|
+
removeLabel: _removeLabel,
|
|
124
|
+
removeButtonProps: _removeButtonProps,
|
|
125
|
+
...props
|
|
126
|
+
}, ref) {
|
|
127
|
+
const expanded = props["aria-expanded"] === true || props["aria-expanded"] === "true";
|
|
128
|
+
return /* @__PURE__ */ jsx2(
|
|
129
|
+
"button",
|
|
130
|
+
{
|
|
131
|
+
...props,
|
|
132
|
+
ref,
|
|
133
|
+
type,
|
|
134
|
+
disabled,
|
|
135
|
+
"data-selected": selected ? "true" : void 0,
|
|
136
|
+
className: cx2(
|
|
137
|
+
"inline-flex w-fit max-w-full min-w-0 items-center rounded-full border border-border-minimal bg-bg-base text-fg-base shadow-sm",
|
|
138
|
+
"transition-all duration-200 ease-out cursor-pointer",
|
|
139
|
+
"hover:border-border-primary-base hover:bg-bg-primary-subtle",
|
|
140
|
+
"active:border-border-primary-emphasis active:bg-bg-primary-subtle-90",
|
|
141
|
+
"focus-visible:shadow-focus focus-visible:border-border-emphasis focus-visible:outline-none",
|
|
142
|
+
"data-[state=open]:border-border-primary-base data-[state=open]:bg-bg-primary-subtle",
|
|
143
|
+
sizes[size],
|
|
144
|
+
(selected || expanded) && "border-border-primary-base bg-bg-primary-subtle",
|
|
145
|
+
disabled && "bg-bg-inactive text-fg-inactive border-border-subtle cursor-not-allowed pointer-events-none",
|
|
146
|
+
className
|
|
147
|
+
),
|
|
148
|
+
children: /* @__PURE__ */ jsxs2("span", { className: "flex min-w-0 flex-1 items-center gap-6", children: [
|
|
149
|
+
hasSlot(startAdornment) && /* @__PURE__ */ jsx2("span", { className: "inline-flex shrink-0 items-center", children: startAdornment }),
|
|
150
|
+
hasSlot(count) && /* @__PURE__ */ jsx2("span", { className: "inline-flex shrink-0 items-center", children: count }),
|
|
151
|
+
/* @__PURE__ */ jsxs2("span", { className: "flex min-w-0 items-center gap-8", children: [
|
|
152
|
+
/* @__PURE__ */ jsx2("span", { className: "min-w-0 truncate text-left", children: label === void 0 ? children : label }),
|
|
153
|
+
hasSlot(endAdornment) && /* @__PURE__ */ jsx2("span", { className: "inline-flex shrink-0 items-center", children: endAdornment })
|
|
154
|
+
] })
|
|
155
|
+
] })
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
);
|
|
87
160
|
export {
|
|
161
|
+
ActionChip,
|
|
88
162
|
Chip
|
|
89
163
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# DS-20 host inventory
|
|
2
|
+
|
|
3
|
+
Read-only inventory captured 29 October 2026. No Payload source is changed by
|
|
4
|
+
this package work.
|
|
5
|
+
|
|
6
|
+
| Host | Current composition | Required behaviour |
|
|
7
|
+
| ---------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
8
|
+
| `ManuscriptFilterChip.jsx` | Div-root Chip with role, tab index, manual Enter/Space activation, TagNumber and Chevron | Native button ref/events, selected styling independent of popup expansion, count and adornment slots |
|
|
9
|
+
| `ManuScriptsHome.jsx` Stages | Controlled Menu, selected status count and filter-active visual | Native Menu trigger; count/filter state stays with host |
|
|
10
|
+
| `ManuScriptsHome.jsx` sort | Controlled Menu with dynamic sort label | Expansion visual without toggle-button semantics |
|
|
11
|
+
| `ManuscriptSeriesFilter.jsx` | Controlled Menu with selected series count and selected filter visual | Forwarded trigger ref/ARIA/events; selection remains controlled by host |
|
|
12
|
+
| `MetaDataHome.jsx` Status | Menu trigger wraps Chip in a div, count is selected count or total | Candidate native trigger adoption, preserving the host's count policy |
|
|
13
|
+
| `MetaDataHome.jsx` sort | Menu trigger wraps a dynamic-label Chip in a div | Candidate native trigger adoption |
|
|
14
|
+
|
|
15
|
+
Sources: audit round 2 README DS-20, review-1 section 3, review-3 R3-03;
|
|
16
|
+
`payload-client/src/features/Manuscripts/components/ManuscriptFilterChip.jsx`,
|
|
17
|
+
`ManuscriptSeriesFilter.jsx`, `elements/ManuScriptsHome.jsx`, and
|
|
18
|
+
`features/Metadata/pages/MetaDataHome.jsx` in the read-only Payload checkout.
|
|
19
|
+
|
|
20
|
+
The existing Chip root remains a div. Its removal button, callback composition,
|
|
21
|
+
ref types and disabled semantics are a separate contract from an action trigger.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# ActionChip
|
|
2
|
+
|
|
3
|
+
`ActionChip` is a native button for compact filter actions and Menu/Popover
|
|
4
|
+
triggers. The existing `Chip` remains a div with an optional removal button.
|
|
5
|
+
|
|
6
|
+
```tsx
|
|
7
|
+
import { ActionChip } from "@edux-design/chips";
|
|
8
|
+
import { TagNumber } from "@edux-design/tags";
|
|
9
|
+
import { Chevron } from "@edux-design/icons";
|
|
10
|
+
import { Menu, MenuList, MenuItem } from "@edux-design/menus";
|
|
11
|
+
|
|
12
|
+
<Menu
|
|
13
|
+
button={
|
|
14
|
+
<ActionChip
|
|
15
|
+
label="Stages"
|
|
16
|
+
selected={selectedStages.length > 0}
|
|
17
|
+
count={
|
|
18
|
+
selectedStages.length > 0 ? (
|
|
19
|
+
<TagNumber variant="brand" tone="solid">
|
|
20
|
+
{selectedStages.length}
|
|
21
|
+
</TagNumber>
|
|
22
|
+
) : null
|
|
23
|
+
}
|
|
24
|
+
endAdornment={<Chevron aria-hidden="true" />}
|
|
25
|
+
/>
|
|
26
|
+
}
|
|
27
|
+
>
|
|
28
|
+
<MenuList>
|
|
29
|
+
<MenuItem onClick={chooseDraft}>Draft</MenuItem>
|
|
30
|
+
</MenuList>
|
|
31
|
+
</Menu>;
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The Menu clones its trigger and supplies popup ARIA, handlers and a composed
|
|
35
|
+
button ref. Popover uses `<PopoverTrigger asChild><ActionChip ... /></PopoverTrigger>`.
|
|
36
|
+
Both compositions retain native Enter/Space/click activation, handler cancellation
|
|
37
|
+
with `event.preventDefault()`, disabled behaviour, Escape dismissal and focus
|
|
38
|
+
restoration. No role, tab index or synthetic keyboard-to-click adapter is needed.
|
|
39
|
+
|
|
40
|
+
| Prop | Contract |
|
|
41
|
+
| ------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
42
|
+
| `ref` | `HTMLButtonElement`; Chip still accepts `HTMLDivElement` |
|
|
43
|
+
| `label` | Noninteractive React content; native `children` is the fallback when label is undefined |
|
|
44
|
+
| `count` | Noninteractive React content before label; host owns value, visibility and accessible naming |
|
|
45
|
+
| `startAdornment`, `endAdornment` | Noninteractive content, such as an icon or chevron |
|
|
46
|
+
| `selected` | Filter-applied visual and `data-selected="true"` only; never sets `aria-pressed` |
|
|
47
|
+
| `aria-expanded` / `data-state="open"` | Independently highlights an open popup |
|
|
48
|
+
| `aria-pressed` | Optional native attribute only when the action is an actual toggle |
|
|
49
|
+
| `disabled` | Native disabled button semantics and muted token colours |
|
|
50
|
+
| `type` | Native button type, default `"button"`; explicit submit/reset remain supported |
|
|
51
|
+
| `size` | `"default"` (40px minimum) or `"small"` (32px minimum), matching existing Chip geometry |
|
|
52
|
+
| Other native button props | Forwarded, including ARIA, form attributes and events |
|
|
53
|
+
|
|
54
|
+
`onRemove`, `removeLabel` and `removeButtonProps` are prohibited by the public
|
|
55
|
+
ActionChip type. Use `Chip` for removal. Do not place buttons, links, form fields,
|
|
56
|
+
removable Chips or other interactive descendants in any label/count/adornment
|
|
57
|
+
slot. Decorative icons should be hidden from assistive technology. Supply an
|
|
58
|
+
accessible name when there is no visible text; for a terse count, an explicit
|
|
59
|
+
`aria-label` can state the filter name and selected count together.
|
|
60
|
+
|
|
61
|
+
The chip minimum heights intentionally remain component-owned 40px and the
|
|
62
|
+
existing 32px spacing token; mapping to the control-size family would alter
|
|
63
|
+
established compact filter geometry. Existing spacing, colours, shadows, radii
|
|
64
|
+
and focus tokens are reused without changing the token package. The label
|
|
65
|
+
truncates in a constrained parent while count and adornments remain visible.
|
|
66
|
+
|
|
67
|
+
The host owns filter choices, loading/search, selection changes, popup state and
|
|
68
|
+
counts. This component does not manage pressed/open state or emit separate state
|
|
69
|
+
callbacks. The host should replace the old ManuscriptFilterChip's div keyboard
|
|
70
|
+
adapter with ActionChip when it adopts a released package; see
|
|
71
|
+
[action-chip-inventory.md](./action-chip-inventory.md) for the inspected hosts.
|
|
72
|
+
|
|
73
|
+
Verification: `src/elements/ActionChip.test.tsx`, strict public fixtures in
|
|
74
|
+
`test/action-chip-api.tsx`, CJS/SSR in `test/action-chip-public-runtime.cjs`,
|
|
75
|
+
four strict-a11y stories, and `node packages/chips/test/action-chip.browser.mjs`.
|
|
76
|
+
The browser suite compares both sizes against legacy Chip geometry in blue/teal,
|
|
77
|
+
light/dark; checks native once-only pointer/Enter/Space activation, real overlay
|
|
78
|
+
Escape/focus restoration, disabled state, and narrow long-label truncation.
|
|
79
|
+
|
|
80
|
+
The Menu composition story counts only true `onOpenChange` requests. Menu owns
|
|
81
|
+
its trigger opening policy, and may handle keyboard events before a native click
|
|
82
|
+
is emitted. Child `onClick` is not an overlay-open notification contract. The
|
|
83
|
+
standalone ActionChip tests separately verify one native click per pointer, Enter
|
|
84
|
+
or Space activation. Keyboard story sequences await Escape focus restoration
|
|
85
|
+
before sending the next activation key.
|
package/docs/removal.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Removal, descriptions and focus
|
|
2
|
+
|
|
3
|
+
`Chip` remains a static native div with a forwarded `HTMLDivElement` ref. Passing
|
|
4
|
+
`onRemove` renders its existing native button. Set a specific `removeLabel`, such
|
|
5
|
+
as `Remove AC9M3N01`.
|
|
6
|
+
|
|
7
|
+
`removeButtonProps` forwards attributes, native handlers, style, className and an
|
|
8
|
+
`HTMLButtonElement` ref to that same button. It adds no wrapper or tab stop. Use
|
|
9
|
+
`aria-describedby` to associate host-owned context with the actual remove action;
|
|
10
|
+
descriptions on an ancestor are not inherited. A visual tooltip can still use
|
|
11
|
+
`TooltipTrigger asChild` around the Chip root.
|
|
12
|
+
|
|
13
|
+
Chip owns the remove button's `type="button"`, `disabled`, accessible name and
|
|
14
|
+
icon children. `type`, `disabled`, `aria-label`, `aria-labelledby` and `children`
|
|
15
|
+
are excluded from the public slot props and cannot override those core semantics.
|
|
16
|
+
The slot's click handler runs before `onRemove`; `event.preventDefault()` cancels
|
|
17
|
+
removal. Click propagation is stopped, including when cancelled, so an ancestor
|
|
18
|
+
does not also toggle the selection. A disabled Chip invokes neither click
|
|
19
|
+
callback. Other native events, including keyboard and focus handlers, forward
|
|
20
|
+
normally. Class names merge with the existing button presentation.
|
|
21
|
+
|
|
22
|
+
Collection state and focus after removal belong to the host. Use the public
|
|
23
|
+
button refs to focus the next enabled remove action, then the previous action,
|
|
24
|
+
then a stable host control when the last chip disappears. Do not query private
|
|
25
|
+
button markup or focus the noninteractive root. The Selection adoption story
|
|
26
|
+
demonstrates this recipe with descriptions and keyboard/pointer removal.
|
package/package.json
CHANGED
|
@@ -1,36 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edux-design/chips",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"**/*.css"
|
|
7
7
|
],
|
|
8
8
|
"files": [
|
|
9
|
-
"dist"
|
|
9
|
+
"dist",
|
|
10
|
+
"docs",
|
|
11
|
+
"README.md",
|
|
12
|
+
"CHANGELOG.md"
|
|
10
13
|
],
|
|
11
14
|
"types": "./dist/index.d.ts",
|
|
12
15
|
"exports": {
|
|
13
16
|
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
14
18
|
"import": "./dist/index.mjs",
|
|
15
|
-
"require": "./dist/index.js"
|
|
16
|
-
"types": "./dist/index.d.ts"
|
|
19
|
+
"require": "./dist/index.js"
|
|
17
20
|
}
|
|
18
21
|
},
|
|
19
|
-
"scripts": {
|
|
20
|
-
"lint": "eslint . --max-warnings 0",
|
|
21
|
-
"build": "tsup src/index.js --format esm,cjs --dts",
|
|
22
|
-
"dev": "tsup src/index.js --watch --format esm,cjs --dts",
|
|
23
|
-
"generate:component": "turbo gen react-component",
|
|
24
|
-
"check-types": "tsc --noEmit"
|
|
25
|
-
},
|
|
26
|
-
"devDependencies": {
|
|
27
|
-
"typescript": "^5.9.2",
|
|
28
|
-
"tailwindcss": "^4.1.12",
|
|
29
|
-
"postcss": "^8.5.6",
|
|
30
|
-
"tsup": "^8.5.0",
|
|
31
|
-
"@tailwindcss/cli": "^4.1.12",
|
|
32
|
-
"@tailwindcss/postcss": "^4.1.12"
|
|
33
|
-
},
|
|
34
22
|
"dependencies": {
|
|
35
23
|
"@edux-design/utils": "*",
|
|
36
24
|
"@edux-design/icons": "*"
|