frutjam 2.0.0 → 2.0.2
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/LICENSE +20 -20
- package/README.md +172 -172
- package/dist/base/reboot/object.js +1 -1
- package/dist/base/tokens/object.js +1 -1
- package/dist/base.css +4 -4
- package/dist/components/accordion/object.js +1 -1
- package/dist/components/accordion.css +2 -2
- package/dist/components/alert.css +2 -2
- package/dist/components/avatar/object.js +1 -1
- package/dist/components/avatar.css +2 -2
- package/dist/components/badge.css +2 -2
- package/dist/components/breadcrumb.css +2 -2
- package/dist/components/button/object.js +1 -1
- package/dist/components/button.css +4 -4
- package/dist/components/card/object.js +1 -1
- package/dist/components/card.css +2 -2
- package/dist/components/carousel/object.js +1 -1
- package/dist/components/carousel.css +2 -2
- package/dist/components/chat/object.js +1 -1
- package/dist/components/chat.css +2 -2
- package/dist/components/checkbox/object.js +1 -1
- package/dist/components/checkbox.css +2 -2
- package/dist/components/collapsible.css +2 -2
- package/dist/components/combobox/object.js +1 -1
- package/dist/components/combobox.css +2 -2
- package/dist/components/countdown/object.js +1 -1
- package/dist/components/countdown.css +2 -2
- package/dist/components/diff/object.js +1 -1
- package/dist/components/diff.css +2 -2
- package/dist/components/divider/object.js +1 -1
- package/dist/components/divider.css +2 -2
- package/dist/components/drawer/object.js +1 -1
- package/dist/components/drawer.css +2 -2
- package/dist/components/footer/object.js +1 -1
- package/dist/components/footer.css +2 -2
- package/dist/components/header/object.js +1 -1
- package/dist/components/header.css +2 -2
- package/dist/components/hero.css +2 -2
- package/dist/components/indicator.css +2 -2
- package/dist/components/input/object.js +1 -1
- package/dist/components/input.css +2 -2
- package/dist/components/join.css +2 -2
- package/dist/components/kbd.css +2 -2
- package/dist/components/link.css +2 -2
- package/dist/components/loading/object.js +1 -1
- package/dist/components/loading.css +2 -2
- package/dist/components/mask.css +2 -2
- package/dist/components/menu/object.js +1 -1
- package/dist/components/menu.css +2 -2
- package/dist/components/modal/object.js +1 -1
- package/dist/components/modal.css +2 -2
- package/dist/components/navbar.css +2 -2
- package/dist/components/popover/object.js +1 -1
- package/dist/components/popover.css +2 -2
- package/dist/components/progress/object.js +1 -1
- package/dist/components/progress.css +2 -2
- package/dist/components/radial-progress/object.js +1 -1
- package/dist/components/radial-progress.css +2 -2
- package/dist/components/radio/object.js +1 -1
- package/dist/components/radio.css +2 -2
- package/dist/components/range/object.js +1 -1
- package/dist/components/range.css +2 -2
- package/dist/components/rating/object.js +1 -1
- package/dist/components/rating.css +2 -2
- package/dist/components/select/object.js +1 -1
- package/dist/components/select.css +2 -2
- package/dist/components/sidebar.css +2 -2
- package/dist/components/skeleton/object.js +1 -1
- package/dist/components/skeleton.css +2 -2
- package/dist/components/stat.css +2 -2
- package/dist/components/status.css +2 -2
- package/dist/components/steps.css +2 -2
- package/dist/components/surface.css +2 -2
- package/dist/components/swap/object.js +1 -1
- package/dist/components/swap.css +2 -2
- package/dist/components/table/object.js +1 -1
- package/dist/components/table.css +2 -2
- package/dist/components/tabs.css +2 -2
- package/dist/components/tag.css +2 -2
- package/dist/components/textarea.css +2 -2
- package/dist/components/timeline/object.js +1 -1
- package/dist/components/timeline.css +2 -2
- package/dist/components/toast.css +2 -2
- package/dist/components/toggle.css +2 -2
- package/dist/components/tooltip/object.js +1 -1
- package/dist/components/tooltip.css +2 -2
- package/dist/frutjam.min.css +6 -6
- package/dist/plugin.js +56 -9
- package/dist/themes/darkberry.css +1 -1
- package/dist/themes/snowberry.css +1 -1
- package/dist/utilities/headings/object.js +1 -1
- package/dist/utilities/headings.css +2 -2
- package/dist/utilities/para.css +2 -2
- package/dist/utilities/typography/object.js +1 -1
- package/dist/utilities/typography.css +2 -2
- package/dist/utilities/wrapper.css +2 -2
- package/index.js +315 -272
- package/package.json +101 -101
- package/packages/js/carousel.js +2 -1
- package/packages/js/collapsible.js +9 -3
- package/packages/js/combobox.js +23 -1
- package/packages/js/tabs.js +58 -2
- package/packages/js/toast.js +8 -0
- package/packages/react/useCollapsible.js +11 -3
- package/packages/react/useCombobox.js +14 -2
- package/packages/react/useTabs.js +33 -3
package/package.json
CHANGED
|
@@ -1,101 +1,101 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "frutjam",
|
|
3
|
-
"version": "2.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.
|
|
94
|
-
"autoprefixer": "^10.5.0",
|
|
95
|
-
"cssnano": "^
|
|
96
|
-
"postcss": "^8.5.
|
|
97
|
-
"postcss-js": "^5.1.0",
|
|
98
|
-
"rimraf": "^6.1.3",
|
|
99
|
-
"tailwindcss": "^4.
|
|
100
|
-
}
|
|
101
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "frutjam",
|
|
3
|
+
"version": "2.0.2",
|
|
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.3.0",
|
|
94
|
+
"autoprefixer": "^10.5.0",
|
|
95
|
+
"cssnano": "^8.0.1",
|
|
96
|
+
"postcss": "^8.5.14",
|
|
97
|
+
"postcss-js": "^5.1.0",
|
|
98
|
+
"rimraf": "^6.1.3",
|
|
99
|
+
"tailwindcss": "^4.3.0"
|
|
100
|
+
}
|
|
101
|
+
}
|
package/packages/js/carousel.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/packages/js/combobox.js
CHANGED
|
@@ -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) =>
|
|
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
|
}
|
package/packages/js/tabs.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
tab.
|
|
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() {
|
package/packages/js/toast.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|