@cosxai/ui 0.24.0 → 0.25.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/package.json +1 -1
- package/src/primitives/Chip.tsx +15 -1
- package/src/primitives/Combobox.tsx +34 -2
- package/src/primitives/Input.tsx +47 -4
- package/src/primitives/MultiCombobox.tsx +191 -43
- package/src/primitives/index.ts +6 -1
- package/src/styles/index.css +37 -0
package/package.json
CHANGED
package/src/primitives/Chip.tsx
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
type CSSProperties,
|
|
4
|
+
type HTMLAttributes,
|
|
5
|
+
type MouseEvent,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from "react";
|
|
2
8
|
|
|
3
9
|
import { cn } from "../lib/cn";
|
|
4
10
|
|
|
@@ -99,6 +105,12 @@ export const Chip = forwardRef<HTMLSpanElement, ChipProps>(function Chip(
|
|
|
99
105
|
ref,
|
|
100
106
|
) {
|
|
101
107
|
const clickable = !!onClick && !disabled;
|
|
108
|
+
// Chip buttons act on click but must NOT take focus on mousedown:
|
|
109
|
+
// when a host has an inline editor open for another chip, the
|
|
110
|
+
// editor's blur (commit + unmount) would otherwise run before this
|
|
111
|
+
// click lands and the click is lost. Keyboard focus via Tab is
|
|
112
|
+
// unaffected.
|
|
113
|
+
const keepFocus = (e: MouseEvent<HTMLButtonElement>): void => e.preventDefault();
|
|
102
114
|
return (
|
|
103
115
|
<span
|
|
104
116
|
ref={ref}
|
|
@@ -131,6 +143,7 @@ export const Chip = forwardRef<HTMLSpanElement, ChipProps>(function Chip(
|
|
|
131
143
|
<button
|
|
132
144
|
type="button"
|
|
133
145
|
className="ck-chip-label"
|
|
146
|
+
onMouseDown={keepFocus}
|
|
134
147
|
onClick={onClick}
|
|
135
148
|
disabled={disabled}
|
|
136
149
|
style={{ ...LABEL_STYLE, cursor: clickable ? "pointer" : "default" }}
|
|
@@ -147,6 +160,7 @@ export const Chip = forwardRef<HTMLSpanElement, ChipProps>(function Chip(
|
|
|
147
160
|
type="button"
|
|
148
161
|
className="ck-chip-remove"
|
|
149
162
|
aria-label={removeLabel ?? "Remove"}
|
|
163
|
+
onMouseDown={keepFocus}
|
|
150
164
|
onClick={onRemove}
|
|
151
165
|
disabled={disabled}
|
|
152
166
|
style={{
|
|
@@ -104,9 +104,25 @@ export interface ComboboxProps {
|
|
|
104
104
|
// and typically passes `committed={null}`. Default false — the
|
|
105
105
|
// single-value echo-into-input behaviour is unchanged.
|
|
106
106
|
clearOnCommit?: boolean | undefined;
|
|
107
|
+
// Bare mode: the input renders without its own field chrome (see
|
|
108
|
+
// Input `bare`) and the root wrapper is `position: static` +
|
|
109
|
+
// `flex: 1 1 140px`, so a host that draws the field (MultiCombobox
|
|
110
|
+
// inline layout) can place this after its chips and the dropdown
|
|
111
|
+
// anchors to the host's `position: relative` container. `label`
|
|
112
|
+
// and the inline error text are not rendered (the host draws them;
|
|
113
|
+
// `aria-invalid` is still set on the input). Default false.
|
|
114
|
+
bare?: boolean | undefined;
|
|
115
|
+
// id for the <input>, so a host-drawn <label htmlFor> can target it.
|
|
116
|
+
inputId?: string | undefined;
|
|
117
|
+
// Extra aria-describedby id(s) for the <input> (e.g. a host-drawn
|
|
118
|
+
// invalid hint in bare mode). Merged with Input's own error/helper
|
|
119
|
+
// association, never replacing it.
|
|
120
|
+
inputDescribedBy?: string | undefined;
|
|
107
121
|
testid?: string | undefined;
|
|
108
122
|
}
|
|
109
123
|
|
|
124
|
+
export const COMBOBOX_DEFAULT_INVALID_HINT = "Pick from the list to continue.";
|
|
125
|
+
|
|
110
126
|
// Imperative handle — `focus()` returns focus to the input (e.g. after
|
|
111
127
|
// an inline editor closes); `clear()` wipes the typed text + dropdown
|
|
112
128
|
// (e.g. a parent rejecting a duplicate commit).
|
|
@@ -144,6 +160,9 @@ export const Combobox = forwardRef<ComboboxHandle, ComboboxProps>(function Combo
|
|
|
144
160
|
onCommit,
|
|
145
161
|
onUncommit,
|
|
146
162
|
clearOnCommit = false,
|
|
163
|
+
bare = false,
|
|
164
|
+
inputId,
|
|
165
|
+
inputDescribedBy,
|
|
147
166
|
testid = "combobox",
|
|
148
167
|
},
|
|
149
168
|
ref,
|
|
@@ -252,6 +271,10 @@ export const Combobox = forwardRef<ComboboxHandle, ComboboxProps>(function Combo
|
|
|
252
271
|
// blurs first) still lands on the row handler.
|
|
253
272
|
const handleBlur = useCallback(() => {
|
|
254
273
|
setTimeout(() => {
|
|
274
|
+
// Re-focused within the delay (e.g. a host frame click handing
|
|
275
|
+
// focus straight back, MultiCombobox inline): still editing —
|
|
276
|
+
// neither close the list nor auto-commit.
|
|
277
|
+
if (inputRef.current && document.activeElement === inputRef.current) return;
|
|
255
278
|
setFocused(false);
|
|
256
279
|
if (isCommitted) return;
|
|
257
280
|
const raw = trimmed;
|
|
@@ -322,9 +345,18 @@ export const Combobox = forwardRef<ComboboxHandle, ComboboxProps>(function Combo
|
|
|
322
345
|
focused && !isCommitted && (results.length > 0 || offerFree || (hasNoResults && !!emptyHint));
|
|
323
346
|
|
|
324
347
|
return (
|
|
325
|
-
<div
|
|
348
|
+
<div
|
|
349
|
+
style={
|
|
350
|
+
bare
|
|
351
|
+
? { position: "static", flex: "1 1 140px", minWidth: 140 }
|
|
352
|
+
: { position: "relative" }
|
|
353
|
+
}
|
|
354
|
+
>
|
|
326
355
|
<Input
|
|
327
356
|
ref={inputRef}
|
|
357
|
+
id={inputId}
|
|
358
|
+
aria-describedby={inputDescribedBy}
|
|
359
|
+
bare={bare}
|
|
328
360
|
label={label}
|
|
329
361
|
data-testid={`${testid}-input`}
|
|
330
362
|
value={query}
|
|
@@ -339,7 +371,7 @@ export const Combobox = forwardRef<ComboboxHandle, ComboboxProps>(function Combo
|
|
|
339
371
|
placeholder={placeholder}
|
|
340
372
|
autoFocus={autoFocus}
|
|
341
373
|
autoComplete="off"
|
|
342
|
-
error={invalid ? (invalidHint ??
|
|
374
|
+
error={invalid ? (invalidHint ?? COMBOBOX_DEFAULT_INVALID_HINT) : null}
|
|
343
375
|
/>
|
|
344
376
|
|
|
345
377
|
{showDropdown ? (
|
package/src/primitives/Input.tsx
CHANGED
|
@@ -33,6 +33,14 @@ export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>,
|
|
|
33
33
|
// currency symbols, unit labels, search icons, etc.
|
|
34
34
|
prefix?: ReactNode;
|
|
35
35
|
suffix?: ReactNode;
|
|
36
|
+
// Bare mode: render ONLY the <input> — no wrapper, label, helper,
|
|
37
|
+
// error text, or field chrome (border / background / height). For
|
|
38
|
+
// hosts that draw the field themselves (MultiCombobox's inline
|
|
39
|
+
// layout, where chips and the input share one frame). `label`,
|
|
40
|
+
// `helper`, `error` text, `prefix`, `suffix` and `fit` are ignored;
|
|
41
|
+
// `aria-invalid` is still set from `error` so the host can react.
|
|
42
|
+
// Default false.
|
|
43
|
+
bare?: boolean;
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
const ADDON_STYLE = {
|
|
@@ -52,12 +60,47 @@ const ADDON_STYLE = {
|
|
|
52
60
|
};
|
|
53
61
|
|
|
54
62
|
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|
55
|
-
{ label, helper, error, fit = "full", prefix, suffix, className, id, style, ...rest },
|
|
63
|
+
{ label, helper, error, fit = "full", prefix, suffix, bare = false, className, id, style, ...rest },
|
|
56
64
|
ref,
|
|
57
65
|
) {
|
|
58
66
|
const autoId = useId();
|
|
59
67
|
const inputId = id ?? autoId;
|
|
60
|
-
const hasAddon = prefix != null || suffix != null;
|
|
68
|
+
const hasAddon = !bare && (prefix != null || suffix != null);
|
|
69
|
+
// `...rest` is spread FIRST so the `error`-derived aria-invalid (and,
|
|
70
|
+
// non-bare, the helper/error text association) stays authoritative;
|
|
71
|
+
// a consumer aria-describedby is merged with ours, not replaced.
|
|
72
|
+
const ownDescribedBy = !bare && (helper || error) ? `${inputId}-helper` : undefined;
|
|
73
|
+
const describedBy =
|
|
74
|
+
[rest["aria-describedby"], ownDescribedBy].filter(Boolean).join(" ") || undefined;
|
|
75
|
+
|
|
76
|
+
if (bare) {
|
|
77
|
+
// Chrome presets restyle .ck-input with !important; the matching
|
|
78
|
+
// .ck-input--bare rule in styles/index.css strips that again.
|
|
79
|
+
return (
|
|
80
|
+
<input
|
|
81
|
+
ref={ref}
|
|
82
|
+
id={inputId}
|
|
83
|
+
{...rest}
|
|
84
|
+
aria-invalid={error ? true : undefined}
|
|
85
|
+
aria-describedby={describedBy}
|
|
86
|
+
className={cn("ck-input", "ck-input--bare", className)}
|
|
87
|
+
style={{
|
|
88
|
+
minWidth: 0,
|
|
89
|
+
width: "100%",
|
|
90
|
+
height: 28,
|
|
91
|
+
padding: "0 4px",
|
|
92
|
+
font: "400 13px/1 var(--ck-font-sans)",
|
|
93
|
+
background: "transparent",
|
|
94
|
+
color: "var(--ck-text-primary)",
|
|
95
|
+
border: "none",
|
|
96
|
+
borderRadius: 0,
|
|
97
|
+
boxShadow: "none",
|
|
98
|
+
outline: "none",
|
|
99
|
+
...(style ?? {}),
|
|
100
|
+
}}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
61
104
|
|
|
62
105
|
// Border / radius live on the WRAPPER when there's an addon so the
|
|
63
106
|
// addon + input share one continuous frame. Otherwise the input
|
|
@@ -69,9 +112,9 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|
|
69
112
|
<input
|
|
70
113
|
ref={ref}
|
|
71
114
|
id={inputId}
|
|
72
|
-
aria-invalid={error ? true : undefined}
|
|
73
|
-
aria-describedby={(helper || error) ? `${inputId}-helper` : undefined}
|
|
74
115
|
{...rest}
|
|
116
|
+
aria-invalid={error ? true : undefined}
|
|
117
|
+
aria-describedby={describedBy}
|
|
75
118
|
className={cn("ck-input", hasAddon ? "ck-input--with-addon" : undefined)}
|
|
76
119
|
style={{
|
|
77
120
|
flex: hasAddon ? "1 1 auto" : undefined,
|
|
@@ -1,7 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useId,
|
|
4
|
+
useImperativeHandle,
|
|
5
|
+
useRef,
|
|
6
|
+
type CSSProperties,
|
|
7
|
+
type KeyboardEvent,
|
|
8
|
+
type MouseEvent,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from "react";
|
|
2
11
|
|
|
3
12
|
import { Chip, type ChipTone } from "./Chip";
|
|
4
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
COMBOBOX_DEFAULT_INVALID_HINT,
|
|
15
|
+
Combobox,
|
|
16
|
+
type ComboboxHandle,
|
|
17
|
+
type ComboboxProps,
|
|
18
|
+
} from "./Combobox";
|
|
5
19
|
|
|
6
20
|
/**
|
|
7
21
|
* MultiCombobox — a Combobox that collects MANY values. The embedded
|
|
@@ -30,6 +44,14 @@ import { Combobox, type ComboboxHandle, type ComboboxProps } from "./Combobox";
|
|
|
30
44
|
* to the embedded Combobox's input, so Backspace inside an entry
|
|
31
45
|
* editor never removes anything.
|
|
32
46
|
*
|
|
47
|
+
* Layouts (`layout`): `stacked` (default) — the search field, then
|
|
48
|
+
* the chip row underneath. `inline` — a mail-client "To" field: one
|
|
49
|
+
* Input-like frame (`.ck-multi-combobox-field`) whose `<ul>` is the
|
|
50
|
+
* flex-wrap row: chips as `<li>`s and the bare search input in a last
|
|
51
|
+
* presentational `<li>`, so typing continues after the last chip and
|
|
52
|
+
* wraps with it. The label renders above as an eyebrow; the invalid
|
|
53
|
+
* hint (aria-describedby on the input), editor and hint stay below.
|
|
54
|
+
*
|
|
33
55
|
* Compose with: Chip (entries), Input (inside `renderEntryEditor`),
|
|
34
56
|
* dialogs collecting recipients.
|
|
35
57
|
*
|
|
@@ -59,11 +81,22 @@ export type MultiComboboxEntry = {
|
|
|
59
81
|
|
|
60
82
|
export type MultiComboboxHandle = ComboboxHandle;
|
|
61
83
|
|
|
84
|
+
export type MultiComboboxLayout = "stacked" | "inline";
|
|
85
|
+
|
|
62
86
|
export type MultiComboboxProps = Omit<
|
|
63
87
|
ComboboxProps,
|
|
64
|
-
|
|
88
|
+
| "committed"
|
|
89
|
+
| "committedExtra"
|
|
90
|
+
| "onUncommit"
|
|
91
|
+
| "clearOnCommit"
|
|
92
|
+
| "bare"
|
|
93
|
+
| "inputId"
|
|
94
|
+
| "inputDescribedBy"
|
|
65
95
|
> & {
|
|
66
96
|
entries: MultiComboboxEntry[];
|
|
97
|
+
// `stacked` (default): field, then chips underneath. `inline`:
|
|
98
|
+
// chips inside the field, input after the last chip.
|
|
99
|
+
layout?: MultiComboboxLayout | undefined;
|
|
67
100
|
onRemoveEntry: (key: string) => void;
|
|
68
101
|
// When set, chip labels become buttons (e.g. click-to-edit).
|
|
69
102
|
onEntryClick?: ((key: string) => void) | undefined;
|
|
@@ -80,6 +113,42 @@ export type MultiComboboxProps = Omit<
|
|
|
80
113
|
hint?: ReactNode | undefined;
|
|
81
114
|
};
|
|
82
115
|
|
|
116
|
+
// Inline layout: the host-drawn field mirrors Input's chrome. Focus
|
|
117
|
+
// ring / invalid border / disabled dim come from styles/index.css
|
|
118
|
+
// (`.ck-multi-combobox-field`).
|
|
119
|
+
const INLINE_FIELD_STYLE: CSSProperties = {
|
|
120
|
+
position: "relative",
|
|
121
|
+
display: "flex",
|
|
122
|
+
flexWrap: "wrap",
|
|
123
|
+
alignItems: "center",
|
|
124
|
+
gap: 6,
|
|
125
|
+
minHeight: 36,
|
|
126
|
+
padding: "4px 8px",
|
|
127
|
+
boxSizing: "border-box",
|
|
128
|
+
background: "var(--ck-bg-surface, #fff)",
|
|
129
|
+
border: "1px solid var(--ck-border-strong, #C5C9D2)",
|
|
130
|
+
borderRadius: "var(--ck-radius-sm, 6px)",
|
|
131
|
+
transition: "border-color var(--ck-dur-fast, 120ms) var(--ck-ease, ease)",
|
|
132
|
+
};
|
|
133
|
+
// The `<ul>` inside the frame is the actual flex-wrap row.
|
|
134
|
+
const INLINE_LIST_STYLE: CSSProperties = {
|
|
135
|
+
listStyle: "none",
|
|
136
|
+
margin: 0,
|
|
137
|
+
padding: 0,
|
|
138
|
+
width: "100%",
|
|
139
|
+
display: "flex",
|
|
140
|
+
flexWrap: "wrap",
|
|
141
|
+
alignItems: "center",
|
|
142
|
+
gap: 6,
|
|
143
|
+
};
|
|
144
|
+
// Last <li>: holds the bare Combobox (its root is `position: static`,
|
|
145
|
+
// so the dropdown anchors to the frame) and takes the remaining width.
|
|
146
|
+
const INLINE_COMBOBOX_ITEM: CSSProperties = {
|
|
147
|
+
flex: "1 1 140px",
|
|
148
|
+
minWidth: 140,
|
|
149
|
+
display: "flex",
|
|
150
|
+
};
|
|
151
|
+
|
|
83
152
|
export const MultiCombobox = forwardRef<MultiComboboxHandle, MultiComboboxProps>(
|
|
84
153
|
function MultiCombobox(
|
|
85
154
|
{
|
|
@@ -91,14 +160,29 @@ export const MultiCombobox = forwardRef<MultiComboboxHandle, MultiComboboxProps>
|
|
|
91
160
|
removeLabel,
|
|
92
161
|
backspaceRemovesLast = true,
|
|
93
162
|
hint,
|
|
163
|
+
layout = "stacked",
|
|
94
164
|
testid = "multi-combobox",
|
|
165
|
+
label,
|
|
95
166
|
...comboboxProps
|
|
96
167
|
},
|
|
97
168
|
ref,
|
|
98
169
|
) {
|
|
99
170
|
// Wraps ONLY the embedded Combobox so the Backspace handler can
|
|
100
171
|
// tell its input apart from any input inside `renderEntryEditor`.
|
|
101
|
-
|
|
172
|
+
// A <div> in stacked mode, the last <li> in inline mode.
|
|
173
|
+
const comboboxWrapRef = useRef<HTMLElement | null>(null);
|
|
174
|
+
const setComboboxWrap = (el: HTMLElement | null): void => {
|
|
175
|
+
comboboxWrapRef.current = el;
|
|
176
|
+
};
|
|
177
|
+
const listRef = useRef<HTMLUListElement | null>(null);
|
|
178
|
+
const handleRef = useRef<ComboboxHandle | null>(null);
|
|
179
|
+
useImperativeHandle(ref, () => ({
|
|
180
|
+
focus: () => handleRef.current?.focus(),
|
|
181
|
+
clear: () => handleRef.current?.clear(),
|
|
182
|
+
}));
|
|
183
|
+
const inputId = useId();
|
|
184
|
+
const hintId = useId();
|
|
185
|
+
const inline = layout === "inline";
|
|
102
186
|
const editing = entries.find((entry) => entry.editing);
|
|
103
187
|
|
|
104
188
|
const handleKeyDownCapture = (e: KeyboardEvent<HTMLDivElement>): void => {
|
|
@@ -112,48 +196,112 @@ export const MultiCombobox = forwardRef<MultiComboboxHandle, MultiComboboxProps>
|
|
|
112
196
|
onRemoveEntry(last.key);
|
|
113
197
|
};
|
|
114
198
|
|
|
199
|
+
// Clicking the field's empty area (inline) focuses the input, like
|
|
200
|
+
// a mail client's To field. Chip / input clicks handle themselves.
|
|
201
|
+
const handleFieldMouseDown = (e: MouseEvent<HTMLDivElement>): void => {
|
|
202
|
+
if (e.target !== e.currentTarget && e.target !== listRef.current) return;
|
|
203
|
+
e.preventDefault();
|
|
204
|
+
handleRef.current?.focus();
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const comboboxEl = (
|
|
208
|
+
<Combobox
|
|
209
|
+
ref={handleRef}
|
|
210
|
+
{...comboboxProps}
|
|
211
|
+
label={inline ? undefined : label}
|
|
212
|
+
bare={inline}
|
|
213
|
+
inputId={inline ? inputId : undefined}
|
|
214
|
+
inputDescribedBy={inline ? hintId : undefined}
|
|
215
|
+
clearOnCommit
|
|
216
|
+
committed={null}
|
|
217
|
+
testid={`${testid}-combobox`}
|
|
218
|
+
/>
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const entriesList = (
|
|
222
|
+
<ul
|
|
223
|
+
ref={listRef}
|
|
224
|
+
role="list"
|
|
225
|
+
aria-label={entriesLabel ?? "Selected items"}
|
|
226
|
+
data-testid={`${testid}-entries`}
|
|
227
|
+
style={
|
|
228
|
+
inline
|
|
229
|
+
? INLINE_LIST_STYLE
|
|
230
|
+
: {
|
|
231
|
+
listStyle: "none",
|
|
232
|
+
margin: entries.length > 0 ? "8px 0 0" : 0,
|
|
233
|
+
padding: 0,
|
|
234
|
+
display: "flex",
|
|
235
|
+
flexWrap: "wrap",
|
|
236
|
+
gap: 6,
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
>
|
|
240
|
+
{entries.map((entry) => (
|
|
241
|
+
<li key={entry.key} style={{ minWidth: 0, maxWidth: "100%" }}>
|
|
242
|
+
<Chip
|
|
243
|
+
data-testid={`${testid}-entry-${entry.key}`}
|
|
244
|
+
title={entry.subtitle}
|
|
245
|
+
tone={entry.tone}
|
|
246
|
+
selected={entry.editing}
|
|
247
|
+
disabled={comboboxProps.disabled}
|
|
248
|
+
onClick={onEntryClick ? () => onEntryClick(entry.key) : undefined}
|
|
249
|
+
onRemove={() => onRemoveEntry(entry.key)}
|
|
250
|
+
removeLabel={removeLabel?.(entry)}
|
|
251
|
+
>
|
|
252
|
+
{entry.title}
|
|
253
|
+
</Chip>
|
|
254
|
+
</li>
|
|
255
|
+
))}
|
|
256
|
+
{inline ? (
|
|
257
|
+
// Not an entry: presentational so the list still counts N
|
|
258
|
+
// chips for assistive tech.
|
|
259
|
+
<li ref={setComboboxWrap} role="presentation" style={INLINE_COMBOBOX_ITEM}>
|
|
260
|
+
{comboboxEl}
|
|
261
|
+
</li>
|
|
262
|
+
) : null}
|
|
263
|
+
</ul>
|
|
264
|
+
);
|
|
265
|
+
|
|
115
266
|
return (
|
|
116
267
|
<div data-testid={testid} onKeyDownCapture={handleKeyDownCapture}>
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
/>
|
|
125
|
-
</div>
|
|
126
|
-
|
|
127
|
-
<ul
|
|
128
|
-
role="list"
|
|
129
|
-
aria-label={entriesLabel ?? "Selected items"}
|
|
130
|
-
data-testid={`${testid}-entries`}
|
|
131
|
-
style={{
|
|
132
|
-
listStyle: "none",
|
|
133
|
-
margin: entries.length > 0 ? "8px 0 0" : 0,
|
|
134
|
-
padding: 0,
|
|
135
|
-
display: "flex",
|
|
136
|
-
flexWrap: "wrap",
|
|
137
|
-
gap: 6,
|
|
138
|
-
}}
|
|
139
|
-
>
|
|
140
|
-
{entries.map((entry) => (
|
|
141
|
-
<li key={entry.key} style={{ minWidth: 0, maxWidth: "100%" }}>
|
|
142
|
-
<Chip
|
|
143
|
-
data-testid={`${testid}-entry-${entry.key}`}
|
|
144
|
-
title={entry.subtitle}
|
|
145
|
-
tone={entry.tone}
|
|
146
|
-
selected={entry.editing}
|
|
147
|
-
disabled={comboboxProps.disabled}
|
|
148
|
-
onClick={onEntryClick ? () => onEntryClick(entry.key) : undefined}
|
|
149
|
-
onRemove={() => onRemoveEntry(entry.key)}
|
|
150
|
-
removeLabel={removeLabel?.(entry)}
|
|
268
|
+
{inline ? (
|
|
269
|
+
<>
|
|
270
|
+
{label ? (
|
|
271
|
+
<label
|
|
272
|
+
htmlFor={inputId}
|
|
273
|
+
className="ck-eyebrow"
|
|
274
|
+
style={{ color: "var(--ck-text-secondary)", display: "block", marginBottom: 6 }}
|
|
151
275
|
>
|
|
152
|
-
{
|
|
153
|
-
</
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
276
|
+
{label}
|
|
277
|
+
</label>
|
|
278
|
+
) : null}
|
|
279
|
+
<div
|
|
280
|
+
className="ck-multi-combobox-field"
|
|
281
|
+
data-testid={`${testid}-field`}
|
|
282
|
+
style={INLINE_FIELD_STYLE}
|
|
283
|
+
onMouseDown={handleFieldMouseDown}
|
|
284
|
+
>
|
|
285
|
+
{entriesList}
|
|
286
|
+
</div>
|
|
287
|
+
<div
|
|
288
|
+
id={hintId}
|
|
289
|
+
className="ck-multi-combobox-invalid-hint"
|
|
290
|
+
style={{
|
|
291
|
+
marginTop: 6,
|
|
292
|
+
font: "400 11px/1.4 var(--ck-font-sans)",
|
|
293
|
+
color: "var(--ck-critical)",
|
|
294
|
+
}}
|
|
295
|
+
>
|
|
296
|
+
{comboboxProps.invalidHint ?? COMBOBOX_DEFAULT_INVALID_HINT}
|
|
297
|
+
</div>
|
|
298
|
+
</>
|
|
299
|
+
) : (
|
|
300
|
+
<>
|
|
301
|
+
<div ref={setComboboxWrap}>{comboboxEl}</div>
|
|
302
|
+
{entriesList}
|
|
303
|
+
</>
|
|
304
|
+
)}
|
|
157
305
|
|
|
158
306
|
{editing && renderEntryEditor ? (
|
|
159
307
|
<div data-testid={`${testid}-editor`} style={{ marginTop: 8 }}>
|
package/src/primitives/index.ts
CHANGED
|
@@ -25,7 +25,12 @@ export type {
|
|
|
25
25
|
export { Chip } from "./Chip";
|
|
26
26
|
export type { ChipProps, ChipTone } from "./Chip";
|
|
27
27
|
export { MultiCombobox } from "./MultiCombobox";
|
|
28
|
-
export type {
|
|
28
|
+
export type {
|
|
29
|
+
MultiComboboxProps,
|
|
30
|
+
MultiComboboxEntry,
|
|
31
|
+
MultiComboboxHandle,
|
|
32
|
+
MultiComboboxLayout,
|
|
33
|
+
} from "./MultiCombobox";
|
|
29
34
|
export { Textarea } from "./Textarea";
|
|
30
35
|
export type { TextareaProps } from "./Textarea";
|
|
31
36
|
export { Checkbox } from "./Checkbox";
|
package/src/styles/index.css
CHANGED
|
@@ -304,3 +304,40 @@ html[data-ck-chrome="seamless"] .ck-card__foot {
|
|
|
304
304
|
animation: none;
|
|
305
305
|
}
|
|
306
306
|
}
|
|
307
|
+
|
|
308
|
+
/* ---------- Input bare mode + MultiCombobox inline field ----------
|
|
309
|
+
`.ck-input--bare` is an <input> whose FIELD is drawn by its host
|
|
310
|
+
(MultiCombobox inline layout: chips + input in one frame). Chrome
|
|
311
|
+
presets restyle `.ck-input` with !important at up to (0,5,1)
|
|
312
|
+
specificity, so this rule uses `:not(#\#)` to out-rank them and
|
|
313
|
+
strip every trace of field chrome back off. */
|
|
314
|
+
.ck-input.ck-input--bare:not(#\#) {
|
|
315
|
+
background: transparent !important;
|
|
316
|
+
border: none !important;
|
|
317
|
+
border-radius: 0 !important;
|
|
318
|
+
box-shadow: none !important;
|
|
319
|
+
outline: none !important;
|
|
320
|
+
height: 28px !important;
|
|
321
|
+
padding: 0 4px !important;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/* The host-drawn field: looks like .ck-input, carries the focus ring
|
|
325
|
+
via :focus-within, the invalid border via :has([aria-invalid]) and
|
|
326
|
+
the disabled dim via :has(:disabled). */
|
|
327
|
+
.ck-multi-combobox-field:focus-within {
|
|
328
|
+
border-color: var(--ck-accent) !important;
|
|
329
|
+
box-shadow: 0 0 0 3px var(--ck-accent-muted);
|
|
330
|
+
}
|
|
331
|
+
.ck-multi-combobox-field:has(.ck-input[aria-invalid="true"]) {
|
|
332
|
+
border-color: var(--ck-critical) !important;
|
|
333
|
+
}
|
|
334
|
+
.ck-multi-combobox-field:has(.ck-input:disabled) {
|
|
335
|
+
opacity: 0.55;
|
|
336
|
+
cursor: not-allowed;
|
|
337
|
+
}
|
|
338
|
+
.ck-multi-combobox-invalid-hint {
|
|
339
|
+
display: none;
|
|
340
|
+
}
|
|
341
|
+
.ck-multi-combobox-field:has(.ck-input[aria-invalid="true"]) + .ck-multi-combobox-invalid-hint {
|
|
342
|
+
display: block;
|
|
343
|
+
}
|