@ssejal8/recipe-ui-kit 0.1.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/.claude/settings.local.json +10 -0
- package/.npmr +1 -0
- package/README.md +354 -0
- package/apps/recipe-web/package.json +31 -0
- package/apps/recipe-web/src/app.css +418 -0
- package/apps/recipe-web/src/app.d.ts +32 -0
- package/apps/recipe-web/src/app.html +25 -0
- package/apps/recipe-web/src/lib/api/mealdb.ts +240 -0
- package/apps/recipe-web/src/lib/components/AppHeader.svelte +290 -0
- package/apps/recipe-web/src/lib/components/ConfirmDialog.svelte +46 -0
- package/apps/recipe-web/src/lib/components/Modal.svelte +33 -0
- package/apps/recipe-web/src/lib/components/RecipeCard.svelte +100 -0
- package/apps/recipe-web/src/lib/components/RecipeForm.svelte +560 -0
- package/apps/recipe-web/src/lib/components/RecipeGrid.svelte +77 -0
- package/apps/recipe-web/src/lib/components/RecipePickerDialog.svelte +185 -0
- package/apps/recipe-web/src/lib/components/Toaster.svelte +111 -0
- package/apps/recipe-web/src/lib/domain/recipe.ts +130 -0
- package/apps/recipe-web/src/lib/domain/validation.ts +189 -0
- package/apps/recipe-web/src/lib/stencil/WebComponent.svelte +145 -0
- package/apps/recipe-web/src/lib/stencil/register.ts +28 -0
- package/apps/recipe-web/src/lib/stores/favorites.svelte.ts +81 -0
- package/apps/recipe-web/src/lib/stores/myRecipes.svelte.ts +98 -0
- package/apps/recipe-web/src/lib/stores/persisted.svelte.ts +73 -0
- package/apps/recipe-web/src/lib/stores/planner.svelte.ts +275 -0
- package/apps/recipe-web/src/lib/stores/theme.svelte.ts +67 -0
- package/apps/recipe-web/src/lib/stores/toast.svelte.ts +66 -0
- package/apps/recipe-web/src/routes/+error.svelte +36 -0
- package/apps/recipe-web/src/routes/+layout.svelte +62 -0
- package/apps/recipe-web/src/routes/+page.svelte +352 -0
- package/apps/recipe-web/src/routes/+page.ts +107 -0
- package/apps/recipe-web/src/routes/favorites/+page.svelte +170 -0
- package/apps/recipe-web/src/routes/my-recipes/+layout.ts +55 -0
- package/apps/recipe-web/src/routes/my-recipes/+page.svelte +180 -0
- package/apps/recipe-web/src/routes/my-recipes/[id]/edit/+page.svelte +104 -0
- package/apps/recipe-web/src/routes/my-recipes/new/+page.svelte +67 -0
- package/apps/recipe-web/src/routes/planner/+page.svelte +422 -0
- package/apps/recipe-web/src/routes/recipes/[id]/+page.svelte +538 -0
- package/apps/recipe-web/src/routes/recipes/[id]/+page.ts +24 -0
- package/apps/recipe-web/static/favicon.svg +8 -0
- package/apps/recipe-web/static/robots.txt +2 -0
- package/apps/recipe-web/svelte.config.js +35 -0
- package/apps/recipe-web/tsconfig.json +14 -0
- package/apps/recipe-web/vite.config.ts +23 -0
- package/package.json +29 -0
- package/packages/recipe-kit-stencil/LICENSE +21 -0
- package/packages/recipe-kit-stencil/README.md +185 -0
- package/packages/recipe-kit-stencil/package-lock.json +4772 -0
- package/packages/recipe-kit-stencil/package.json +91 -0
- package/packages/recipe-kit-stencil/src/components/rk-badge/readme.md +38 -0
- package/packages/recipe-kit-stencil/src/components/rk-badge/rk-badge.css +102 -0
- package/packages/recipe-kit-stencil/src/components/rk-badge/rk-badge.tsx +31 -0
- package/packages/recipe-kit-stencil/src/components/rk-chip/readme.md +52 -0
- package/packages/recipe-kit-stencil/src/components/rk-chip/rk-chip.css +155 -0
- package/packages/recipe-kit-stencil/src/components/rk-chip/rk-chip.tsx +105 -0
- package/packages/recipe-kit-stencil/src/components/rk-empty-state/readme.md +44 -0
- package/packages/recipe-kit-stencil/src/components/rk-empty-state/rk-empty-state.css +64 -0
- package/packages/recipe-kit-stencil/src/components/rk-empty-state/rk-empty-state.tsx +50 -0
- package/packages/recipe-kit-stencil/src/components/rk-meal-slot/readme.md +58 -0
- package/packages/recipe-kit-stencil/src/components/rk-meal-slot/rk-meal-slot.css +225 -0
- package/packages/recipe-kit-stencil/src/components/rk-meal-slot/rk-meal-slot.tsx +182 -0
- package/packages/recipe-kit-stencil/src/components/rk-modal/readme.md +57 -0
- package/packages/recipe-kit-stencil/src/components/rk-modal/rk-modal.css +163 -0
- package/packages/recipe-kit-stencil/src/components/rk-modal/rk-modal.tsx +176 -0
- package/packages/recipe-kit-stencil/src/components/rk-rating/readme.md +42 -0
- package/packages/recipe-kit-stencil/src/components/rk-rating/rk-rating.css +71 -0
- package/packages/recipe-kit-stencil/src/components/rk-rating/rk-rating.tsx +114 -0
- package/packages/recipe-kit-stencil/src/components/rk-recipe-card/readme.md +66 -0
- package/packages/recipe-kit-stencil/src/components/rk-recipe-card/rk-recipe-card.css +280 -0
- package/packages/recipe-kit-stencil/src/components/rk-recipe-card/rk-recipe-card.tsx +179 -0
- package/packages/recipe-kit-stencil/src/components/rk-recipe-card/test/rk-recipe-card.spec.tsx +70 -0
- package/packages/recipe-kit-stencil/src/components/rk-search-bar/readme.md +67 -0
- package/packages/recipe-kit-stencil/src/components/rk-search-bar/rk-search-bar.css +141 -0
- package/packages/recipe-kit-stencil/src/components/rk-search-bar/rk-search-bar.tsx +152 -0
- package/packages/recipe-kit-stencil/src/components/rk-search-bar/test/rk-search-bar.spec.tsx +93 -0
- package/packages/recipe-kit-stencil/src/components/rk-segmented-control/readme.md +41 -0
- package/packages/recipe-kit-stencil/src/components/rk-segmented-control/rk-segmented-control.css +91 -0
- package/packages/recipe-kit-stencil/src/components/rk-segmented-control/rk-segmented-control.tsx +115 -0
- package/packages/recipe-kit-stencil/src/components/rk-tag-input/readme.md +47 -0
- package/packages/recipe-kit-stencil/src/components/rk-tag-input/rk-tag-input.css +146 -0
- package/packages/recipe-kit-stencil/src/components/rk-tag-input/rk-tag-input.tsx +181 -0
- package/packages/recipe-kit-stencil/src/components/rk-tag-input/test/rk-tag-input.spec.tsx +76 -0
- package/packages/recipe-kit-stencil/src/components.d.ts +1192 -0
- package/packages/recipe-kit-stencil/src/global/rk-global.css +104 -0
- package/packages/recipe-kit-stencil/src/index.html +259 -0
- package/packages/recipe-kit-stencil/src/index.ts +6 -0
- package/packages/recipe-kit-stencil/stencil.config.ts +41 -0
- package/packages/recipe-kit-stencil/tsconfig.json +24 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
display: block;
|
|
3
|
+
font-family: var(--rk-font);
|
|
4
|
+
font-size: var(--rk-font-size, 15px);
|
|
5
|
+
color: var(--rk-ink, #211a16);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.label {
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: space-between;
|
|
12
|
+
gap: 10px;
|
|
13
|
+
margin-bottom: 7px;
|
|
14
|
+
font-size: 0.88em;
|
|
15
|
+
font-weight: 600;
|
|
16
|
+
color: var(--rk-ink-2, #52443c);
|
|
17
|
+
cursor: text;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.counter {
|
|
21
|
+
font-size: 0.88em;
|
|
22
|
+
font-weight: 500;
|
|
23
|
+
color: var(--rk-muted, #857166);
|
|
24
|
+
font-variant-numeric: tabular-nums;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.shell {
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-wrap: wrap;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: 6px;
|
|
32
|
+
padding: 8px 10px;
|
|
33
|
+
min-height: 46px;
|
|
34
|
+
background: var(--rk-surface, #fff);
|
|
35
|
+
border: 1px solid var(--rk-border, #ecdfd4);
|
|
36
|
+
border-radius: var(--rk-radius, 14px);
|
|
37
|
+
cursor: text;
|
|
38
|
+
transition: border-color var(--rk-transition), box-shadow var(--rk-transition);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.shell:focus-within {
|
|
42
|
+
border-color: var(--rk-primary, #d94f1b);
|
|
43
|
+
box-shadow: var(--rk-focus-ring);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.shell.has-error {
|
|
47
|
+
border-color: var(--rk-danger, #c2410c);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.shell.has-error:focus-within {
|
|
51
|
+
box-shadow: 0 0 0 3px rgba(194, 65, 12, 0.24);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.shell.is-disabled {
|
|
55
|
+
opacity: 0.6;
|
|
56
|
+
cursor: not-allowed;
|
|
57
|
+
background: var(--rk-surface-2, #fdf5ef);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.tag {
|
|
61
|
+
display: inline-flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
gap: 2px;
|
|
64
|
+
padding: 4px 4px 4px 11px;
|
|
65
|
+
border-radius: var(--rk-radius-pill, 999px);
|
|
66
|
+
background: var(--rk-primary-soft, #fdeee6);
|
|
67
|
+
color: var(--rk-primary-strong, #b23c11);
|
|
68
|
+
font-size: 0.85em;
|
|
69
|
+
font-weight: 600;
|
|
70
|
+
max-width: 100%;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.tag-text {
|
|
74
|
+
overflow: hidden;
|
|
75
|
+
text-overflow: ellipsis;
|
|
76
|
+
white-space: nowrap;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.tag-remove {
|
|
80
|
+
display: inline-flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
justify-content: center;
|
|
83
|
+
width: 19px;
|
|
84
|
+
height: 19px;
|
|
85
|
+
padding: 0;
|
|
86
|
+
border: 0;
|
|
87
|
+
border-radius: 50%;
|
|
88
|
+
background: transparent;
|
|
89
|
+
color: inherit;
|
|
90
|
+
cursor: pointer;
|
|
91
|
+
opacity: 0.7;
|
|
92
|
+
transition: background var(--rk-transition), opacity var(--rk-transition);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.tag-remove:hover:not(:disabled) {
|
|
96
|
+
opacity: 1;
|
|
97
|
+
background: rgba(178, 60, 17, 0.16);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.tag-remove:focus-visible {
|
|
101
|
+
outline: none;
|
|
102
|
+
opacity: 1;
|
|
103
|
+
box-shadow: var(--rk-focus-ring);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.tag-remove:disabled {
|
|
107
|
+
cursor: not-allowed;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.tag-remove svg {
|
|
111
|
+
width: 10px;
|
|
112
|
+
height: 10px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.entry {
|
|
116
|
+
flex: 1 1 120px;
|
|
117
|
+
min-width: 110px;
|
|
118
|
+
border: 0;
|
|
119
|
+
outline: none;
|
|
120
|
+
background: transparent;
|
|
121
|
+
font: inherit;
|
|
122
|
+
color: inherit;
|
|
123
|
+
padding: 5px 2px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.entry::placeholder {
|
|
127
|
+
color: var(--rk-muted, #857166);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.entry:disabled {
|
|
131
|
+
cursor: not-allowed;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.error {
|
|
135
|
+
margin: 7px 0 0;
|
|
136
|
+
font-size: 0.84em;
|
|
137
|
+
color: var(--rk-danger, #c2410c);
|
|
138
|
+
font-weight: 550;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@media (prefers-reduced-motion: reduce) {
|
|
142
|
+
.shell,
|
|
143
|
+
.tag-remove {
|
|
144
|
+
transition: none;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { Component, Event, EventEmitter, h, Host, Prop, State, Watch } from '@stencil/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A tag / token editor used for recipe tags and free-form ingredient lists.
|
|
5
|
+
*
|
|
6
|
+
* `values` is an array property — the SvelteKit app assigns it as a DOM
|
|
7
|
+
* property. A JSON-encoded or comma-separated string attribute also works.
|
|
8
|
+
*/
|
|
9
|
+
@Component({
|
|
10
|
+
tag: 'rk-tag-input',
|
|
11
|
+
styleUrl: 'rk-tag-input.css',
|
|
12
|
+
shadow: true,
|
|
13
|
+
})
|
|
14
|
+
export class RkTagInput {
|
|
15
|
+
/** Current tags. Array, JSON string, or comma-separated string. */
|
|
16
|
+
@Prop() values: string[] | string = [];
|
|
17
|
+
|
|
18
|
+
/** Placeholder for the entry field. */
|
|
19
|
+
@Prop() placeholder = 'Add a tag and press Enter';
|
|
20
|
+
|
|
21
|
+
/** Visible field label. */
|
|
22
|
+
@Prop() label?: string;
|
|
23
|
+
|
|
24
|
+
/** Maximum number of tags. `0` means unlimited. */
|
|
25
|
+
@Prop() maxTags = 0;
|
|
26
|
+
|
|
27
|
+
/** Prevent editing. */
|
|
28
|
+
@Prop() disabled = false;
|
|
29
|
+
|
|
30
|
+
/** Validation message rendered under the field. */
|
|
31
|
+
@Prop() error?: string;
|
|
32
|
+
|
|
33
|
+
/** Emitted with the full, updated list whenever tags change. */
|
|
34
|
+
@Event({ eventName: 'rkTagsChange' }) rkTagsChange!: EventEmitter<string[]>;
|
|
35
|
+
|
|
36
|
+
@State() private draft = '';
|
|
37
|
+
@State() private tags: string[] = [];
|
|
38
|
+
|
|
39
|
+
private inputEl?: HTMLInputElement;
|
|
40
|
+
|
|
41
|
+
@Watch('values')
|
|
42
|
+
protected onValuesChange() {
|
|
43
|
+
this.syncTags();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
componentWillLoad() {
|
|
47
|
+
this.syncTags();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private syncTags() {
|
|
51
|
+
const incoming = this.values;
|
|
52
|
+
if (Array.isArray(incoming)) {
|
|
53
|
+
this.tags = incoming.filter(tag => typeof tag === 'string');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (typeof incoming === 'string' && incoming.trim()) {
|
|
57
|
+
const trimmed = incoming.trim();
|
|
58
|
+
if (trimmed.startsWith('[')) {
|
|
59
|
+
try {
|
|
60
|
+
const decoded = JSON.parse(trimmed);
|
|
61
|
+
this.tags = Array.isArray(decoded) ? decoded.map(String) : [];
|
|
62
|
+
return;
|
|
63
|
+
} catch {
|
|
64
|
+
/* fall through to comma parsing */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
this.tags = trimmed
|
|
68
|
+
.split(',')
|
|
69
|
+
.map(tag => tag.trim())
|
|
70
|
+
.filter(Boolean);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.tags = [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private get atCapacity() {
|
|
77
|
+
return this.maxTags > 0 && this.tags.length >= this.maxTags;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private commit(next: string[]) {
|
|
81
|
+
this.tags = next;
|
|
82
|
+
this.rkTagsChange.emit([...next]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private addDraft() {
|
|
86
|
+
const candidates = this.draft
|
|
87
|
+
.split(',')
|
|
88
|
+
.map(tag => tag.trim())
|
|
89
|
+
.filter(Boolean);
|
|
90
|
+
if (candidates.length === 0) return;
|
|
91
|
+
|
|
92
|
+
const next = [...this.tags];
|
|
93
|
+
for (const candidate of candidates) {
|
|
94
|
+
if (this.maxTags > 0 && next.length >= this.maxTags) break;
|
|
95
|
+
// Case-insensitive de-duplication keeps "Vegan" and "vegan" from coexisting.
|
|
96
|
+
if (next.some(tag => tag.toLowerCase() === candidate.toLowerCase())) continue;
|
|
97
|
+
next.push(candidate);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.draft = '';
|
|
101
|
+
if (next.length !== this.tags.length) this.commit(next);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private removeAt(index: number) {
|
|
105
|
+
this.commit(this.tags.filter((_, position) => position !== index));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private handleKeyDown = (event: KeyboardEvent) => {
|
|
109
|
+
if (event.key === 'Enter' || event.key === ',') {
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
this.addDraft();
|
|
112
|
+
} else if (event.key === 'Backspace' && this.draft === '' && this.tags.length > 0) {
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
this.removeAt(this.tags.length - 1);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
render() {
|
|
119
|
+
return (
|
|
120
|
+
<Host>
|
|
121
|
+
{this.label && (
|
|
122
|
+
<label class="label" part="label" onClick={() => this.inputEl?.focus()}>
|
|
123
|
+
{this.label}
|
|
124
|
+
{this.maxTags > 0 && (
|
|
125
|
+
<span class="counter">
|
|
126
|
+
{this.tags.length}/{this.maxTags}
|
|
127
|
+
</span>
|
|
128
|
+
)}
|
|
129
|
+
</label>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
<div
|
|
133
|
+
class={{ shell: true, 'is-disabled': this.disabled, 'has-error': !!this.error }}
|
|
134
|
+
part="shell"
|
|
135
|
+
onClick={() => !this.disabled && this.inputEl?.focus()}
|
|
136
|
+
>
|
|
137
|
+
{this.tags.map((tag, index) => (
|
|
138
|
+
<span class="tag" part="tag" key={`${tag}-${index}`}>
|
|
139
|
+
<span class="tag-text">{tag}</span>
|
|
140
|
+
<button
|
|
141
|
+
class="tag-remove"
|
|
142
|
+
type="button"
|
|
143
|
+
aria-label={`Remove ${tag}`}
|
|
144
|
+
disabled={this.disabled}
|
|
145
|
+
onClick={event => {
|
|
146
|
+
event.stopPropagation();
|
|
147
|
+
this.removeAt(index);
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" aria-hidden="true">
|
|
151
|
+
<path d="M6 6l12 12M18 6 6 18" />
|
|
152
|
+
</svg>
|
|
153
|
+
</button>
|
|
154
|
+
</span>
|
|
155
|
+
))}
|
|
156
|
+
|
|
157
|
+
<input
|
|
158
|
+
ref={el => (this.inputEl = el)}
|
|
159
|
+
class="entry"
|
|
160
|
+
part="input"
|
|
161
|
+
type="text"
|
|
162
|
+
value={this.draft}
|
|
163
|
+
placeholder={this.tags.length === 0 ? this.placeholder : this.atCapacity ? '' : 'Add another…'}
|
|
164
|
+
aria-label={this.label ?? 'Add a tag'}
|
|
165
|
+
disabled={this.disabled || this.atCapacity}
|
|
166
|
+
autocomplete="off"
|
|
167
|
+
onInput={event => (this.draft = (event.target as HTMLInputElement).value)}
|
|
168
|
+
onKeyDown={this.handleKeyDown}
|
|
169
|
+
onBlur={() => this.addDraft()}
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{this.error && (
|
|
174
|
+
<p class="error" part="error" role="alert">
|
|
175
|
+
{this.error}
|
|
176
|
+
</p>
|
|
177
|
+
)}
|
|
178
|
+
</Host>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { newSpecPage } from '@stencil/core/testing';
|
|
2
|
+
import { RkTagInput } from '../rk-tag-input';
|
|
3
|
+
|
|
4
|
+
describe('rk-tag-input', () => {
|
|
5
|
+
async function setup(html = `<rk-tag-input></rk-tag-input>`) {
|
|
6
|
+
const page = await newSpecPage({ components: [RkTagInput], html });
|
|
7
|
+
const input = page.root.shadowRoot.querySelector('input') as HTMLInputElement;
|
|
8
|
+
const onChange = jest.fn();
|
|
9
|
+
page.root.addEventListener('rkTagsChange', onChange);
|
|
10
|
+
return { page, input, onChange };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function enter(page: Awaited<ReturnType<typeof newSpecPage>>, input: HTMLInputElement, value: string) {
|
|
14
|
+
input.value = value;
|
|
15
|
+
input.dispatchEvent(new Event('input'));
|
|
16
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
|
17
|
+
await page.waitForChanges();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
it('adds a tag on Enter and emits the whole list', async () => {
|
|
21
|
+
const { page, input, onChange } = await setup();
|
|
22
|
+
|
|
23
|
+
await enter(page, input, 'vegan');
|
|
24
|
+
expect(onChange.mock.calls[0][0].detail).toEqual(['vegan']);
|
|
25
|
+
|
|
26
|
+
await enter(page, input, 'one pot');
|
|
27
|
+
expect(onChange.mock.calls[1][0].detail).toEqual(['vegan', 'one pot']);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('splits a comma-separated entry into several tags', async () => {
|
|
31
|
+
const { page, input, onChange } = await setup();
|
|
32
|
+
|
|
33
|
+
await enter(page, input, 'quick, easy, spicy');
|
|
34
|
+
expect(onChange.mock.calls[0][0].detail).toEqual(['quick', 'easy', 'spicy']);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('rejects case-insensitive duplicates', async () => {
|
|
38
|
+
const { page, input, onChange } = await setup();
|
|
39
|
+
|
|
40
|
+
await enter(page, input, 'Vegan');
|
|
41
|
+
await enter(page, input, 'vegan');
|
|
42
|
+
|
|
43
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
44
|
+
expect(page.root.shadowRoot.querySelectorAll('.tag')).toHaveLength(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('stops accepting tags at maxTags', async () => {
|
|
48
|
+
const { page, input, onChange } = await setup(`<rk-tag-input max-tags="2"></rk-tag-input>`);
|
|
49
|
+
|
|
50
|
+
await enter(page, input, 'a');
|
|
51
|
+
await enter(page, input, 'b');
|
|
52
|
+
expect(onChange).toHaveBeenCalledTimes(2);
|
|
53
|
+
|
|
54
|
+
expect(input.disabled).toBe(true);
|
|
55
|
+
expect(page.root.shadowRoot.querySelectorAll('.tag')).toHaveLength(2);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('accepts a JSON string attribute as a fallback for the array property', async () => {
|
|
59
|
+
const page = await newSpecPage({
|
|
60
|
+
components: [RkTagInput],
|
|
61
|
+
html: `<rk-tag-input values='["comfort food","one pot"]'></rk-tag-input>`,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const labels = Array.from(page.root.shadowRoot.querySelectorAll('.tag-text')).map(node => node.textContent);
|
|
65
|
+
expect(labels).toEqual(['comfort food', 'one pot']);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('removes the last tag on Backspace when the field is empty', async () => {
|
|
69
|
+
const { page, input, onChange } = await setup(`<rk-tag-input values='["a","b"]'></rk-tag-input>`);
|
|
70
|
+
|
|
71
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Backspace' }));
|
|
72
|
+
await page.waitForChanges();
|
|
73
|
+
|
|
74
|
+
expect(onChange.mock.calls[0][0].detail).toEqual(['a']);
|
|
75
|
+
});
|
|
76
|
+
});
|