Skip to content

Commit 7b2dc8e

Browse files
committed
fix(tags-input): only exclude tag items in interaction outside
1 parent 4790d22 commit 7b2dc8e

10 files changed

Lines changed: 99 additions & 37 deletions

File tree

.changeset/upset-readers-wear.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@zag-js/tags-input": patch
3+
---
4+
5+
Fix issue where highlighted item doesn't clear when tabbing out of the input to an external button within the `control`
6+
part.

e2e/models/tags-input.model.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ export class TagsInputModel extends Model {
1414
return this.page.locator("[data-testid=input]")
1515
}
1616

17+
get clearTrigger() {
18+
return this.page.locator("[data-part=clear-trigger]")
19+
}
20+
21+
getTagElements() {
22+
return this.page.locator("[data-part=item]")
23+
}
24+
1725
getTag(value: string) {
1826
return this.page.locator(`[data-testid=${value.toLowerCase()}-tag]`)
1927
}
@@ -77,6 +85,10 @@ export class TagsInputModel extends Model {
7785
return this.getTagClose(value).click({ force: true })
7886
}
7987

88+
clickClearTrigger() {
89+
return this.clearTrigger.click({ force: true })
90+
}
91+
8092
async seeTagIsHighlighted(value: string) {
8193
await expect(this.getTag(value)).toHaveAttribute("data-highlighted", "")
8294
}
@@ -85,6 +97,10 @@ export class TagsInputModel extends Model {
8597
await expect(this.getTag(value)).toBeVisible()
8698
}
8799

100+
async seeNoTags() {
101+
return expect(this.getTagElements()).toHaveCount(0)
102+
}
103+
88104
async dontSeeTag(value: string) {
89105
await expect(this.getTag(value)).toBeHidden()
90106
}
@@ -109,7 +125,11 @@ export class TagsInputModel extends Model {
109125
await expect(this.getTagInput(value)).toHaveValue(value)
110126
}
111127

112-
async expectNoTagToBeHighlighted() {
128+
async seeNoHighlightedTag() {
113129
return expect(await this.page.locator("[data-part=item][data-selected]").count()).toBe(0)
114130
}
131+
132+
async seeClearTriggerIsFocused() {
133+
await expect(this.clearTrigger).toBeFocused()
134+
}
115135
}

e2e/tags-input.e2e.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ test.describe("tags-input", () => {
109109
await I.pressKey("ArrowLeft")
110110
await I.clickOutside()
111111

112-
await I.expectNoTagToBeHighlighted()
112+
await I.seeNoHighlightedTag()
113113
})
114114

115115
test("removes tag on close button click", async () => {
@@ -150,7 +150,7 @@ test.describe("tags-input", () => {
150150
await I.addTag("Svelte")
151151
await I.pressKey("ArrowLeft")
152152
await I.pressKey("Escape")
153-
await I.expectNoTagToBeHighlighted()
153+
await I.seeNoHighlightedTag()
154154
})
155155

156156
test("delete + backspace interaction", async () => {
@@ -169,15 +169,15 @@ test.describe("tags-input", () => {
169169

170170
await I.pressKey("Delete")
171171
await I.dontSeeTag("Angular")
172-
await I.expectNoTagToBeHighlighted()
172+
await I.seeNoHighlightedTag()
173173

174174
await I.pressKey("Backspace")
175175
await I.seeTagIsHighlighted("React")
176176

177177
await I.pressKey("Backspace")
178178
await I.dontSeeTag("React")
179179

180-
await I.expectNoTagToBeHighlighted()
180+
await I.seeNoHighlightedTag()
181181
})
182182

183183
test("[addOnPaste: false] pasting should work every time", async () => {
@@ -232,6 +232,26 @@ test.describe("tags-input", () => {
232232
await I.clickControl()
233233
await I.pressKey("Backspace", 1)
234234

235-
await I.expectNoTagToBeHighlighted()
235+
await I.seeNoHighlightedTag()
236+
})
237+
238+
test("tabbing out of input should clear highlighted tag", async () => {
239+
await I.focusInput()
240+
241+
await I.pressKey("ArrowLeft")
242+
await I.seeTagIsHighlighted("Vue")
243+
244+
await I.pressKey("Tab")
245+
await I.seeNoHighlightedTag()
246+
})
247+
248+
test("clear trigger should clear tags", async () => {
249+
await I.addTag("Svelte")
250+
await I.addTag("Solid")
251+
252+
await I.clickClearTrigger()
253+
await I.seeNoTags()
254+
255+
await I.seeInputIsFocused()
236256
})
237257
})

examples/next-ts/pages/tags-input.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export default function Page() {
4444
</span>
4545
))}
4646
<input data-testid="input" placeholder="add tag" {...api.getInputProps()} />
47+
<button {...api.getClearTriggerProps()}>X</button>
4748
</div>
4849
<input {...api.getHiddenInputProps()} />
4950
</div>

examples/nuxt-ts/app/pages/tags-input.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const api = computed(() => tagsInput.connect(service, normalizeProps))
4545
</span>
4646

4747
<input data-testid="input" placeholder="add tag" v-bind="api.getInputProps()" />
48+
<button v-bind="api.getClearTriggerProps()">X</button>
4849
</div>
4950
<input v-bind="api.getHiddenInputProps()" />
5051
</div>

examples/solid-ts/src/routes/tags-input.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,15 @@ export default function Page() {
5454
)}
5555
</For>
5656
<input data-testid="input" placeholder="add tag" {...api().getInputProps()} />
57+
<button {...api().getClearTriggerProps()}>X</button>
5758
</div>
5859
<input {...api().getHiddenInputProps()} />
5960
</div>
6061
</main>
6162

62-
{/* <Toolbar viz controls={controls}>
63+
<Toolbar viz controls={controls}>
6364
<StateVisualizer state={service} context={["value"]} />
64-
</Toolbar> */}
65+
</Toolbar>
6566
</>
6667
)
6768
}

