frutjam 2.0.0 → 2.0.1

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.
Files changed (106) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +172 -172
  3. package/dist/base/reboot/object.js +1 -1
  4. package/dist/base/tokens/object.js +1 -1
  5. package/dist/base.css +4 -4
  6. package/dist/components/accordion/object.js +1 -1
  7. package/dist/components/accordion.css +2 -2
  8. package/dist/components/alert.css +2 -2
  9. package/dist/components/avatar/object.js +1 -1
  10. package/dist/components/avatar.css +2 -2
  11. package/dist/components/badge.css +2 -2
  12. package/dist/components/breadcrumb.css +2 -2
  13. package/dist/components/button/object.js +1 -1
  14. package/dist/components/button.css +4 -4
  15. package/dist/components/card/object.js +1 -1
  16. package/dist/components/card.css +2 -2
  17. package/dist/components/carousel/object.js +1 -1
  18. package/dist/components/carousel.css +2 -2
  19. package/dist/components/chat/object.js +1 -1
  20. package/dist/components/chat.css +2 -2
  21. package/dist/components/checkbox/object.js +1 -1
  22. package/dist/components/checkbox.css +2 -2
  23. package/dist/components/collapsible.css +2 -2
  24. package/dist/components/combobox/object.js +1 -1
  25. package/dist/components/combobox.css +2 -2
  26. package/dist/components/countdown/object.js +1 -1
  27. package/dist/components/countdown.css +2 -2
  28. package/dist/components/diff/object.js +1 -1
  29. package/dist/components/diff.css +2 -2
  30. package/dist/components/divider/object.js +1 -1
  31. package/dist/components/divider.css +2 -2
  32. package/dist/components/drawer/object.js +1 -1
  33. package/dist/components/drawer.css +2 -2
  34. package/dist/components/footer/object.js +1 -1
  35. package/dist/components/footer.css +2 -2
  36. package/dist/components/header/object.js +1 -1
  37. package/dist/components/header.css +2 -2
  38. package/dist/components/hero.css +2 -2
  39. package/dist/components/indicator.css +2 -2
  40. package/dist/components/input/object.js +1 -1
  41. package/dist/components/input.css +2 -2
  42. package/dist/components/join.css +2 -2
  43. package/dist/components/kbd.css +2 -2
  44. package/dist/components/link.css +2 -2
  45. package/dist/components/loading/object.js +1 -1
  46. package/dist/components/loading.css +2 -2
  47. package/dist/components/mask.css +2 -2
  48. package/dist/components/menu/object.js +1 -1
  49. package/dist/components/menu.css +2 -2
  50. package/dist/components/modal/object.js +1 -1
  51. package/dist/components/modal.css +2 -2
  52. package/dist/components/navbar.css +2 -2
  53. package/dist/components/popover/object.js +1 -1
  54. package/dist/components/popover.css +2 -2
  55. package/dist/components/progress/object.js +1 -1
  56. package/dist/components/progress.css +2 -2
  57. package/dist/components/radial-progress/object.js +1 -1
  58. package/dist/components/radial-progress.css +2 -2
  59. package/dist/components/radio/object.js +1 -1
  60. package/dist/components/radio.css +2 -2
  61. package/dist/components/range/object.js +1 -1
  62. package/dist/components/range.css +2 -2
  63. package/dist/components/rating/object.js +1 -1
  64. package/dist/components/rating.css +2 -2
  65. package/dist/components/select/object.js +1 -1
  66. package/dist/components/select.css +2 -2
  67. package/dist/components/sidebar.css +2 -2
  68. package/dist/components/skeleton/object.js +1 -1
  69. package/dist/components/skeleton.css +2 -2
  70. package/dist/components/stat.css +2 -2
  71. package/dist/components/status.css +2 -2
  72. package/dist/components/steps.css +2 -2
  73. package/dist/components/surface.css +2 -2
  74. package/dist/components/swap/object.js +1 -1
  75. package/dist/components/swap.css +2 -2
  76. package/dist/components/table/object.js +1 -1
  77. package/dist/components/table.css +2 -2
  78. package/dist/components/tabs.css +2 -2
  79. package/dist/components/tag.css +2 -2
  80. package/dist/components/textarea.css +2 -2
  81. package/dist/components/timeline/object.js +1 -1
  82. package/dist/components/timeline.css +2 -2
  83. package/dist/components/toast.css +2 -2
  84. package/dist/components/toggle.css +2 -2
  85. package/dist/components/tooltip/object.js +1 -1
  86. package/dist/components/tooltip.css +2 -2
  87. package/dist/frutjam.min.css +6 -6
  88. package/dist/plugin.js +6 -6
  89. package/dist/themes/darkberry.css +1 -1
  90. package/dist/themes/snowberry.css +1 -1
  91. package/dist/utilities/headings/object.js +1 -1
  92. package/dist/utilities/headings.css +2 -2
  93. package/dist/utilities/para.css +2 -2
  94. package/dist/utilities/typography/object.js +1 -1
  95. package/dist/utilities/typography.css +2 -2
  96. package/dist/utilities/wrapper.css +2 -2
  97. package/index.js +272 -272
  98. package/package.json +101 -101
  99. package/packages/js/carousel.js +2 -1
  100. package/packages/js/collapsible.js +9 -3
  101. package/packages/js/combobox.js +23 -1
  102. package/packages/js/tabs.js +58 -2
  103. package/packages/js/toast.js +8 -0
  104. package/packages/react/useCollapsible.js +11 -3
  105. package/packages/react/useCombobox.js +14 -2
  106. package/packages/react/useTabs.js +33 -3
