Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
8551d7e
migrate some utils to ts
silviuaavram Jul 9, 2025
3c2e826
start useTagGroup
silviuaavram Jul 9, 2025
493652d
docusaurus changes
silviuaavram Jul 9, 2025
1598860
change some styles in docusaurus
silviuaavram Jul 11, 2025
180dbfe
add focus
silviuaavram Jul 11, 2025
925e2b4
focus, delete, add
silviuaavram Jul 14, 2025
11c5967
more updates on active and focus
silviuaavram Jul 15, 2025
7c7803c
tests and some fixes
silviuaavram Jul 19, 2025
ac2a67c
improve styles
silviuaavram Jul 25, 2025
ff7fcca
improve types
silviuaavram Jul 25, 2025
3185d03
more tests
silviuaavram Jul 25, 2025
84f641a
ids
silviuaavram Jul 29, 2025
3a793c1
move utils to ts
silviuaavram Aug 11, 2025
3b2a688
fix build ts errors
silviuaavram Aug 12, 2025
379077a
fix state change types in taggroup
silviuaavram Aug 13, 2025
0d10f55
some more import fixes
silviuaavram Aug 15, 2025
115fe66
move test utils to __tests__
silviuaavram Aug 18, 2025
4aae9fb
add accessible description
silviuaavram Aug 20, 2025
c186454
make coverage 100%
silviuaavram Aug 26, 2025
34f2397
change to listbox + tests
silviuaavram Sep 1, 2025
88dd81d
cypress test
silviuaavram Sep 1, 2025
0f94c5a
fix rebase issue
silviuaavram Dec 2, 2025
88f3781
fix rebase issue
silviuaavram Dec 2, 2025
ef7c17d
improve types support
silviuaavram Dec 3, 2025
a79b46d
improve environment type
silviuaavram Dec 4, 2025
71bb8ec
merge correctly legacy and generated types
silviuaavram Dec 5, 2025
9f9f065
do not allow incorrect indeces
silviuaavram Dec 5, 2025
f5c5b66
remove rollup ts plugin
silviuaavram Dec 9, 2025
7841392
fix rebase error
silviuaavram Dec 9, 2025
af270f5
export from ts file
silviuaavram Dec 9, 2025
fe639b0
export useTagGroup type
silviuaavram Dec 9, 2025
28b725c
revert index
silviuaavram Dec 9, 2025
ddb6c30
add another docusaurus example
silviuaavram Dec 12, 2025
2c4d30e
change types export to modules again
silviuaavram Dec 12, 2025
de1dc3c
fix initial focus
silviuaavram Dec 14, 2025
c74dfc8
add onChange
silviuaavram Jan 15, 2026
e0562e8
wip readme
silviuaavram Jan 15, 2026
154ef55
finish readme
silviuaavram Jan 18, 2026
cf02e5c
fix title
silviuaavram Jan 18, 2026
61a6b92
fix example ts
silviuaavram Jan 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions cypress/e2e/useTagGroup.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
describe('useTagGroup', () => {
const colors = ['Black', 'Red', 'Green', 'Blue', 'Orange']

beforeEach(() => {
cy.visit('/useTagGroup')

// Ensure the listbox exists
cy.findByRole('listbox', {name: /colors example/i}).should('exist')

// Ensure it has 5 color tags
cy.findAllByRole('option').should('have.length', 5)
})

it('clicks a tag and navigates with circular arrow keys', () => {
// Click first tag ("Black")
cy.findByRole('option', {name: /Black/i}).click().should('have.focus')

// Arrow Right navigation through all tags
for (let index = 0; index < colors.length; index++) {
const nextIndex = (index + 1) % colors.length
cy.focused().trigger('keydown', {key: 'ArrowRight'})
cy.findByRole('option', {name: colors[nextIndex]}).should('have.focus')
}

// Arrow Left navigation through all tags (circular)
for (let index = colors.length - 1; index >= 0; index--) {
const prevIndex = (index + colors.length) % colors.length
cy.focused().trigger('keydown', {key: 'ArrowLeft'})
cy.findByRole('option', {name: colors[prevIndex]}).should('have.focus')
}

// Circular on the left.
cy.focused().trigger('keydown', {key: 'ArrowLeft'})
cy.findByRole('option', {name: colors[colors.length - 1]}).should(
'have.focus',
)
})

it('deletes a tag using Delete and Backspace', () => {
// Focus "Red"
cy.findByRole('option', {name: /Red/i}).click()

// Delete key
cy.focused().trigger('keydown', {key: 'Delete'})
cy.findAllByRole('option').should('have.length', 4)

// Next tag should be "Green"
cy.focused().should('contain.text', 'Green')

// Backspace key removes "Green"
cy.focused().trigger('keydown', {key: 'Backspace'})
cy.findAllByRole('option').should('have.length', 3)

// Focus should now be on "Blue"
cy.focused().should('contain.text', 'Blue')
})

it('removes a tag via remove button', () => {
// Remove "Blue" via its remove button
cy.findByRole('option', {name: /Blue/i}).within(() => {
cy.findByRole('button', {name: /remove/i}).click()
})

// Verify 4 tags remain
cy.findAllByRole('option').should('have.length', 4)

// Orange tag should have focus.
cy.findByRole('option', {name: /Orange/i}).should('have.focus')
})

it('adds a tag from the list', () => {
// Focus "Red"
cy.findByRole('option', {name: /Red/i}).click()

// Clicks the Lime option from the add tags list.
cy.findByRole('button', {name: /Lime/i}).click()

// Verify 6 tags are visible
cy.findAllByRole('option').should('have.length', 6)

cy.findByRole('option', {name: /Lime/i}).should('be.visible')
// Including the new option
})
})
2 changes: 1 addition & 1 deletion docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const config = {
blog: false,
pages: {
path: 'docusaurus/pages',
include: ['**/*.{js,jsx}'],
include: ['**/*.{js,jsx,tsx}'],
},
}),
],
Expand Down
6 changes: 6 additions & 0 deletions docusaurus/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ export default function Docs() {
<li>
<a href="./combobox">Downshift</a>
</li>
<li>
<a href="./useTagGroup">useTagGroup</a>
</li>
<li>
<a href="./useTagGroupCombobox">useTagGroupCombobox</a>
</li>
</ul>
</div>
)
Expand Down
8 changes: 4 additions & 4 deletions docusaurus/pages/useMultipleCombobox.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
colors,
containerStyles,
menuStyles,
selectedItemsContainerSyles,
selectedItemStyles,
tagGroupSyles,
tagStyles,
} from '../utils'

