@gcforms/tag-input 1.0.4 → 1.0.6

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 CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.6] - 2026-05-07
9
+
10
+ - Allow passing onBlur enabling flows to mark valid and invalid addresses without requiring the user to press Enter
11
+
12
+ ## [1.0.5] - 2025-12-05
13
+
14
+ - Remove cypress test over to vitest outside of package directory
8
15
 
9
16
  ## [1.0.4] - 2025-10-01
10
17
 
@@ -32,6 +39,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
32
39
 
33
40
  ## [1.0.0] - 2025-05-14
34
41
 
35
- ### Added
36
-
37
42
  - Initial release
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gcforms/tag-input",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "author": "Canadian Digital Service",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
package/src/TagInput.tsx CHANGED
@@ -28,6 +28,7 @@ export const TagInput = ({
28
28
  restrictDuplicates = true,
29
29
  allowSpacesInTags = true,
30
30
  maxTags,
31
+ onBlur,
31
32
  onTagAdd,
32
33
  onTagRemove,
33
34
  validateTag,
@@ -42,6 +43,10 @@ export const TagInput = ({
42
43
  restrictDuplicates?: boolean;
43
44
  allowSpacesInTags?: boolean;
44
45
  maxTags?: number;
46
+ onBlur?: (
47
+ event: React.FocusEvent<HTMLInputElement>,
48
+ helpers: { addTag: (tag: string) => void }
49
+ ) => void;
45
50
  onTagAdd?: (tag: string) => void;
46
51
  onTagRemove?: (tag: string) => void;
47
52
  validateTag?: (tag: string) => {
@@ -214,6 +219,10 @@ export const TagInput = ({
214
219
  }
215
220
  };
216
221
 
222
+ const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
223
+ onBlur?.(event, { addTag: handleAddTag });
224
+ };
225
+
217
226
  return (
218
227
  <div className="gc-tag-input-container" onClick={() => tagInputRef.current?.focus()}>
219
228
  <label htmlFor={id} className="gc-tag-input-label">
@@ -244,6 +253,7 @@ export const TagInput = ({
244
253
  name={name}
245
254
  type="text"
246
255
  placeholder={placeholder}
256
+ onBlur={handleBlur}
247
257
  onKeyDown={handleKeyDown}
248
258
  ref={tagInputRef}
249
259
  />
@@ -1,239 +0,0 @@
1
- "use client";
2
- import React from "react";
3
-
4
- import { TagInput } from "../TagInput";
5
-
6
- describe("<TagInput />", () => {
7
- it("renders without crashing", () => {
8
- cy.mount(
9
- <div>
10
- <TagInput initialTags={[]} />
11
- </div>
12
- );
13
- });
14
-
15
- it("accepts initial tags", () => {
16
- cy.mount(
17
- <div>
18
- <TagInput
19
- initialTags={["Tag one", "Tag two", "Tag three"]}
20
- onTagAdd={() => {}}
21
- onTagRemove={() => {}}
22
- />
23
- </div>
24
- );
25
-
26
- cy.get(".gc-tag").should("have.length", 3);
27
- cy.get(".gc-tag").should("contain", "Tag one");
28
- cy.get(".gc-tag").should("contain", "Tag two");
29
- cy.get(".gc-tag").should("contain", "Tag three");
30
- });
31
-
32
- it("sets the name attribute", () => {
33
- cy.mount(
34
- <div>
35
- <TagInput initialTags={[]} name="test-name" />
36
- </div>
37
- );
38
-
39
- cy.get("[data-testid='tag-input']").should("have.attr", "name", "test-name");
40
- });
41
-
42
- it("adds a custom label", () => {
43
- cy.mount(
44
- <div>
45
- <TagInput initialTags={[]} label="Custom Label" />
46
- </div>
47
- );
48
-
49
- cy.get(".gc-tag-input-label").should("contain", "Custom Label");
50
- });
51
-
52
- it("adds a custom description", () => {
53
- cy.mount(
54
- <div>
55
- <TagInput initialTags={[]} description="Custom Description" />
56
- </div>
57
- );
58
-
59
- cy.get(".gc-tag-input-description").should("contain", "Custom Description");
60
- });
61
-
62
- it("adds a tag", () => {
63
- cy.mount(
64
- <div>
65
- <TagInput initialTags={[]} />
66
- </div>
67
- );
68
-
69
- cy.get("[data-testid='tag-input']").type("New Tag{enter}");
70
- cy.get("[data-testid='tag-input']").should("have.value", "");
71
- cy.get(".gc-tag").should("contain", "New Tag");
72
- });
73
-
74
- it("announces that a tag was added", () => {
75
- cy.mount(
76
- <div>
77
- <TagInput initialTags={[]} />
78
- </div>
79
- );
80
- cy.get("[data-testid='tag-input']").type("New Tag{enter}");
81
- cy.get("#tag-input-live-region").should("exist").and("contain", `Tag "New Tag" added`);
82
- });
83
-
84
- it("restricts duplicates", () => {
85
- cy.mount(
86
- <div>
87
- <TagInput initialTags={["Tag 1"]} restrictDuplicates={true} />
88
- </div>
89
- );
90
-
91
- cy.get("[data-testid='tag-input']").type("Tag 1{enter}");
92
- cy.get("[data-testid='tag-input']").should("have.value", "");
93
- cy.get(".gc-tag").should("have.length", 1);
94
- });
95
-
96
- it("announces that a duplicate tag was added", () => {
97
- cy.mount(
98
- <div>
99
- <TagInput restrictDuplicates={true} />
100
- </div>
101
- );
102
-
103
- cy.get("[data-testid='tag-input']").type("Tag 1{enter}Tag 1{enter}");
104
- cy.get("#tag-input-live-region").should("exist").and("contain", `"Tag 1" is a duplicate tag`);
105
- });
106
-
107
- it("allows duplicates", () => {
108
- cy.mount(
109
- <div>
110
- <TagInput initialTags={["Tag 1"]} restrictDuplicates={false} />
111
- </div>
112
- );
113
-
114
- cy.get("[data-testid='tag-input']").type("Tag 1{enter}");
115
- cy.get("[data-testid='tag-input']").should("have.value", "");
116
- cy.get(".gc-tag").should("have.length", 2);
117
- });
118
-
119
- it("removes a tag", () => {
120
- const onTagRemove = cy.stub().as("onTagRemove");
121
-
122
- cy.mount(
123
- <div>
124
- <TagInput initialTags={["Tag 1", "Tag 2"]} onTagAdd={() => {}} onTagRemove={onTagRemove} />
125
- </div>
126
- );
127
-
128
- cy.get(".gc-tag").should("contain", "Tag 2").should("contain", "Tag 1");
129
- cy.get("#tag-0 button").click();
130
- cy.get(".gc-tag").should("contain", "Tag 2").should("not.contain", "Tag 1");
131
- cy.get("@onTagRemove").should("have.been.calledWith", "Tag 1");
132
- });
133
-
134
- it("removes a selected tag", () => {
135
- cy.mount(
136
- <div>
137
- <TagInput initialTags={["one", "two", "three", "four", "five", "six"]} />
138
- </div>
139
- );
140
-
141
- cy.get("[data-testid='tag-input']").type("{leftarrow}{leftarrow}{leftarrow}{leftarrow}{del}");
142
- cy.get("#tag-input-live-region").should("exist").and("contain", `Tag "three" removed`);
143
- cy.get(".gc-tag").should("not.contain", "three");
144
- });
145
-
146
- it("announces when a tag is removed", () => {
147
- cy.mount(
148
- <div>
149
- <TagInput initialTags={["Tag 1"]} onTagAdd={() => {}} onTagRemove={() => {}} />
150
- </div>
151
- );
152
-
153
- cy.get(".gc-tag").should("contain", "Tag 1");
154
- cy.get(".gc-tag button").click();
155
- cy.get("#tag-input-live-region").should("exist").and("contain", `Tag "Tag 1" removed`);
156
- });
157
-
158
- it("calls onTagAdd handler when adding a tag", () => {
159
- const onTagAdd = cy.stub().as("onTagAdd");
160
-
161
- cy.mount(
162
- <div>
163
- <TagInput initialTags={[]} onTagAdd={onTagAdd} onTagRemove={() => {}} />
164
- </div>
165
- );
166
-
167
- cy.get("[data-testid='tag-input']").type("New Tag{enter}");
168
- cy.get("[data-testid='tag-input']").should("have.value", "");
169
- cy.get(".gc-tag").should("contain", "New Tag");
170
- cy.get("@onTagAdd").should("have.been.calledWith", "New Tag");
171
- });
172
-
173
- it("calls onTagRemove handler when removing a tag", () => {
174
- const onTagRemove = cy.stub().as("onTagRemove");
175
-
176
- cy.mount(
177
- <div>
178
- <TagInput initialTags={["Tag one", "Tag two"]} onTagRemove={onTagRemove} />
179
- </div>
180
- );
181
-
182
- cy.get(".gc-tag").should("contain", "Tag one");
183
- cy.get(".gc-tag").first().find("button").click();
184
- cy.get("[data-testid='tag-input']").should("not.contain", "Tag one");
185
- cy.get("@onTagRemove").should("have.been.calledWith", "Tag one");
186
- });
187
-
188
- it("validates the tag according to the validation function", () => {
189
- const validateTag = (tag: string) => {
190
- const errors: string[] = [];
191
-
192
- if (tag.length < 3) {
193
- errors.push("Tag must be at least 3 characters long");
194
- }
195
-
196
- if (/\d/.test(tag)) {
197
- errors.push("Tag must not include numbers");
198
- }
199
-
200
- if (tag.length > 10) {
201
- errors.push("Tag must be at most 10 characters long");
202
- }
203
-
204
- return {
205
- isValid: errors.length === 0,
206
- errors: errors,
207
- };
208
- };
209
-
210
- cy.mount(
211
- <div>
212
- <TagInput validateTag={validateTag} />
213
- </div>
214
- );
215
-
216
- cy.get("[data-testid='tag-input']").type("ab{enter}");
217
- cy.get("[data-testid='tag-input-error'] div").should("have.length", 1);
218
- cy.get("[data-testid='tag-input-error']").should(
219
- "contain",
220
- "Tag must be at least 3 characters long"
221
- );
222
-
223
- cy.get("[data-testid='tag-input']").type("abcdefghijklmnopqrstuvwxy{enter}");
224
- cy.get("[data-testid='tag-input-error'] div").should("have.length", 1);
225
- cy.get("[data-testid='tag-input-error']").should(
226
- "contain",
227
- "Tag must be at most 10 characters long"
228
- );
229
-
230
- // Multiple errors
231
- cy.get("[data-testid='tag-input']").type("T1{enter}");
232
- cy.get("[data-testid='tag-input-error'] div").should("have.length", 2);
233
- cy.get("[data-testid='tag-input-error']").should(
234
- "contain",
235
- "Tag must be at least 3 characters long"
236
- );
237
- cy.get("[data-testid='tag-input-error']").should("contain", "Tag must not include numbers");
238
- });
239
- });