package/package.json CHANGED
@@ -1,101 +1,101 @@
1
- {
2
- "name": "frutjam",
3
- "version": "2.0.0",
4
- "description": "A utility-first CSS UI Library for Tailwind CSS",
5
- "type": "module",
6
- "exports": {
7
- ".": "./dist/plugin.js",
8
- "./postcss": "./index.js",
9
- "./js": "./packages/js/index.js",
10
- "./js/*": "./packages/js/*.js",
11
- "./react": "./packages/react/index.js",
12
- "./react/*": "./packages/react/*.js",
13
- "./components/*": "./dist/components/*/index.js",
14
- "./utilities/*": "./dist/utilities/*/index.js",
15
- "./themes/*": "./dist/themes/*/index.js",
16
- "./base/tokens": "./dist/base/tokens/index.js",
17
- "./base/reboot": "./dist/base/reboot/index.js",
18
- "./functions/addPrefix": "./dist/functions/addPrefix.js"
19
- },
20
- "files": [
21
- "index.js",
22
- "dist/",
23
- "packages/"
24
- ],
25
- "style": "./dist/frutjam.min.css",
26
- "scripts": {
27
- "clean": "rimraf ./dist",
28
- "build:cdn": "node scripts/build-cdn.mjs",
29
- "build:plugin": "node scripts/build-plugin.mjs",
30
- "build": "rimraf ./dist && node scripts/build-cdn.mjs && node scripts/build-plugin.mjs",
31
- "prepublishOnly": "rimraf ./dist && node scripts/build-cdn.mjs && node scripts/build-plugin.mjs",
32
- "postversion": "git push origin main --tags"
33
- },
34
- "funding": "https://ko-fi.com/nezanuha",
35
- "repository": {
36
- "type": "git",
37
- "url": "git+https://github.com/nezanuha/frutjam.git"
38
- },
39
- "publishConfig": {
40
- "access": "public",
41
- "provenance": true
42
- },
43
- "keywords": [
44
- "frutjam",
45
- "tailwindcss",
46
- "tailwind css plugin",
47
- "UI Library",
48
- "tailwind",
49
- "components",
50
- "tailwind components",
51
- "tailwind component library",
52
- "tailwind ui",
53
- "tailwind ui library",
54
- "tailwind next",
55
- "tailwind nuxt",
56
- "tailwind svelte",
57
- "tailwind astro",
58
- "tailwind django",
59
- "tailwind rails",
60
- "tailwind laravel",
61
- "component library",
62
- "tailwind themes",
63
- "css library",
64
- "theme",
65
- "design system",
66
- "front-end",
67
- "framework",
68
- "W3C standards",
69
- "accessibility web development",
70
- "seo friendly ui",
71
- "rtl ui",
72
- "responsive ui library"
73
- ],
74
- "author": "Nezanuha",
75
- "license": "MIT",
76
- "homepage": "https://frutjam.com",
77
- "bugs": {
78
- "url": "https://github.com/nezanuha/frutjam/issues"
79
- },
80
- "engines": {
81
- "node": ">=18"
82
- },
83
- "peerDependencies": {
84
- "tailwindcss": "^4.0.0",
85
- "react": ">=18"
86
- },
87
- "peerDependenciesMeta": {
88
- "react": {
89
- "optional": true
90
- }
91
- },
92
- "devDependencies": {
93
- "@tailwindcss/postcss": "^4.2.4",
94
- "autoprefixer": "^10.5.0",
95
- "cssnano": "^7.1.7",
96
- "postcss": "^8.5.13",
97
- "postcss-js": "^5.1.0",
98
- "rimraf": "^6.1.3",
99
- "tailwindcss": "^4.2.4"
100
- }
101
- }
1
+ {
2
+ "name": "frutjam",
3
+ "version": "2.0.1",
4
+ "description": "A utility-first CSS UI Library for Tailwind CSS",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./dist/plugin.js",
8
+ "./postcss": "./index.js",
9
+ "./js": "./packages/js/index.js",
10
+ "./js/*": "./packages/js/*.js",
11
+ "./react": "./packages/react/index.js",
12
+ "./react/*": "./packages/react/*.js",
13
+ "./components/*": "./dist/components/*/index.js",
14
+ "./utilities/*": "./dist/utilities/*/index.js",
15
+ "./themes/*": "./dist/themes/*/index.js",
16
+ "./base/tokens": "./dist/base/tokens/index.js",
17
+ "./base/reboot": "./dist/base/reboot/index.js",
18
+ "./functions/addPrefix": "./dist/functions/addPrefix.js"
19
+ },
20
+ "files": [
21
+ "index.js",
22
+ "dist/",
23
+ "packages/"
24
+ ],
25
+ "style": "./dist/frutjam.min.css",
26
+ "scripts": {
27
+ "clean": "rimraf ./dist",
28
+ "build:cdn": "node scripts/build-cdn.mjs",
29
+ "build:plugin": "node scripts/build-plugin.mjs",
30
+ "build": "rimraf ./dist && node scripts/build-cdn.mjs && node scripts/build-plugin.mjs",
31
+ "prepublishOnly": "rimraf ./dist && node scripts/build-cdn.mjs && node scripts/build-plugin.mjs",
32
+ "postversion": "git push origin main --tags"
33
+ },
34
+ "funding": "https://ko-fi.com/nezanuha",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/nezanuha/frutjam.git"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "provenance": true
42
+ },
43
+ "keywords": [
44
+ "frutjam",
45
+ "tailwindcss",
46
+ "tailwind css plugin",
47
+ "UI Library",
48
+ "tailwind",
49
+ "components",
50
+ "tailwind components",
51
+ "tailwind component library",
52
+ "tailwind ui",
53
+ "tailwind ui library",
54
+ "tailwind next",
55
+ "tailwind nuxt",
56
+ "tailwind svelte",
57
+ "tailwind astro",
58
+ "tailwind django",
59
+ "tailwind rails",
60
+ "tailwind laravel",
61
+ "component library",
62
+ "tailwind themes",
63
+ "css library",
64
+ "theme",
65
+ "design system",
66
+ "front-end",
67
+ "framework",
68
+ "W3C standards",
69
+ "accessibility web development",
70
+ "seo friendly ui",
71
+ "rtl ui",
72
+ "responsive ui library"
73
+ ],
74
+ "author": "Nezanuha",
75
+ "license": "MIT",
76
+ "homepage": "https://frutjam.com",
77
+ "bugs": {
78
+ "url": "https://github.com/nezanuha/frutjam/issues"
79
+ },
80
+ "engines": {
81
+ "node": ">=18"
82
+ },
83
+ "peerDependencies": {
84
+ "tailwindcss": "^4.0.0",
85
+ "react": ">=18"
86
+ },
87
+ "peerDependenciesMeta": {
88
+ "react": {
89
+ "optional": true
90
+ }
91
+ },
92
+ "devDependencies": {
93
+ "@tailwindcss/postcss": "^4.2.4",
94
+ "autoprefixer": "^10.5.0",
95
+ "cssnano": "^8.0.0",
96
+ "postcss": "^8.5.14",
97
+ "postcss-js": "^5.1.0",
98
+ "rimraf": "^6.1.3",
99
+ "tailwindcss": "^4.2.4"
100
+ }
101
+ }
@@ -6,7 +6,8 @@ export function createCarousel(el) {
6
6
  const items = getItems()
7
7
  if (!items.length) return
8
8
  current = Math.max(0, Math.min(index, items.length - 1))
9
- items[current].scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' })
9
+ const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
10
+ items[current].scrollIntoView({ behavior: reduced ? 'auto' : 'smooth', block: 'nearest', inline: 'start' })
10
11
  }
