@m3e/chips 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +539 -0
- package/cem.config.mjs +16 -0
- package/demo/index.html +183 -0
- package/dist/css-custom-data.json +777 -0
- package/dist/custom-elements.json +3307 -0
- package/dist/html-custom-data.json +277 -0
- package/dist/index.js +1516 -0
- package/dist/index.js.map +1 -0
- package/dist/index.min.js +480 -0
- package/dist/index.min.js.map +1 -0
- package/dist/src/AssistChipElement.d.ts +82 -0
- package/dist/src/AssistChipElement.d.ts.map +1 -0
- package/dist/src/ChipElement.d.ts +86 -0
- package/dist/src/ChipElement.d.ts.map +1 -0
- package/dist/src/ChipSetElement.d.ts +43 -0
- package/dist/src/ChipSetElement.d.ts.map +1 -0
- package/dist/src/ChipVariant.d.ts +3 -0
- package/dist/src/ChipVariant.d.ts.map +1 -0
- package/dist/src/FilterChipElement.d.ts +93 -0
- package/dist/src/FilterChipElement.d.ts.map +1 -0
- package/dist/src/FilterChipSetElement.d.ts +78 -0
- package/dist/src/FilterChipSetElement.d.ts.map +1 -0
- package/dist/src/InputChipElement.d.ts +104 -0
- package/dist/src/InputChipElement.d.ts.map +1 -0
- package/dist/src/InputChipSetElement.d.ts +75 -0
- package/dist/src/InputChipSetElement.d.ts.map +1 -0
- package/dist/src/SuggestionChipElement.d.ts +83 -0
- package/dist/src/SuggestionChipElement.d.ts.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.d.ts.map +1 -0
- package/eslint.config.mjs +13 -0
- package/package.json +55 -0
- package/rollup.config.js +32 -0
- package/src/AssistChipElement.ts +103 -0
- package/src/ChipElement.ts +336 -0
- package/src/ChipSetElement.ts +60 -0
- package/src/ChipVariant.ts +2 -0
- package/src/FilterChipElement.ts +254 -0
- package/src/FilterChipSetElement.ts +161 -0
- package/src/InputChipElement.ts +287 -0
- package/src/InputChipSetElement.ts +360 -0
- package/src/SuggestionChipElement.ts +104 -0
- package/src/index.ts +9 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
|
|
2
|
+
import { css, CSSResultGroup, html, nothing, PropertyValues } from "lit";
|
|
3
|
+
import { customElement, property, query } from "lit/decorators.js";
|
|
4
|
+
import { ifDefined } from "lit/directives/if-defined.js";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
DisabledInteractive,
|
|
8
|
+
Disabled,
|
|
9
|
+
EventAttribute,
|
|
10
|
+
Role,
|
|
11
|
+
AttachInternals,
|
|
12
|
+
DesignToken,
|
|
13
|
+
hasAssignedNodes,
|
|
14
|
+
} from "@m3e/core";
|
|
15
|
+
|
|
16
|
+
import { M3eIconButtonElement } from "@m3e/icon-button";
|
|
17
|
+
|
|
18
|
+
import { M3eChipElement } from "./ChipElement";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @summary
|
|
22
|
+
* A chip which represents a discrete piece of information entered by a user.
|
|
23
|
+
*
|
|
24
|
+
* @description
|
|
25
|
+
* The `m3e-input-chip` component represents an input chip, allowing users to enter, display,
|
|
26
|
+
* and manage discrete values such as tags or keywords. It supports expressive styling, accessibility,
|
|
27
|
+
* keyboard interaction, and appearance variants including `elevated` and `outlined`.
|
|
28
|
+
*
|
|
29
|
+
* @tag m3e-input-chip
|
|
30
|
+
*
|
|
31
|
+
* @slot - Renders the label of the chip.
|
|
32
|
+
* @slot avatar - Renders an avatar before the chip's label.
|
|
33
|
+
* @slot icon - Renders an icon before the chip's label.
|
|
34
|
+
* @slot remove-icon - Renders the icon for the button used to remove the chip.
|
|
35
|
+
*
|
|
36
|
+
* @attr disabled - Whether the element is disabled.
|
|
37
|
+
* @attr disabled-interactive - Whether the element is disabled and interactive.
|
|
38
|
+
* @attr removable - Whether the chip is removable.
|
|
39
|
+
* @attr remove-label - The accessible label given to the button used to remove the chip.
|
|
40
|
+
* @attr value - A string representing the value of the chip.
|
|
41
|
+
* @attr variant - The appearance variant of the chip.
|
|
42
|
+
*
|
|
43
|
+
* @fires remove - Emitted when the remove button is clicked or DELETE or BACKSPACE key is pressed.
|
|
44
|
+
*
|
|
45
|
+
* @cssprop --m3e-chip-container-shape - Border radius of the chip container.
|
|
46
|
+
* @cssprop --m3e-chip-container-height - Base height of the chip container before density adjustment.
|
|
47
|
+
* @cssprop --m3e-chip-label-text-font-size - Font size of the chip label text.
|
|
48
|
+
* @cssprop --m3e-chip-label-text-font-weight - Font weight of the chip label text.
|
|
49
|
+
* @cssprop --m3e-chip-label-text-line-height - Line height of the chip label text.
|
|
50
|
+
* @cssprop --m3e-chip-label-text-tracking - Letter spacing of the chip label text.
|
|
51
|
+
* @cssprop --m3e-chip-label-text-color - Label text color in default state.
|
|
52
|
+
* @cssprop --m3e-chip-icon-color - Icon color in default state.
|
|
53
|
+
* @cssprop --m3e-chip-icon-size - Font size of leading/trailing icons.
|
|
54
|
+
* @cssprop --m3e-chip-spacing - Horizontal gap between chip content elements.
|
|
55
|
+
* @cssprop --m3e-chip-padding-start - Default start padding when no icon is present.
|
|
56
|
+
* @cssprop --m3e-chip-padding-end - Default end padding when no trailing icon is present.
|
|
57
|
+
* @cssprop --m3e-chip-with-icon-padding-start - Start padding when leading icon is present.
|
|
58
|
+
* @cssprop --m3e-chip-with-icon-padding-end - End padding when trailing icon is present.
|
|
59
|
+
* @cssprop --m3e-chip-disabled-label-text-color - Base color for disabled label text.
|
|
60
|
+
* @cssprop --m3e-chip-disabled-label-text-opacity - Opacity applied to disabled label text.
|
|
61
|
+
* @cssprop --m3e-chip-disabled-icon-color - Base color for disabled icons.
|
|
62
|
+
* @cssprop --m3e-chip-disabled-icon-opacity - Opacity applied to disabled icons.
|
|
63
|
+
* @cssprop --m3e-elevated-chip-container-color - Background color for elevated variant.
|
|
64
|
+
* @cssprop --m3e-elevated-chip-elevation - Elevation level for elevated variant.
|
|
65
|
+
* @cssprop --m3e-elevated-chip-hover-elevation - Elevation level on hover.
|
|
66
|
+
* @cssprop --m3e-elevated-chip-disabled-container-color - Background color for disabled elevated variant.
|
|
67
|
+
* @cssprop --m3e-elevated-chip-disabled-container-opacity - Opacity applied to disabled elevated background.
|
|
68
|
+
* @cssprop --m3e-elevated-chip-disabled-elevation - Elevation level for disabled elevated variant.
|
|
69
|
+
* @cssprop --m3e-outlined-chip-outline-thickness - Outline thickness for outlined variant.
|
|
70
|
+
* @cssprop --m3e-outlined-chip-outline-color - Outline color for outlined variant.
|
|
71
|
+
* @cssprop --m3e-outlined-chip-disabled-outline-color - Outline color for disabled outlined variant.
|
|
72
|
+
* @cssprop --m3e-outlined-chip-disabled-outline-opacity - Opacity applied to disabled outline.
|
|
73
|
+
* @cssprop --m3e-chip-avatar-size - Font size of the avatar slot content.
|
|
74
|
+
* @cssprop --m3e-chip-disabled-avatar-opacity - Opacity applied to the avatar when disabled.
|
|
75
|
+
* @cssprop --m3e-chip-with-avatar-padding-start - Start padding when an avatar is present.
|
|
76
|
+
*/
|
|
77
|
+
@customElement("m3e-input-chip")
|
|
78
|
+
export class M3eInputChipElement extends EventAttribute(
|
|
79
|
+
DisabledInteractive(Disabled(AttachInternals(Role(M3eChipElement, "row"), true))),
|
|
80
|
+
"remove"
|
|
81
|
+
) {
|
|
82
|
+
/** The styles of the element. */
|
|
83
|
+
static override styles: CSSResultGroup = [
|
|
84
|
+
M3eChipElement.styles,
|
|
85
|
+
css`
|
|
86
|
+
.cell {
|
|
87
|
+
display: inline-flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
outline: none;
|
|
90
|
+
column-gap: var(--m3e-chip-spacing, 0.5rem);
|
|
91
|
+
min-width: 0;
|
|
92
|
+
}
|
|
93
|
+
.remove-button {
|
|
94
|
+
--m3e-icon-button-extra-small-container-height: 1.5rem;
|
|
95
|
+
--m3e-icon-button-extra-small-icon-size: var(--m3e-chip-icon-size, 1.125rem);
|
|
96
|
+
width: 1.5rem;
|
|
97
|
+
}
|
|
98
|
+
.remove-icon {
|
|
99
|
+
flex: none;
|
|
100
|
+
width: var(--m3e-chip-icon-size, 1.125rem);
|
|
101
|
+
height: var(--m3e-chip-icon-size, 1.125rem);
|
|
102
|
+
}
|
|
103
|
+
.touch {
|
|
104
|
+
top: calc(
|
|
105
|
+
0px - calc(calc(3rem - calc(var(--m3e-chip-container-height, 2rem) + ${DesignToken.density.calc(-2)})) / 2)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
.wrapper {
|
|
109
|
+
height: 100%;
|
|
110
|
+
overflow: visible;
|
|
111
|
+
min-width: 0;
|
|
112
|
+
}
|
|
113
|
+
::slotted([slot="avatar"]) {
|
|
114
|
+
flex: none;
|
|
115
|
+
font-size: var(--m3e-chip-avatar-size, 1.5rem);
|
|
116
|
+
}
|
|
117
|
+
:host(:disabled) ::slotted([slot="avatar"]),
|
|
118
|
+
:host([disabled-interactive]) ::slotted([slot="avatar"]) {
|
|
119
|
+
opacity: var(--m3e-chip-disabled-avatar-opacity, 38%);
|
|
120
|
+
color: var(--m3e-chip-disabled-icon-color, ${DesignToken.color.onSurface});
|
|
121
|
+
}
|
|
122
|
+
:host(.-with-avatar) ::slotted([slot="icon"]) {
|
|
123
|
+
display: none;
|
|
124
|
+
}
|
|
125
|
+
:host(.-with-avatar) .wrapper {
|
|
126
|
+
padding-inline-start: var(--m3e-chip-with-avatar-padding-start, 0.25rem);
|
|
127
|
+
}
|
|
128
|
+
@media (forced-colors: active) {
|
|
129
|
+
:host(:disabled) ::slotted([slot="avatar"]),
|
|
130
|
+
:host([disabled-interactive]) ::slotted([slot="avatar"]) {
|
|
131
|
+
color: CanvasText;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
`,
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
/** A reference to the grid cell of the chip. */
|
|
138
|
+
@query(".cell") readonly cell!: HTMLSpanElement;
|
|
139
|
+
|
|
140
|
+
/** A reference to the button used to remove the chip. */
|
|
141
|
+
@query(".remove-button") readonly removeButton!: M3eIconButtonElement | null;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Whether the chip is removable.
|
|
145
|
+
* @default false
|
|
146
|
+
*/
|
|
147
|
+
@property({ type: Boolean }) removable = false;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The accessible label given to the button used to remove the chip.
|
|
151
|
+
* @default "Remove"
|
|
152
|
+
*/
|
|
153
|
+
@property({ attribute: "remove-label" }) removeLabel = "Remove";
|
|
154
|
+
|
|
155
|
+
/** @inheritdoc */
|
|
156
|
+
override connectedCallback(): void {
|
|
157
|
+
super.connectedCallback();
|
|
158
|
+
this.removeAttribute("tabindex");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** @inheritdoc */
|
|
162
|
+
protected override update(changedProperties: PropertyValues<this>): void {
|
|
163
|
+
super.update(changedProperties);
|
|
164
|
+
this.removeAttribute("tabindex");
|
|
165
|
+
|
|
166
|
+
if (changedProperties.has("removable")) {
|
|
167
|
+
this.classList.toggle("-with-trailing-icon", this.removable);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** @inheritdoc */
|
|
172
|
+
protected override render(): unknown {
|
|
173
|
+
return html`<div class="base">
|
|
174
|
+
<m3e-elevation
|
|
175
|
+
class="elevation"
|
|
176
|
+
for="cell"
|
|
177
|
+
?disabled="${this.disabled || this.disabledInteractive}"
|
|
178
|
+
></m3e-elevation>
|
|
179
|
+
<m3e-state-layer
|
|
180
|
+
class="state-layer"
|
|
181
|
+
for="cell"
|
|
182
|
+
?disabled="${this.disabled || this.disabledInteractive}"
|
|
183
|
+
></m3e-state-layer>
|
|
184
|
+
<m3e-focus-ring class="focus-ring" for="cell" ?disabled="${this.disabled}"></m3e-focus-ring>
|
|
185
|
+
<m3e-ripple class="ripple" for="cell" ?disabled="${this.disabled || this.disabledInteractive}"></m3e-ripple>
|
|
186
|
+
<div class="wrapper">
|
|
187
|
+
<div
|
|
188
|
+
id="cell"
|
|
189
|
+
class="cell"
|
|
190
|
+
role="gridcell"
|
|
191
|
+
tabindex="${ifDefined(this.disabled ? undefined : "-1")}"
|
|
192
|
+
@keydown="${this.#handleKeyDown}"
|
|
193
|
+
>
|
|
194
|
+
<slot name="avatar" @slotchange="${this.#handleAvatarSlotChange}"></slot>
|
|
195
|
+
${this._renderIcon()}
|
|
196
|
+
<div class="label">${this._renderSlot()}</div>
|
|
197
|
+
<div class="touch" aria-hidden="true"></div>
|
|
198
|
+
</div>
|
|
199
|
+
${this._renderTrailingIcon()}
|
|
200
|
+
</div>
|
|
201
|
+
</div>`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** @internal @inheritdoc */
|
|
205
|
+
protected override _renderTrailingIcon(): unknown {
|
|
206
|
+
return this.removable
|
|
207
|
+
? html`<span role="gridcell" class="remove">
|
|
208
|
+
<m3e-icon-button
|
|
209
|
+
class="remove-button"
|
|
210
|
+
aria-label="${this.removeLabel}"
|
|
211
|
+
size="extra-small"
|
|
212
|
+
tabindex="-1"
|
|
213
|
+
?disabled="${this.disabled}"
|
|
214
|
+
?disabled-interactive="${this.disabledInteractive}"
|
|
215
|
+
@click="${this.#handleRemoveButtonClick}"
|
|
216
|
+
>
|
|
217
|
+
<slot name="remove-icon">
|
|
218
|
+
<svg class="remove-icon" viewBox="0 -960 960 960" fill="currentColor">
|
|
219
|
+
<path
|
|
220
|
+
d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"
|
|
221
|
+
/>
|
|
222
|
+
</svg>
|
|
223
|
+
</slot>
|
|
224
|
+
</m3e-icon-button>
|
|
225
|
+
</span>`
|
|
226
|
+
: nothing;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** @private */
|
|
230
|
+
#handleAvatarSlotChange(e: Event): void {
|
|
231
|
+
this.classList.toggle("-with-avatar", hasAssignedNodes(<HTMLSlotElement>e.target));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** @private */
|
|
235
|
+
#handleRemoveButtonClick(e: Event): void {
|
|
236
|
+
e.stopPropagation();
|
|
237
|
+
this.dispatchEvent(new Event("remove"));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** @private */
|
|
241
|
+
#handleKeyDown(e: KeyboardEvent): void {
|
|
242
|
+
if (this.removable) {
|
|
243
|
+
switch (e.key) {
|
|
244
|
+
case "Backspace":
|
|
245
|
+
case "Delete":
|
|
246
|
+
this.dispatchEvent(new Event("remove"));
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
interface M3eInputChipElementEventMap extends HTMLElementEventMap {
|
|
254
|
+
remove: Event;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export interface M3eInputChipElement {
|
|
258
|
+
addEventListener<K extends keyof M3eInputChipElementEventMap>(
|
|
259
|
+
type: K,
|
|
260
|
+
listener: (this: M3eInputChipElement, ev: M3eInputChipElementEventMap[K]) => void,
|
|
261
|
+
options?: boolean | AddEventListenerOptions
|
|
262
|
+
): void;
|
|
263
|
+
|
|
264
|
+
addEventListener(
|
|
265
|
+
type: string,
|
|
266
|
+
listener: EventListenerOrEventListenerObject,
|
|
267
|
+
options?: boolean | AddEventListenerOptions
|
|
268
|
+
): void;
|
|
269
|
+
|
|
270
|
+
removeEventListener<K extends keyof M3eInputChipElementEventMap>(
|
|
271
|
+
type: K,
|
|
272
|
+
listener: (this: M3eInputChipElement, ev: M3eInputChipElementEventMap[K]) => void,
|
|
273
|
+
options?: boolean | EventListenerOptions
|
|
274
|
+
): void;
|
|
275
|
+
|
|
276
|
+
removeEventListener(
|
|
277
|
+
type: string,
|
|
278
|
+
listener: EventListenerOrEventListenerObject,
|
|
279
|
+
options?: boolean | EventListenerOptions
|
|
280
|
+
): void;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
declare global {
|
|
284
|
+
interface HTMLElementTagNameMap {
|
|
285
|
+
"m3e-input-chip": M3eInputChipElement;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { css, CSSResultGroup, html, PropertyValues } from "lit";
|
|
2
|
+
import { customElement } from "lit/decorators.js";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
AttachInternals,
|
|
6
|
+
ConstraintValidation,
|
|
7
|
+
DesignToken,
|
|
8
|
+
Dirty,
|
|
9
|
+
Disabled,
|
|
10
|
+
FormAssociated,
|
|
11
|
+
formValue,
|
|
12
|
+
Required,
|
|
13
|
+
RequiredConstraintValidation,
|
|
14
|
+
Role,
|
|
15
|
+
Touched,
|
|
16
|
+
} from "@m3e/core";
|
|
17
|
+
|
|
18
|
+
import { ListKeyManager, ListManager } from "@m3e/core/a11y";
|
|
19
|
+
import { FormFieldControl } from "@m3e/form-field";
|
|
20
|
+
|
|
21
|
+
import { M3eChipSetElement } from "./ChipSetElement";
|
|
22
|
+
import { M3eInputChipElement } from "./InputChipElement";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @summary
|
|
26
|
+
* A container that transforms user input into a cohesive set of interactive chips, supporting entry, editing, and removal of discrete values.
|
|
27
|
+
*
|
|
28
|
+
* @description
|
|
29
|
+
* The `m3e-input-chip-set` component enables users to input, display, and manage a collection of discrete
|
|
30
|
+
* values as input chips. Designed for expressive, accessible forms, it supports keyboard navigation, validation,
|
|
31
|
+
* and seamless integration with form controls. This component is ideal for capturing user-generated tags,
|
|
32
|
+
* keywords, or selections in a visually consistent and interactive manner.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* The following example illustrates the use of the `m3e-input-chip-set` inside a `m3e-form-field`.
|
|
36
|
+
* In this example, the `input` slot specifies the `input` element used to add input chips and the
|
|
37
|
+
* field label's `for` attribute targets the `input` element to provide an accessible label.
|
|
38
|
+
* ```html
|
|
39
|
+
* <m3e-form-field>
|
|
40
|
+
* <label slot="label" for="keywords">Keywords</label>
|
|
41
|
+
* <m3e-input-chip-set aria-label="Enter keywords">
|
|
42
|
+
* <input id="keywords" slot="input" placeholder="New keyword..." />
|
|
43
|
+
* </m3e-input-chip-set>
|
|
44
|
+
* </m3e-form-field>
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @tag m3e-input-chip-set
|
|
48
|
+
*
|
|
49
|
+
* @slot - Renders the chips of the set.
|
|
50
|
+
* @slot input - Renders the input element used to add new chips to the set.
|
|
51
|
+
*
|
|
52
|
+
* @attr disabled - Whether the element is disabled.
|
|
53
|
+
* @attr name - The name that identifies the element when submitting the associated form.
|
|
54
|
+
* @attr required - Whether a value is required for the element.
|
|
55
|
+
* @attr vertical - Whether the element is oriented vertically.
|
|
56
|
+
*
|
|
57
|
+
* @fires change - Emitted when a chip is added to, or removed from, the set.
|
|
58
|
+
*
|
|
59
|
+
* @cssprop --m3e-chip-set-spacing - The spacing (gap) between chips in the set.
|
|
60
|
+
*/
|
|
61
|
+
@customElement("m3e-input-chip-set")
|
|
62
|
+
export class M3eInputChipSetElement
|
|
63
|
+
extends RequiredConstraintValidation(
|
|
64
|
+
Required(
|
|
65
|
+
ConstraintValidation(Dirty(Touched(FormAssociated(Disabled(AttachInternals(Role(M3eChipSetElement, "grid")))))))
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
implements FormFieldControl
|
|
69
|
+
{
|
|
70
|
+
/** The styles of the element. */
|
|
71
|
+
static override styles: CSSResultGroup = [
|
|
72
|
+
M3eChipSetElement.styles,
|
|
73
|
+
css`
|
|
74
|
+
::slotted([slot="input"]) {
|
|
75
|
+
outline: unset;
|
|
76
|
+
border: unset;
|
|
77
|
+
background-color: transparent;
|
|
78
|
+
box-shadow: none;
|
|
79
|
+
font-family: inherit;
|
|
80
|
+
font-size: inherit;
|
|
81
|
+
line-height: initial;
|
|
82
|
+
letter-spacing: inherit;
|
|
83
|
+
color: var(--_form-field-input-color, inherit);
|
|
84
|
+
flex: 1 1 auto;
|
|
85
|
+
min-width: 0;
|
|
86
|
+
padding: unset;
|
|
87
|
+
}
|
|
88
|
+
::slotted(m3e-input-chip) {
|
|
89
|
+
min-width: 0;
|
|
90
|
+
}
|
|
91
|
+
::slotted([slot="input"])::placeholder {
|
|
92
|
+
user-select: none;
|
|
93
|
+
color: currentColor;
|
|
94
|
+
transition: opacity ${DesignToken.motion.duration.extraLong1};
|
|
95
|
+
}
|
|
96
|
+
:host(:not(:focus-within)) ::slotted([slot="input"])::placeholder {
|
|
97
|
+
opacity: 0;
|
|
98
|
+
transition: 0s;
|
|
99
|
+
}
|
|
100
|
+
:host(:hover) ::slotted([slot="input"])::placeholder {
|
|
101
|
+
transition: 0s;
|
|
102
|
+
}
|
|
103
|
+
span[role="row"],
|
|
104
|
+
span[role="gridcell"] {
|
|
105
|
+
display: contents;
|
|
106
|
+
}
|
|
107
|
+
@media (prefers-reduced-motion) {
|
|
108
|
+
::slotted([slot="input"])::placeholder {
|
|
109
|
+
transition: none !important;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
`,
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
/** @private */ readonly #inputChangeHandler = () => this.#handleInputChange();
|
|
116
|
+
/** @private */ readonly #inputKeyDownHandler = (e: KeyboardEvent) => this.#handleInputKeyDown(e);
|
|
117
|
+
/** @private */ readonly #focusHandler = () => this.#handleFocus();
|
|
118
|
+
/** @private */ readonly #focusInHandler = () => this.#handleFocusIn();
|
|
119
|
+
/** @private */ readonly #focusOutHandler = () => this.#handleFocusOut();
|
|
120
|
+
/** @private */ readonly #chipRemoveHandler = (e: Event) => this.#handleChipRemove(e);
|
|
121
|
+
/** @private */ readonly #chipClickHandler = (e: Event) => this.#handleChipClick(e);
|
|
122
|
+
|
|
123
|
+
/** @private */ readonly #listManager = new ListManager<M3eInputChipElement>();
|
|
124
|
+
/** @private */ readonly #listKeyManager = new ListKeyManager<HTMLElement>()
|
|
125
|
+
.onActiveItemChange(() => this.#listKeyManager.activeItem?.focus())
|
|
126
|
+
.withHomeAndEnd()
|
|
127
|
+
.withSkipPredicate((x) => !x.hasAttribute("tabindex"));
|
|
128
|
+
|
|
129
|
+
/** @private */ #ignoreInputChange = false;
|
|
130
|
+
/** @private */ #input: HTMLInputElement | null = null;
|
|
131
|
+
/** @private */ #tabindex = 0;
|
|
132
|
+
|
|
133
|
+
/** The chips of the set. */
|
|
134
|
+
get chips(): readonly M3eInputChipElement[] {
|
|
135
|
+
// NOTE: query is used instead of the internal list management due to
|
|
136
|
+
// validating required state on change to support form-field integration.
|
|
137
|
+
return [...this.querySelectorAll("m3e-input-chip")];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The selected values of the set. */
|
|
141
|
+
get value(): readonly string[] | null {
|
|
142
|
+
const values = this.chips.map((x) => x.value);
|
|
143
|
+
return values.length == 0 ? null : values;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** @inheritdoc @internal */
|
|
147
|
+
override get [formValue]() {
|
|
148
|
+
const values = this.value;
|
|
149
|
+
if (!values) return null;
|
|
150
|
+
const data = new FormData();
|
|
151
|
+
for (const value of values) {
|
|
152
|
+
data.append(this.name, value);
|
|
153
|
+
}
|
|
154
|
+
return data;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** @inheritdoc */
|
|
158
|
+
get shouldLabelFloat(): boolean {
|
|
159
|
+
return this.chips.length > 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** @inheritdoc */
|
|
163
|
+
onContainerClick(): void {
|
|
164
|
+
this.#input?.focus();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** @inheritdoc */
|
|
168
|
+
override connectedCallback(): void {
|
|
169
|
+
super.connectedCallback();
|
|
170
|
+
|
|
171
|
+
this.closest("m3e-form-field")?.notifyControlStateChange();
|
|
172
|
+
|
|
173
|
+
this.#tabindex = Number.parseInt(this.getAttribute("tabindex") ?? "0");
|
|
174
|
+
this.addEventListener("focus", this.#focusHandler);
|
|
175
|
+
this.addEventListener("focusin", this.#focusInHandler);
|
|
176
|
+
this.addEventListener("focusout", this.#focusOutHandler);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** @inheritdoc */
|
|
180
|
+
override disconnectedCallback(): void {
|
|
181
|
+
super.disconnectedCallback();
|
|
182
|
+
|
|
183
|
+
this.removeEventListener("focus", this.#focusHandler);
|
|
184
|
+
this.removeEventListener("focusin", this.#focusInHandler);
|
|
185
|
+
this.removeEventListener("focusout", this.#focusOutHandler);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** @inheritdoc */
|
|
189
|
+
protected override firstUpdated(_changedProperties: PropertyValues): void {
|
|
190
|
+
super.firstUpdated(_changedProperties);
|
|
191
|
+
|
|
192
|
+
if (!this.hasAttribute("tabindex")) {
|
|
193
|
+
this.setAttribute("tabindex", `${this.#tabindex}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** @inheritdoc */
|
|
198
|
+
protected override update(changedProperties: PropertyValues<this>): void {
|
|
199
|
+
super.update(changedProperties);
|
|
200
|
+
|
|
201
|
+
if (changedProperties.has("vertical")) {
|
|
202
|
+
this.ariaOrientation = null;
|
|
203
|
+
}
|
|
204
|
+
if (changedProperties.has("disabled")) {
|
|
205
|
+
this.#listManager.items.forEach((x) => (x.disabled = this.disabled));
|
|
206
|
+
if (this.#input) {
|
|
207
|
+
this.#input.disabled = this.disabled;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** @inheritdoc */
|
|
213
|
+
protected override render(): unknown {
|
|
214
|
+
return html`<slot @keydown="${this.#handleKeyDown}" @slotchange="${this.#handleSlotChange}"></slot>
|
|
215
|
+
<span role="row">
|
|
216
|
+
<span role="gridcell"><slot name="input" @slotchange="${this.#handleInputSlotChange}"></slot></span>
|
|
217
|
+
</span> `;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** @private */
|
|
221
|
+
#handleKeyDown(e: KeyboardEvent): void {
|
|
222
|
+
this.#listKeyManager.onKeyDown(e);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** @private */
|
|
226
|
+
async #handleSlotChange(): Promise<void> {
|
|
227
|
+
const { added, removed } = this.#listManager.setItems([...this.querySelectorAll("m3e-input-chip")]);
|
|
228
|
+
|
|
229
|
+
for (const chip of added) {
|
|
230
|
+
if (chip.isUpdatePending) {
|
|
231
|
+
await chip.updateComplete;
|
|
232
|
+
}
|
|
233
|
+
if (this.disabled) {
|
|
234
|
+
chip.disabled = true;
|
|
235
|
+
}
|
|
236
|
+
chip.addEventListener("remove", this.#chipRemoveHandler);
|
|
237
|
+
chip.cell.addEventListener("click", this.#chipClickHandler);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
removed.forEach((x) => {
|
|
241
|
+
x.removeEventListener("remove", this.#chipRemoveHandler);
|
|
242
|
+
x.cell.removeEventListener("click", this.#chipClickHandler);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
this.#listKeyManager.setItems(
|
|
246
|
+
this.#listManager.items.flatMap((x) => (x.removeButton ? [x.cell, x.removeButton] : [x.cell]))
|
|
247
|
+
);
|
|
248
|
+
if (!this.#listKeyManager.activeItem) {
|
|
249
|
+
this.#listKeyManager.updateActiveItem(this.#listKeyManager.items.find((x) => x.hasAttribute("tabindex")));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** @private */
|
|
254
|
+
#handleInputSlotChange(): void {
|
|
255
|
+
const input = this.querySelector("input");
|
|
256
|
+
if (this.#input) {
|
|
257
|
+
this.#input.removeEventListener("change", this.#inputChangeHandler);
|
|
258
|
+
this.#input.removeEventListener("keydown", this.#inputKeyDownHandler);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.#input = input;
|
|
262
|
+
if (this.#input) {
|
|
263
|
+
this.#input.disabled = this.disabled;
|
|
264
|
+
this.#input.addEventListener("change", this.#inputChangeHandler);
|
|
265
|
+
this.#input.addEventListener("keydown", this.#inputKeyDownHandler);
|
|
266
|
+
|
|
267
|
+
const property = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!;
|
|
268
|
+
Object.defineProperty(input, "value", {
|
|
269
|
+
get: () => property.get?.call(input),
|
|
270
|
+
set: (value: string) => {
|
|
271
|
+
property.set?.call(input, value);
|
|
272
|
+
if (this.#input === input && !this.#ignoreInputChange) {
|
|
273
|
+
this.#handleInputChange();
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** @private */
|
|
281
|
+
#handleFocus(): void {
|
|
282
|
+
setTimeout(() => (this.#listKeyManager.activeItem ?? this.#input)?.focus());
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** @private */
|
|
286
|
+
#handleFocusIn(): void {
|
|
287
|
+
this.setAttribute("tabindex", "-1");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** @private */
|
|
291
|
+
#handleFocusOut(): void {
|
|
292
|
+
this.setAttribute("tabindex", `${this.#tabindex}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** @private */
|
|
296
|
+
#handleChipRemove(e: Event): void {
|
|
297
|
+
const chip = <M3eInputChipElement>e.target;
|
|
298
|
+
const index = this.#listManager.items.indexOf(chip);
|
|
299
|
+
const nextChip = this.#listManager.items.find((x, y) => y > index && !x.disabled && x.removable);
|
|
300
|
+
|
|
301
|
+
chip.remove();
|
|
302
|
+
|
|
303
|
+
this.#listKeyManager.setActiveItem(this.#listKeyManager.items.find((x) => x === nextChip?.removeButton));
|
|
304
|
+
if (!this.#listKeyManager.activeItem) {
|
|
305
|
+
this.#input?.focus();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** @private */
|
|
312
|
+
#handleChipClick(e: Event): void {
|
|
313
|
+
this.#listKeyManager.updateActiveItem(e.composedPath().find((x) => x instanceof M3eInputChipElement)?.cell);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** @private */
|
|
317
|
+
#handleInputChange(): void {
|
|
318
|
+
const value = this.#input?.value;
|
|
319
|
+
if (!value) return;
|
|
320
|
+
|
|
321
|
+
setTimeout(() => {
|
|
322
|
+
const value = this.#input?.value;
|
|
323
|
+
if (!value) return;
|
|
324
|
+
|
|
325
|
+
const chip = document.createElement("m3e-input-chip");
|
|
326
|
+
chip.removable = true;
|
|
327
|
+
chip.appendChild(document.createTextNode(value));
|
|
328
|
+
this.appendChild(chip);
|
|
329
|
+
|
|
330
|
+
if (this.#input) {
|
|
331
|
+
try {
|
|
332
|
+
this.#ignoreInputChange = true;
|
|
333
|
+
this.#input.value = "";
|
|
334
|
+
} finally {
|
|
335
|
+
this.#ignoreInputChange = false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** @private */
|
|
344
|
+
#handleInputKeyDown(e: KeyboardEvent): void {
|
|
345
|
+
if (e.key === "Backspace" && !this.#input?.value) {
|
|
346
|
+
const item = [...this.#listManager.items]
|
|
347
|
+
.reverse()
|
|
348
|
+
.find((x) => !x.disabled && !x.disabledInteractive && x.removable);
|
|
349
|
+
if (item) {
|
|
350
|
+
item.dispatchEvent(new Event("remove"));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
declare global {
|
|
357
|
+
interface HTMLElementTagNameMap {
|
|
358
|
+
"m3e-input-chip-set": M3eInputChipSetElement;
|
|
359
|
+
}
|
|
360
|
+
}
|