const initialSelectedItems = [colors[0], colors[1]]
Expand Down Expand Up @@ -105,14 +105,14 @@ export default function DropdownMultipleCombobox() {
>
Choose an element:
</label>
<div style={selectedItemsContainerSyles}>
<div style={tagGroupSyles}>
{selectedItems.map(function renderSelectedItem(
selectedItemForRender,
index,
) {
return (
<span
style={selectedItemStyles}
style={tagStyles}
key={`selected-item-${index}`}
{...getSelectedItemProps({
selectedItem: selectedItemForRender,
Expand Down
8 changes: 4 additions & 4 deletions docusaurus/pages/useMultipleSelect.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
colors,
containerStyles,
menuStyles,
selectedItemsContainerSyles,
selectedItemStyles,
tagGroupSyles,
tagStyles,
} from '../utils'

const initialSelectedItems = [colors[0], colors[1]]
Expand Down Expand Up @@ -76,14 +76,14 @@ export default function DropdownMultipleSelect() {
>
Choose an element:
</label>
<div style={selectedItemsContainerSyles}>
<div style={tagGroupSyles}>
{selectedItems.map(function renderSelectedItem(
selectedItemForRender,
index,
) {
return (
<span
style={selectedItemStyles}
style={tagStyles}
key={`selected-item-${index}`}
{...getSelectedItemProps({
selectedItem: selectedItemForRender,
Expand Down
40 changes: 40 additions & 0 deletions docusaurus/pages/useTagGroup.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.tag-group {
display: inline-flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
padding: 6px;
}

.tag {
border: solid 1px darkgreen;
background-color: green;
padding: 0 6px;
margin: 0 2px;
border-radius: 10px;
cursor: default;
}

.tag:hover {
opacity: 0.5;
}

.tag:focus {
background-color: red;
border-color: darkred;
}

.tag-remove-button {
padding: 4px;
cursor: pointer;
border: none;
background-color: transparent;
}

.item-to-add {
cursor: pointer;
}

.selected-tag {
font-style: italic;
}
64 changes: 64 additions & 0 deletions docusaurus/pages/useTagGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as React from 'react'

import {useTagGroup} from '../../src'
import {colors} from '../utils'

import './useTagGroup.css'

export default function TagGroup() {
const initialItems = colors.slice(0, 5)
const {
addItem,
getTagProps,
getTagRemoveProps,
getTagGroupProps,
items,
activeIndex,
} = useTagGroup({initialItems})
const itemsToAdd = colors.filter(color => !items.includes(color))

return (
<div>
<div
{...getTagGroupProps({'aria-label': 'colors example'})}
className="tag-group"
>
{items.map((color, index) => (
<span
className={`${index === activeIndex ? 'selected-tag' : ''} tag`}
key={color}
{...getTagProps({index, 'aria-label': color})}
>
{color}
<button
className="tag-remove-button"
type="button"
{...getTagRemoveProps({index, 'aria-label': 'remove'})}
>
&#10005;
</button>
</span>
))}
</div>
<div>Add more items:</div>
<ul>
{itemsToAdd.map(item => (
<li key={item}>
<button
className="item-to-add"
tabIndex={0}
onClick={() => {
addItem(item)
}}
onKeyDown={({key}) => {
key === 'Enter' && addItem(item)
}}
>
{item}
</button>
</li>
))}
</ul>
</div>
)
}
54 changes: 54 additions & 0 deletions docusaurus/pages/useTagGroupCombobox.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
.wrapper {
width: 18rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}

.wrapper label {
width: fit-content;
}

.input-wrapper {
display: flex;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
background-color: white;
gap: 0.125rem;
}

.text-input {
width: 100%;
padding: 0.375rem;
}

.toggle-button {
padding-left: 0.5rem;
padding-right: 0.5rem;
}

.menu {
position: absolute;
width: 18rem;
background-color: white;
margin-top: 0.25rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
max-height: 20rem;
overflow-y: scroll;
padding: 0;
z-index: 10;
}

.menu.hidden {
display: none;
}

.menu-item {
padding: 0.5rem 0.75rem;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
}

.menu-item.highlighted {
background-color: #93c5fd;
}
Loading