diff --git a/collections/forms/i18n/en.pot b/collections/forms/i18n/en.pot
index 589f3f8baa..7451179c93 100644
--- a/collections/forms/i18n/en.pot
+++ b/collections/forms/i18n/en.pot
@@ -5,8 +5,8 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
-"POT-Creation-Date: 2024-07-17T14:10:05.852Z\n"
-"PO-Revision-Date: 2024-07-17T14:10:05.853Z\n"
+"POT-Creation-Date: 2024-09-02T18:17:51.871Z\n"
+"PO-Revision-Date: 2024-09-02T18:17:51.873Z\n"
msgid "Upload file"
msgstr "Upload file"
diff --git a/components/chip/API.md b/components/chip/API.md
index 5a687e7615..2d78cc9892 100644
--- a/components/chip/API.md
+++ b/components/chip/API.md
@@ -1,3 +1,21 @@
+### ChipGroup
+
+#### Usage
+
+**Note**: If possible, import the component from the main UI (`@dhis2/ui`) package.
+
+```js
+import { ChipGroup } from '@dhis2-ui/chip'
+```
+
+#### Props
+
+|Name|Type|Default|Required|Description|
+|---|---|---|---|---|
+|children|node|||The Chip components to be rendered within the group|
+|className|string|||Additional CSS class for the chip group|
+|dataTest|string|`'dhis2-uicore-chipgroup'`||Data test id for testing purposes|
+
### Chip
#### Usage
diff --git a/components/chip/src/chip-group/chip-group.js b/components/chip/src/chip-group/chip-group.js
new file mode 100644
index 0000000000..d8b62458f3
--- /dev/null
+++ b/components/chip/src/chip-group/chip-group.js
@@ -0,0 +1,106 @@
+import { theme } from '@dhis2/ui-constants'
+import PropTypes from 'prop-types'
+import React, { useRef, useEffect, useState } from 'react'
+
+const ChipGroup = ({ className, dataTest, children }) => {
+ const chipContainer = useRef(null)
+ const [childrenToFocus, setChildrenToFocus] = useState([])
+
+ useEffect(() => {
+ if (chipContainer.current) {
+ const chips =
+ chipContainer.current.querySelectorAll('[role="option"]')
+ if (chips.length > 0) {
+ const childrenToFocus = Array.from(chips)
+ setChildrenToFocus(childrenToFocus)
+ }
+ }
+ }, [children])
+
+ const handleKeyDown = (event) => {
+ if (!childrenToFocus.length) {
+ return
+ }
+
+ const currentFocus = document.activeElement
+
+ const currentIndex = childrenToFocus.findIndex(
+ (element) => element === currentFocus
+ )
+
+ if (currentIndex === -1) {
+ return
+ }
+
+ if (event.key === 'ArrowRight') {
+ event.preventDefault()
+ const nextIndex = (currentIndex + 1) % childrenToFocus.length
+ childrenToFocus[nextIndex].focus()
+ }
+
+ if (event.key === 'ArrowLeft') {
+ event.preventDefault()
+ const prevIndex =
+ (currentIndex - 1 + childrenToFocus.length) %
+ childrenToFocus.length
+ childrenToFocus[prevIndex].focus()
+ }
+
+ if (event.key === 'Backspace' || event.key === 'Delete') {
+ event.preventDefault()
+ event.stopPropagation()
+ const nextIndex = (currentIndex + 1) % childrenToFocus.length
+ childrenToFocus[nextIndex].focus()
+ }
+ }
+
+ const handleFocus = (event) => {
+ if (!childrenToFocus.length) {
+ return
+ }
+ if (event.target === chipContainer.current) {
+ const selectedChild = childrenToFocus.find(
+ (child) => child.getAttribute('aria-selected') === 'true'
+ )
+ if (!selectedChild && childrenToFocus[0]) {
+ childrenToFocus[0].focus()
+ }
+ selectedChild.focus()
+ }
+ }
+
+ return (
+
+ {children}
+
+
+ )
+}
+
+ChipGroup.defaultProps = {
+ dataTest: 'dhis2-uicore-chipgroup',
+}
+
+ChipGroup.propTypes = {
+ children: PropTypes.node,
+ className: PropTypes.string,
+ dataTest: PropTypes.string,
+}
+
+export { ChipGroup }
diff --git a/components/chip/src/chip-group/index.js b/components/chip/src/chip-group/index.js
new file mode 100644
index 0000000000..d94f294e98
--- /dev/null
+++ b/components/chip/src/chip-group/index.js
@@ -0,0 +1 @@
+export { ChipGroup } from './chip-group.js'
diff --git a/components/chip/src/chip.js b/components/chip/src/chip.js
deleted file mode 100644
index 221bf677cd..0000000000
--- a/components/chip/src/chip.js
+++ /dev/null
@@ -1,149 +0,0 @@
-import { colors, theme } from '@dhis2/ui-constants'
-import cx from 'classnames'
-import PropTypes from 'prop-types'
-import React from 'react'
-import { Content } from './content.js'
-import { Icon } from './icon.js'
-import { Remove } from './remove.js'
-
-const DEFAULT_INLINE_MARGIN = '4'
-
-const Chip = ({
- selected,
- dense,
- disabled,
- dragging,
- overflow,
- className,
- children,
- onRemove,
- onClick,
- icon,
- dataTest,
- marginBottom,
- marginLeft,
- marginRight,
- marginTop,
- marginInlineStart,
- marginInlineEnd,
-}) => (
- {
- if (!disabled && onClick) {
- onClick({}, e)
- }
- }}
- className={cx(className, {
- selected,
- dense,
- disabled,
- dragging,
- })}
- data-test={dataTest}
- >
-
- {children}
-
-
-
-
-
-)
-
-Chip.defaultProps = {
- dataTest: 'dhis2-uicore-chip',
- marginBottom: 4,
- marginTop: 4,
-}
-
-Chip.propTypes = {
- children: PropTypes.any,
- className: PropTypes.string,
- dataTest: PropTypes.string,
- dense: PropTypes.bool,
- disabled: PropTypes.bool,
- dragging: PropTypes.bool,
- icon: PropTypes.element,
- /** `margin-bottom` value, applied in `px` */
- marginBottom: PropTypes.number,
- /** `margin-inline-end` value, applied in `px` */
- marginInlineEnd: PropTypes.number,
- /** `margin-inline-start` value, applied in `px` */
- marginInlineStart: PropTypes.number,
- /** `margin-inline-start` value, applied in `px` */
- marginLeft: PropTypes.number,
- /** `margin-inline-end` value, applied in `px` */
- marginRight: PropTypes.number,
- /** `margin-top` value, applied in `px` */
- marginTop: PropTypes.number,
- overflow: PropTypes.bool,
- selected: PropTypes.bool,
- onClick: PropTypes.func,
- onRemove: PropTypes.func,
-}
-
-export { Chip }
diff --git a/components/chip/src/chip.e2e.stories.js b/components/chip/src/chip/chip.e2e.stories.js
similarity index 100%
rename from components/chip/src/chip.e2e.stories.js
rename to components/chip/src/chip/chip.e2e.stories.js
diff --git a/components/chip/src/chip/chip.js b/components/chip/src/chip/chip.js
new file mode 100644
index 0000000000..a8eaef83f9
--- /dev/null
+++ b/components/chip/src/chip/chip.js
@@ -0,0 +1,178 @@
+import { colors, theme } from '@dhis2/ui-constants'
+import cx from 'classnames'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Content } from './content.js'
+import { Icon } from './icon.js'
+import { Remove } from './remove.js'
+
+const DEFAULT_INLINE_MARGIN = '4'
+
+const Chip = ({
+ selected,
+ dense,
+ disabled,
+ dragging,
+ overflow,
+ className,
+ children,
+ onRemove,
+ onClick,
+ icon,
+ dataTest,
+ marginBottom,
+ marginLeft,
+ marginRight,
+ marginTop,
+ marginInlineStart,
+ marginInlineEnd,
+ component: Component,
+ href,
+ to,
+}) => {
+ const handleKeyDown = (event) => {
+ if (!onRemove) {
+ return
+ }
+ if (event.key === 'Backspace' || event.key === 'Delete') {
+ onRemove({}, event)
+ }
+ }
+
+ const chipProps = {
+ onClick: (e) => {
+ if (!disabled && onClick) {
+ onClick({}, e)
+ }
+ },
+ onKeyDown: handleKeyDown,
+ className: cx('chip', className, {
+ selected,
+ dense,
+ disabled,
+ dragging,
+ }),
+ 'data-test': dataTest,
+ tabIndex: -1,
+ role: 'option',
+ 'aria-selected': selected ? 'true' : 'false',
+ 'aria-disabled': disabled ? 'true' : 'false',
+ }
+
+ const content = (
+ <>
+
+ {children}
+
+ >
+ )
+
+ const chipElement =
+ Component === 'a' ? (
+
+ {content}
+
+ ) : (
+ {content}
+ )
+
+ return (
+ <>
+ {chipElement}
+
+ >
+ )
+}
+
+Chip.defaultProps = {
+ dataTest: 'dhis2-uicore-chip',
+ marginBottom: 4,
+ marginTop: 4,
+}
+
+Chip.propTypes = {
+ children: PropTypes.any,
+ className: PropTypes.string,
+ component: PropTypes.oneOf(['a', 'span']),
+ dataTest: PropTypes.string,
+ dense: PropTypes.bool,
+ disabled: PropTypes.bool,
+ dragging: PropTypes.bool,
+ href: PropTypes.string,
+ icon: PropTypes.element,
+ marginBottom: PropTypes.number,
+ marginInlineEnd: PropTypes.number,
+ marginInlineStart: PropTypes.number,
+ marginLeft: PropTypes.number,
+ marginRight: PropTypes.number,
+ marginTop: PropTypes.number,
+ overflow: PropTypes.bool,
+ selected: PropTypes.bool,
+ to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
+ onClick: PropTypes.func,
+ onRemove: PropTypes.func,
+}
+
+export { Chip }
diff --git a/components/chip/src/chip.prod.stories.js b/components/chip/src/chip/chip.prod.stories.js
similarity index 100%
rename from components/chip/src/chip.prod.stories.js
rename to components/chip/src/chip/chip.prod.stories.js
diff --git a/components/chip/src/content.js b/components/chip/src/chip/content.js
similarity index 100%
rename from components/chip/src/content.js
rename to components/chip/src/chip/content.js
diff --git a/components/chip/src/features/accepts_children.feature b/components/chip/src/chip/features/accepts_children.feature
similarity index 100%
rename from components/chip/src/features/accepts_children.feature
rename to components/chip/src/chip/features/accepts_children.feature
diff --git a/components/chip/src/features/accepts_children/index.js b/components/chip/src/chip/features/accepts_children/index.js
similarity index 100%
rename from components/chip/src/features/accepts_children/index.js
rename to components/chip/src/chip/features/accepts_children/index.js
diff --git a/components/chip/src/features/accepts_icon.feature b/components/chip/src/chip/features/accepts_icon.feature
similarity index 100%
rename from components/chip/src/features/accepts_icon.feature
rename to components/chip/src/chip/features/accepts_icon.feature
diff --git a/components/chip/src/features/accepts_icon/index.js b/components/chip/src/chip/features/accepts_icon/index.js
similarity index 100%
rename from components/chip/src/features/accepts_icon/index.js
rename to components/chip/src/chip/features/accepts_icon/index.js
diff --git a/components/chip/src/features/is_clickable.feature b/components/chip/src/chip/features/is_clickable.feature
similarity index 100%
rename from components/chip/src/features/is_clickable.feature
rename to components/chip/src/chip/features/is_clickable.feature
diff --git a/components/chip/src/features/is_clickable/index.js b/components/chip/src/chip/features/is_clickable/index.js
similarity index 100%
rename from components/chip/src/features/is_clickable/index.js
rename to components/chip/src/chip/features/is_clickable/index.js
diff --git a/components/chip/src/features/is_removable.feature b/components/chip/src/chip/features/is_removable.feature
similarity index 100%
rename from components/chip/src/features/is_removable.feature
rename to components/chip/src/chip/features/is_removable.feature
diff --git a/components/chip/src/features/is_removable/index.js b/components/chip/src/chip/features/is_removable/index.js
similarity index 100%
rename from components/chip/src/features/is_removable/index.js
rename to components/chip/src/chip/features/is_removable/index.js
diff --git a/components/chip/src/icon.js b/components/chip/src/chip/icon.js
similarity index 100%
rename from components/chip/src/icon.js
rename to components/chip/src/chip/icon.js
diff --git a/components/chip/src/chip/index.js b/components/chip/src/chip/index.js
new file mode 100644
index 0000000000..8214f999ee
--- /dev/null
+++ b/components/chip/src/chip/index.js
@@ -0,0 +1 @@
+export { Chip } from './chip.js'
diff --git a/components/chip/src/remove.js b/components/chip/src/chip/remove.js
similarity index 100%
rename from components/chip/src/remove.js
rename to components/chip/src/chip/remove.js
diff --git a/components/chip/src/index.js b/components/chip/src/index.js
index 8214f999ee..9d4b3062bd 100644
--- a/components/chip/src/index.js
+++ b/components/chip/src/index.js
@@ -1 +1,2 @@
-export { Chip } from './chip.js'
+export { Chip } from './chip/index.js'
+export { ChipGroup } from './chip-group/index.js'