11
12
 
12
13
  return {
@@ -1,8 +1,14 @@
1
1
  export function createCollapsible(el) {
2
+ function syncAria() {
3
+ const isOpen = el.classList.contains('collapsible-open')
4
+ const trigger = el.querySelector('[aria-expanded]')
5
+ if (trigger) trigger.setAttribute('aria-expanded', String(isOpen))
6
+ }
7
+
2
8
  return {
3
- open: () => el.classList.add('collapsible-open'),
4
- close: () => el.classList.remove('collapsible-open'),
5
- toggle: () => el.classList.toggle('collapsible-open'),
9
+ open: () => { el.classList.add('collapsible-open'); syncAria() },
10
+ close: () => { el.classList.remove('collapsible-open'); syncAria() },
11
+ toggle: () => { el.classList.toggle('collapsible-open'); syncAria() },
6
12
  isOpen: () => el.classList.contains('collapsible-open'),
7
13
  }
8
14
  }
@@ -1,7 +1,23 @@
1
+ let _comboboxIdCounter = 0
2
+
1
3
  export function createCombobox(el) {
2
4
  const input = el.querySelector('input')
3
5
  let activeIndex = -1
4
6
 
7
+ const uid = ++_comboboxIdCounter
8
+ const listbox = el.querySelector('[role="listbox"]') ?? el.querySelector('.combobox-dropdown') ?? (() => {
9
+ const found = el.querySelector('.combobox-option, [role="option"]')
10
+ return found ? found.parentElement : null
11
+ })()
12
+
13
+ if (listbox && !listbox.id) listbox.id = `fj-combobox-listbox-${uid}`
14
+ if (listbox && !listbox.getAttribute('role')) listbox.setAttribute('role', 'listbox')
15
+ if (input && listbox) {
16
+ input.setAttribute('aria-controls', listbox.id)
17
+ if (!input.getAttribute('role')) input.setAttribute('role', 'combobox')
18
+ if (!input.getAttribute('aria-autocomplete')) input.setAttribute('aria-autocomplete', 'list')
19
+ }
20
+
5
21
  const allOptions = () => Array.from(el.querySelectorAll('[role="option"], .combobox-option'))
6
22
  const visibleOptions = () => allOptions().filter((o) => !o.hidden)
7
23
 
@@ -29,10 +45,16 @@ export function createCombobox(el) {
29
45
 
30
46
  function highlight(index) {
31
47
  const opts = visibleOptions()
32
- opts.forEach((o) => o.classList.remove('combobox-option-active'))
48
+ opts.forEach((o, i) => {
49
+ o.classList.remove('combobox-option-active')
50
+ if (!o.id) o.id = `fj-combobox-option-${uid}-${i}`
51
+ })
33
52
  if (index >= 0 && opts[index]) {
34
53
  opts[index].classList.add('combobox-option-active')
35
54
  opts[index].scrollIntoView({ block: 'nearest' })
55
+ input?.setAttribute('aria-activedescendant', opts[index].id)
56
+ } else {
57
+ input?.removeAttribute('aria-activedescendant')
36
58
  }
37
59
  activeIndex = index
38
60
  }
@@ -1,22 +1,78 @@
1
+ let _tabsIdCounter = 0
2
+
1
3
  export function createTabs(tabsEl, panelsEl) {
4
+ const uid = ++_tabsIdCounter
2
5
  const getTabs = () => Array.from(tabsEl.querySelectorAll('.tab'))
3
6
  const getPanels = () => (panelsEl ? Array.from(panelsEl.children) : [])
4
7
 
8
+ if (!tabsEl.getAttribute('role')) tabsEl.setAttribute('role', 'tablist')
9
+
10
+ function initAria() {
11
+ getTabs().forEach((tab, i) => {
12
+ if (!tab.id) tab.id = `fj-tab-${uid}-${i}`
13
+ if (!tab.getAttribute('role')) tab.setAttribute('role', 'tab')
14
+ })
15
+ getPanels().forEach((panel, i) => {
16
+ if (!panel.id) panel.id = `fj-panel-${uid}-${i}`
17
+ if (!panel.getAttribute('role')) panel.setAttribute('role', 'tabpanel')
18
+ const tab = getTabs()[i]
19
+ if (tab && !panel.getAttribute('aria-labelledby')) {
20
+ panel.setAttribute('aria-labelledby', tab.id)
21
+ }
22
+ if (!panel.hasAttribute('tabindex')) panel.setAttribute('tabindex', '0')
23
+ })
24
+ getTabs().forEach((tab, i) => {
25
+ const panel = getPanels()[i]
26
+ if (panel && !tab.getAttribute('aria-controls')) {
27
+ tab.setAttribute('aria-controls', panel.id)
28
+ }
29
+ })
30
+ }
31
+
5
32
  function activate(index) {
6
33
  getTabs().forEach((tab, i) => {
7
- tab.classList.toggle('tab-active', i === index)
8
- tab.setAttribute('aria-selected', String(i === index))
34
+ const active = i === index
35
+ tab.classList.toggle('tab-active', active)
36
+ tab.setAttribute('aria-selected', String(active))
37
+ tab.setAttribute('tabindex', active ? '0' : '-1')
9
38
  })
10
39
  getPanels().forEach((panel, i) => {
11
40
  panel.hidden = i !== index
12
41
  })
13
42
  }
14
43
 
44
+ initAria()
45
+
15
46
  tabsEl.addEventListener('click', (e) => {
16
47
  const tab = e.target.closest('.tab')
17
48
  if (tab) activate(getTabs().indexOf(tab))
18
49
  })
19
50
 
51
+ tabsEl.addEventListener('keydown', (e) => {
52
+ const tabs = getTabs()
53
+ const current = tabs.findIndex((t) => t === document.activeElement)
54
+ if (current === -1) return
55
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
56
+ e.preventDefault()
57
+ const next = (current + 1) % tabs.length
58
+ activate(next)
59
+ tabs[next].focus()
60
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
61
+ e.preventDefault()
62
+ const prev = (current - 1 + tabs.length) % tabs.length
63
+ activate(prev)
64
+ tabs[prev].focus()
65
+ } else if (e.key === 'Home') {
66
+ e.preventDefault()
67
+ activate(0)
68
+ tabs[0].focus()
69
+ } else if (e.key === 'End') {
70
+ e.preventDefault()
71
+ activate(tabs.length - 1)
72
+ tabs[tabs.length - 1].focus()
73
+ }
74
+ })
75
+
20
76
  return {
21
77
  activate,
22
78
  next() {
@@ -1,8 +1,16 @@
1
1
  export function createToastManager(containerEl) {
2
+ containerEl.setAttribute('aria-live', 'polite')
3
+ containerEl.setAttribute('aria-atomic', 'false')
4
+
2
5
  function show(message, { duration = 3000, type = '' } = {}) {
3
6
  const el = document.createElement('div')
4
7
  el.className = ['alert', type && `alert-${type}`].filter(Boolean).join(' ')
5
8
  el.textContent = message
9
+ if (type === 'error' || type === 'warning') {
10
+ el.setAttribute('role', 'alert')
11
+ } else {
12
+ el.setAttribute('role', 'status')
13
+ }
6
14
  containerEl.appendChild(el)
7
15
  if (duration > 0) setTimeout(() => el.remove(), duration)
8
16
  return el
@@ -2,11 +2,19 @@ import { useRef, useCallback } from 'react'
2
2
 
3
3
  export function useCollapsible() {
4
4
  const ref = useRef(null)
5
+
6
+ function syncAria() {
7
+ if (!ref.current) return
8
+ const isOpen = ref.current.classList.contains('collapsible-open')
9
+ const trigger = ref.current.querySelector('[aria-expanded]')
10
+ if (trigger) trigger.setAttribute('aria-expanded', String(isOpen))
11
+ }
12
+
5
13
  return {
6
14
  ref,
7
- open: useCallback(() => ref.current?.classList.add('collapsible-open'), []),
8
- close: useCallback(() => ref.current?.classList.remove('collapsible-open'), []),
9
- toggle: useCallback(() => ref.current?.classList.toggle('collapsible-open'), []),
15
+ open: useCallback(() => { ref.current?.classList.add('collapsible-open'); syncAria() }, []),
16
+ close: useCallback(() => { ref.current?.classList.remove('collapsible-open'); syncAria() }, []),
17
+ toggle: useCallback(() => { ref.current?.classList.toggle('collapsible-open'); syncAria() }, []),
10
18
  isOpen: () => ref.current?.classList.contains('collapsible-open') ?? false,
11
19
  }
12
20
  }
@@ -1,6 +1,9 @@
1
- import { useState, useCallback } from 'react'
1
+ import { useState, useCallback, useId } from 'react'
2
2
 
3
3
  export function useCombobox({ items = [], filterFn } = {}) {
4
+ const uid = useId()
5
+ const listboxId = `${uid}-listbox`
6
+
4
7
  const [open, setOpen] = useState(false)
5
8
  const [query, setQuery] = useState('')
6
9
  const [activeIndex, setActiveIndex] = useState(-1)
@@ -12,6 +15,8 @@ export function useCombobox({ items = [], filterFn } = {}) {
12
15
  String(item.label ?? item).toLowerCase().includes(query.toLowerCase())
13
16
  )
14
17
 
18
+ const activeDescendant = activeIndex >= 0 ? `${uid}-option-${activeIndex}` : undefined
19
+
15
20
  const select = useCallback((item) => {
16
21
  const label = item.label ?? String(item)
17
22
  setValue(label)
@@ -53,11 +58,18 @@ export function useCombobox({ items = [], filterFn } = {}) {
53
58
  onKeyDown: handleKeyDown,
54
59
  onFocus: () => setOpen(true),
55
60
  onBlur: () => setTimeout(() => setOpen(false), 150),
61
+ role: 'combobox',
56
62
  'aria-expanded': open,
57
63
  'aria-autocomplete': 'list',
58
- role: 'combobox',
64
+ 'aria-controls': listboxId,
65
+ 'aria-activedescendant': activeDescendant,
66
+ },
67
+ listboxProps: {
68
+ id: listboxId,
69
+ role: 'listbox',
59
70
  },
60
71
  optionProps: (item, index) => ({
72
+ id: `${uid}-option-${index}`,
61
73
  className: activeIndex === index ? 'combobox-option combobox-option-active' : 'combobox-option',
62
74
  role: 'option',
63
75
  'aria-selected': activeIndex === index,
@@ -1,23 +1,53 @@
1
- import { useState, useCallback } from 'react'
1
+ import { useState, useCallback, useId } from 'react'
2
2
 
3
3
  export function useTabs(initialIndex = 0) {
4
+ const uid = useId()
4
5
  const [active, setActive] = useState(initialIndex)
5
6
 
6
7
  const activate = useCallback((index) => setActive(index), [])
7
8
 
9
+ const tabId = (index) => `${uid}-tab-${index}`
10
+ const panelId = (index) => `${uid}-panel-${index}`
11
+
12
+ const handleKeyDown = useCallback((index, count) => (e) => {
13
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
14
+ e.preventDefault()
15
+ activate((index + 1) % count)
16
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
17
+ e.preventDefault()
18
+ activate((index - 1 + count) % count)
19
+ } else if (e.key === 'Home') {
20
+ e.preventDefault()
21
+ activate(0)
22
+ } else if (e.key === 'End') {
23
+ e.preventDefault()
24
+ activate(count - 1)
25
+ }
26
+ }, [activate])
27
+
8
28
  return {
9
29
  active,
10
30
  activate,
11
31
  isActive: (index) => active === index,
12
- tabProps: (index) => ({
32
+ tablistProps: {
33
+ role: 'tablist',
34
+ },
35
+ tabProps: (index, count = 0) => ({
36
+ id: tabId(index),
13
37
  className: active === index ? 'tab tab-active' : 'tab',
14
38
  onClick: () => activate(index),
15
- 'aria-selected': active === index,
39
+ onKeyDown: handleKeyDown(index, count),
16
40
  role: 'tab',
41
+ 'aria-selected': active === index,
42
+ 'aria-controls': panelId(index),
43
+ tabIndex: active === index ? 0 : -1,
17
44
  }),
18
45
  panelProps: (index) => ({
46
+ id: panelId(index),
19
47
  hidden: active !== index,
20
48
  role: 'tabpanel',
49
+ 'aria-labelledby': tabId(index),
50
+ tabIndex: 0,
21
51
  }),
22
52
  }
23
53
  }