examples/svelte-ts/src/routes/tags-input.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
{/each}
4848

4949
<input data-testid="input" placeholder="add tag" {...api.getInputProps()} />
50+
<button {...api.getClearTriggerProps()}>X</button>
5051
</div>
5152
<input {...api.getHiddenInputProps()} />
5253
</div>

packages/machines/tags-input/src/tags-input.dom.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const getItemInputId = (ctx: Scope, opt: ItemProps) =>
1818
export const getEditInputId = (id: string) => `${id}:input`
1919
export const getEditInputEl = (ctx: Scope, id: string) => ctx.getById<HTMLInputElement>(getEditInputId(id))
2020

21+
export const getItemEls = (ctx: Scope) => queryAll(getRootEl(ctx), `[data-part=item]`)
2122
export const getTagInputEl = (ctx: Scope, opt: ItemProps) => ctx.getById<HTMLInputElement>(getItemInputId(ctx, opt))
2223
export const getRootEl = (ctx: Scope) => ctx.getById<HTMLDivElement>(getRootId(ctx))
2324
export const getInputEl = (ctx: Scope) => ctx.getById<HTMLInputElement>(getInputId(ctx))

packages/machines/tags-input/src/tags-input.machine.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,8 @@ export const machine = createMachine<TagsInputSchema>({
377377
trackInteractOutside({ scope, prop, send }) {
378378
return trackInteractOutside(dom.getInputEl(scope), {
379379
exclude(target) {
380-
return contains(dom.getRootEl(scope), target)
380+
const itemEls = dom.getItemEls(scope)
381+
return itemEls.some((el) => contains(el, target))
381382
},
382383
onFocusOutside: prop("onFocusOutside"),
383384
onPointerDownOutside: prop("onPointerDownOutside"),
@@ -402,7 +403,7 @@ export const machine = createMachine<TagsInputSchema>({
402403
autoResize({ context, prop, scope }) {
403404
let fn_cleanup: VoidFunction | undefined
404405

405-
const raf_cleanup = raf(() => {
406+
queueMicrotask(() => {
406407
const editedTagValue = context.get("editedTagValue")
407408
const editedTagIndex = context.get("editedTagIndex")
408409
if (!editedTagValue || editedTagIndex == null || !prop("editable")) return
@@ -414,7 +415,6 @@ export const machine = createMachine<TagsInputSchema>({
414415
})
415416

416417
return () => {
417-
raf_cleanup()
418418
fn_cleanup?.()
419419
}
420420
},

shared/src/css/tags-input.css

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,18 @@
99
border: 1px solid #ccc;
1010
width: 40em;
1111
border-radius: 2px;
12+
position: relative;
1213
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
13-
}
1414

15-
[data-scope="tags-input"][data-part="control"][data-disabled] {
16-
background: #f9f9f9;
17-
}
15+
&[data-disabled] {
16+
background: #f9f9f9;
17+
}
1818

19-
[data-scope="tags-input"][data-part="control"][data-focus],
20-
[data-scope="tags-input"][data-part="control"]:focus {
21-
outline: 2px solid var(--ring-color);
22-
outline-offset: 2px;
19+
&[data-focus],
20+
&:focus {
21+
outline: 2px solid var(--ring-color);
22+
outline-offset: 2px;
23+
}
2324
}
2425

2526
[data-scope="tags-input"][data-part="item-preview"] {
@@ -32,19 +33,21 @@
3233
font: inherit;
3334
user-select: none;
3435
display: inline-block;
35-
}
3636

37-
[data-scope="tags-input"][data-part="item-preview"][hidden] {
38-
display: none !important;
39-
}
40-
[data-scope="tags-input"][data-part="item-preview"][data-highlighted] {
41-
background-color: #777;
42-
border-color: #777;
43-
color: #eee;
44-
}
45-
[data-scope="tags-input"][data-part="item-preview"][data-disabled] {
46-
opacity: 0.6;
47-
cursor: default;
37+
&[hidden] {
38+
display: none !important;
39+
}
40+
41+
&[data-highlighted] {
42+
background-color: #777;
43+
border-color: #777;
44+
color: #eee;
45+
}
46+
47+
&[data-disabled] {
48+
opacity: 0.6;
49+
cursor: default;
50+
}
4851
}
4952

5053
[data-scope="tags-input"][data-part*="input"] {
@@ -58,16 +61,24 @@
5861
font-size: 100%;
5962
outline: none;
6063
display: inline-block !important;
61-
}
6264

63-
[data-scope="tags-input"][data-part*="input"][hidden] {
64-
display: none !important;
65-
}
65+
&[hidden] {
66+
display: none !important;
67+
}
6668

67-
[data-scope="tags-input"][data-part*="input"]:disabled {
68-
opacity: 0.6;
69+
&:disabled {
70+
opacity: 0.6;
71+
}
6972
}
7073

7174
[data-scope="tags-input"][data-part="item-delete-trigger"] {
7275
all: unset;
7376
}
77+
78+
[data-scope="tags-input"][data-part="clear-trigger"] {
79+
position: absolute;
80+
top: 0;
81+
right: 0.5px;
82+
bottom: 0;
83+
scale: 0.8;
84+
}

0 commit comments

Comments
 (0)