Add ARIA accessbility attributes to elements

This commit is contained in:
Tom Southall
2022-02-19 17:09:45 +00:00
parent 2bb09af284
commit d14bd62aeb
12 changed files with 406 additions and 42 deletions

312
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "0.3.0", "version": "0.3.0",
"dependencies": { "dependencies": {
"first-of-type": "^1.0.0", "first-of-type": "^1.0.0",
"nanoid": "^3.3.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react-use": "^17.3.2", "react-use": "^17.3.2",
"setify": "^1.0.4", "setify": "^1.0.4",
@@ -16,6 +17,7 @@
"use-debounce": "^7.0.1" "use-debounce": "^7.0.1"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/react": "^12.1.3",
"@testing-library/react-hooks": "^7.0.2", "@testing-library/react-hooks": "^7.0.2",
"@vitejs/plugin-react": "^1.0.7", "@vitejs/plugin-react": "^1.0.7",
"c8": "^7.11.0", "c8": "^7.11.0",
@@ -594,6 +596,113 @@
"node": ">= 8.0.0" "node": ">= 8.0.0"
} }
}, },
"node_modules/@testing-library/dom": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.11.3.tgz",
"integrity": "sha512-9LId28I+lx70wUiZjLvi1DB/WT2zGOxUh46glrSNMaWVx849kKAluezVzZrXJfTKKoQTmEOutLes/bHg4Bj3aA==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^4.2.0",
"aria-query": "^5.0.0",
"chalk": "^4.1.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.4.4",
"pretty-format": "^27.0.2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@testing-library/dom/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@testing-library/dom/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@testing-library/dom/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/@testing-library/dom/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/@testing-library/dom/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/@testing-library/dom/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@testing-library/react": {
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.3.tgz",
"integrity": "sha512-oCULRXWRrBtC9m6G/WohPo1GLcLesH7T4fuKzRAKn1CWVu9BzXtqLXDDTA6KhFNNtRwLtfSMr20HFl+Qrdrvmg==",
"dev": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@testing-library/dom": "^8.0.0",
"@types/react-dom": "*"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
}
},
"node_modules/@testing-library/react-hooks": { "node_modules/@testing-library/react-hooks": {
"version": "7.0.2", "version": "7.0.2",
"resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-7.0.2.tgz", "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-7.0.2.tgz",
@@ -623,6 +732,12 @@
} }
} }
}, },
"node_modules/@types/aria-query": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
"integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==",
"dev": true
},
"node_modules/@types/chai": { "node_modules/@types/chai": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.0.tgz", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.0.tgz",
@@ -873,6 +988,15 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true "dev": true
}, },
"node_modules/aria-query": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz",
"integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==",
"dev": true,
"engines": {
"node": ">=6.0"
}
},
"node_modules/array-includes": { "node_modules/array-includes": {
"version": "3.1.4", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz",
@@ -1393,6 +1517,12 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/dom-accessibility-api": {
"version": "0.5.11",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.11.tgz",
"integrity": "sha512-7X6GvzjYf4yTdRKuCVScV+aA9Fvh5r8WzWrXBH9w82ZWB/eYDMGCnazoC/YAqAzUJWHzLOnZqr46K3iEyUhUvw==",
"dev": true
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.4.68", "version": "1.4.68",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.68.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.68.tgz",
@@ -3238,6 +3368,15 @@
"get-func-name": "^2.0.0" "get-func-name": "^2.0.0"
} }
}, },
"node_modules/lz-string": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
"integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=",
"dev": true,
"bin": {
"lz-string": "bin/bin.js"
}
},
"node_modules/make-dir": { "node_modules/make-dir": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -3462,10 +3601,9 @@
} }
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.2.0", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz",
"integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==", "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==",
"dev": true,
"bin": { "bin": {
"nanoid": "bin/nanoid.cjs" "nanoid": "bin/nanoid.cjs"
}, },
@@ -3908,6 +4046,38 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
"react-is": "^17.0.1"
},
"engines": {
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
"node_modules/pretty-format/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/pretty-format/node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true
},
"node_modules/prop-types": { "node_modules/prop-types": {
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -5440,6 +5610,84 @@
"picomatch": "^2.2.2" "picomatch": "^2.2.2"
} }
}, },
"@testing-library/dom": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.11.3.tgz",
"integrity": "sha512-9LId28I+lx70wUiZjLvi1DB/WT2zGOxUh46glrSNMaWVx849kKAluezVzZrXJfTKKoQTmEOutLes/bHg4Bj3aA==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^4.2.0",
"aria-query": "^5.0.0",
"chalk": "^4.1.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.4.4",
"pretty-format": "^27.0.2"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"@testing-library/react": {
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.3.tgz",
"integrity": "sha512-oCULRXWRrBtC9m6G/WohPo1GLcLesH7T4fuKzRAKn1CWVu9BzXtqLXDDTA6KhFNNtRwLtfSMr20HFl+Qrdrvmg==",
"dev": true,
"requires": {
"@babel/runtime": "^7.12.5",
"@testing-library/dom": "^8.0.0",
"@types/react-dom": "*"
}
},
"@testing-library/react-hooks": { "@testing-library/react-hooks": {
"version": "7.0.2", "version": "7.0.2",
"resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-7.0.2.tgz", "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-7.0.2.tgz",
@@ -5453,6 +5701,12 @@
"react-error-boundary": "^3.1.0" "react-error-boundary": "^3.1.0"
} }
}, },
"@types/aria-query": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
"integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==",
"dev": true
},
"@types/chai": { "@types/chai": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.0.tgz", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.0.tgz",
@@ -5666,6 +5920,12 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true "dev": true
}, },
"aria-query": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz",
"integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==",
"dev": true
},
"array-includes": { "array-includes": {
"version": "3.1.4", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz",
@@ -6048,6 +6308,12 @@
"esutils": "^2.0.2" "esutils": "^2.0.2"
} }
}, },
"dom-accessibility-api": {
"version": "0.5.11",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.11.tgz",
"integrity": "sha512-7X6GvzjYf4yTdRKuCVScV+aA9Fvh5r8WzWrXBH9w82ZWB/eYDMGCnazoC/YAqAzUJWHzLOnZqr46K3iEyUhUvw==",
"dev": true
},
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.4.68", "version": "1.4.68",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.68.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.68.tgz",
@@ -7307,6 +7573,12 @@
"get-func-name": "^2.0.0" "get-func-name": "^2.0.0"
} }
}, },
"lz-string": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
"integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=",
"dev": true
},
"make-dir": { "make-dir": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -7476,10 +7748,9 @@
} }
}, },
"nanoid": { "nanoid": {
"version": "3.2.0", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz",
"integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==", "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw=="
"dev": true
}, },
"natural-compare": { "natural-compare": {
"version": "1.4.0", "version": "1.4.0",
@@ -7789,6 +8060,31 @@
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true "dev": true
}, },
"pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"requires": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
"react-is": "^17.0.1"
},
"dependencies": {
"ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true
},
"react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true
}
}
},
"prop-types": { "prop-types": {
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",

View File

@@ -26,6 +26,7 @@
}, },
"dependencies": { "dependencies": {
"first-of-type": "^1.0.0", "first-of-type": "^1.0.0",
"nanoid": "^3.3.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react-use": "^17.3.2", "react-use": "^17.3.2",
"setify": "^1.0.4", "setify": "^1.0.4",
@@ -33,6 +34,7 @@
"use-debounce": "^7.0.1" "use-debounce": "^7.0.1"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/react": "^12.1.3",
"@testing-library/react-hooks": "^7.0.2", "@testing-library/react-hooks": "^7.0.2",
"@vitejs/plugin-react": "^1.0.7", "@vitejs/plugin-react": "^1.0.7",
"c8": "^7.11.0", "c8": "^7.11.0",

View File

@@ -8,18 +8,22 @@ const styles = {
const App = () => { const App = () => {
return ( return (
<Turnstone <>
autoFocus={true} <label htmlFor="autocomplete">Search:</label>&nbsp;
data={fruits} <Turnstone
dataSearchType={'startswith'} autoFocus={true}
debounceWait={0} clearButton={true}
itemGroupsAreImmutable={true} data={fruits}
maxItems={10} dataSearchType={'startswith'}
noItemsMessage={'No matching fruit found'} debounceWait={0}
placeholder={'Type something fruity'} id={'autocomplete'}
styles={styles} itemGroupsAreImmutable={true}
minQueryLength={1} maxItems={10}
/> noItemsMessage={'No matching fruit found'}
placeholder={'Type something fruity'}
styles={styles}
/>
</>
) )
} }

View File

@@ -1,5 +1,5 @@
body { body {
margin: 0; margin: 30px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif; sans-serif;

View File

@@ -2,14 +2,21 @@
exports[`Container > Component renders correctly 1`] = ` exports[`Container > Component renders correctly 1`] = `
<div <div
aria-expanded={false}
aria-haspopup="listbox"
aria-owns="undefined-dropdown"
className="query-container-class" className="query-container-class"
role="combobox"
style={ style={
{ {
"display": "inline-block",
"position": "relative", "position": "relative",
} }
} }
> >
<input <input
aria-autocomplete="both"
aria-controls="undefined-dropdown"
autoCapitalize="off" autoCapitalize="off"
autoComplete="off" autoComplete="off"
autoCorrect="off" autoCorrect="off"
@@ -31,6 +38,7 @@ exports[`Container > Component renders correctly 1`] = `
type="text" type="text"
/> />
<input <input
aria-hidden="true"
autoCapitalize="off" autoCapitalize="off"
autoComplete="off" autoComplete="off"
autoCorrect="off" autoCorrect="off"

View File

@@ -2,9 +2,12 @@
exports[`Item > Component renders correctly 1`] = ` exports[`Item > Component renders correctly 1`] = `
<div <div
aria-label="Chicago, Illinois, United States"
aria-selected={[Function]}
className="item-class top-item-class" className="item-class top-item-class"
onMouseDown={[Function]} onMouseDown={[Function]}
onMouseEnter={[Function]} onMouseEnter={[Function]}
role="option"
style={ style={
{ {
"overflow": "hidden", "overflow": "hidden",
@@ -23,9 +26,12 @@ exports[`Item > Component renders correctly 1`] = `
exports[`Item > Link changes the class when hovered 1`] = ` exports[`Item > Link changes the class when hovered 1`] = `
<div <div
aria-label="Chicago, Illinois, United States"
aria-selected={[Function]}
className="highlighted-item-class top-item-class" className="highlighted-item-class top-item-class"
onMouseDown={[Function]} onMouseDown={[Function]}
onMouseEnter={[Function]} onMouseEnter={[Function]}
role="option"
style={ style={
{ {
"overflow": "hidden", "overflow": "hidden",

View File

@@ -1,4 +1,4 @@
import React, { useState, useRef, useContext } from 'react' import React, { useState, useMemo, useRef, useContext } from 'react'
import { StateContext } from '../context/state' import { StateContext } from '../context/state'
import Items from './items' import Items from './items'
import { useDebounce } from 'use-debounce' import { useDebounce } from 'use-debounce'
@@ -25,12 +25,16 @@ export default function Container(props) {
// Destructure props // Destructure props
const { const {
autoFocus, autoFocus,
clearButton,
clearButtonAriaLabel,
clearButtonText,
debounceWait, debounceWait,
defaultItemGroups, defaultItemGroups,
defaultItemGroupsAreImmutable, defaultItemGroupsAreImmutable,
displayField, displayField,
data, data,
dataSearchType, dataSearchType,
id,
isDisabled, isDisabled,
itemGroupsAreImmutable, itemGroupsAreImmutable,
maxItems, maxItems,
@@ -56,6 +60,8 @@ export default function Container(props) {
] ]
} = props } = props
const dropdownId = `${id}-dropdown`
// Global state from context // Global state from context
const { state, dispatch } = useContext(StateContext) const { state, dispatch } = useContext(StateContext)
const { customStyles } = state const { customStyles } = state
@@ -117,15 +123,15 @@ export default function Container(props) {
if (typeof f === 'function') f(queryInput.current.value, highlightedItem) if (typeof f === 'function') f(queryInput.current.value, highlightedItem)
} }
const isX = () => { const hasClearButton = () => {
return !!state.query return clearButton && !!state.query
} }
const isDropdown = () => { const isExpanded = useMemo(() => {
if (hasFocus && !state.query && defaultItemGroups) return true if (hasFocus && !state.query && defaultItemGroups) return true
if (state.query.length < minQueryLength) return false if (state.query.length < minQueryLength) return false
return hasFocus && state.query return hasFocus && !!state.query
} }, [hasFocus, state.query, defaultItemGroups, minQueryLength])
// Handle different keypresses and call the appropriate action creators // Handle different keypresses and call the appropriate action creators
const checkKey = (evt) => { const checkKey = (evt) => {
@@ -157,7 +163,8 @@ export default function Container(props) {
dispatch(setQuery(queryInput.current.value)) dispatch(setQuery(queryInput.current.value))
} }
const handleX = (evt) => { const handleClearButton = (evt) => {
console.log('handleClearButton')
evt.preventDefault() evt.preventDefault()
clearState() clearState()
} }
@@ -181,8 +188,15 @@ export default function Container(props) {
return ( return (
<React.Fragment> <React.Fragment>
<div className={customStyles.queryContainer} style={defaultStyles.queryContainer}> <div
className={customStyles.queryContainer}
style={defaultStyles.queryContainer}
role='combobox'
aria-expanded={isExpanded}
aria-owns={dropdownId}
aria-haspopup='listbox'>
<input <input
id={id}
className={customStyles.query} className={customStyles.query}
style={defaultStyles.query} style={defaultStyles.query}
disabled={isDisabled} disabled={isDisabled}
@@ -198,6 +212,8 @@ export default function Container(props) {
onInput={handleInput} onInput={handleInput}
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur} onBlur={handleBlur}
aria-autocomplete='both'
aria-controls={dropdownId}
/> />
<input <input
@@ -211,15 +227,23 @@ export default function Container(props) {
spellCheck='false' spellCheck='false'
tabIndex='-1' tabIndex='-1'
readOnly='readonly' readOnly='readonly'
aria-hidden='true'
ref={typeaheadInput} ref={typeaheadInput}
/> />
{isX() && ( {hasClearButton() && (
<div className={customStyles.x} style={defaultStyles.x} onMouseDown={handleX} /> <div
className={customStyles.clearButton}
style={defaultStyles.clearButton}
onMouseDown={handleClearButton}
tabIndex={-1}
role='button'
aria-label={clearButtonAriaLabel}>{clearButtonText}</div>
)} )}
{isDropdown() && ( {isExpanded && (
<Items <Items
id={dropdownId}
items={state.items} items={state.items}
noItemsMessage={noItemsMessage} noItemsMessage={noItemsMessage}
/> />

View File

@@ -22,9 +22,11 @@ export default function Item(props) {
return item.text.split(regex) return item.text.split(regex)
}, [splitChar, item]) }, [splitChar, item])
const divClassName = useMemo(() => { const isHighlighted = () => highlighted && index === highlighted.index
const divClassName = () => {
let itemStyle = customStyles[ let itemStyle = customStyles[
(highlighted && index === highlighted.index) (isHighlighted())
? 'highlightedItem' ? 'highlightedItem'
: 'item' : 'item'
] ]
@@ -32,7 +34,7 @@ export default function Item(props) {
return (index === 0 && customStyles.topItem) return (index === 0 && customStyles.topItem)
? `${itemStyle} ${customStyles.topItem}` ? `${itemStyle} ${customStyles.topItem}`
: itemStyle : itemStyle
}, [customStyles, highlighted, index]) }
const handleMouseEnter = () => { const handleMouseEnter = () => {
dispatch(setHighlighted(index)) dispatch(setHighlighted(index))
@@ -63,10 +65,13 @@ export default function Item(props) {
return ( return (
<div <div
className={divClassName} className={divClassName()}
style={defaultStyles.item} style={defaultStyles.item}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseDown={handleClick}> onMouseDown={handleClick}
role='option'
aria-selected={isHighlighted}
aria-label={item.text}>
{itemElement()} {itemElement()}
</div> </div>
) )

View File

@@ -5,13 +5,13 @@ import ItemFirst from './itemFirst'
import Item from './item' import Item from './item'
export default function Items(props) { export default function Items(props) {
const { items, noItemsMessage } = props const { id, items, noItemsMessage } = props
const { state } = useContext(StateContext) const { state } = useContext(StateContext)
const { customStyles } = state const { customStyles } = state
const itemElements = () => { const itemElements = () => {
return ( return (
<div className={customStyles.dropdown} style={defaultStyles.dropdown}> <div id={id} className={customStyles.dropdown} style={defaultStyles.dropdown}>
{items.map((item, index) => {items.map((item, index) =>
index === 0 || item.groupIndex !== items[index - 1].groupIndex ? ( index === 0 || item.groupIndex !== items[index - 1].groupIndex ? (
<ItemFirst <ItemFirst
@@ -30,7 +30,7 @@ export default function Items(props) {
const noItemsMsg = () => { const noItemsMsg = () => {
return ( return (
<div className={customStyles.dropdown} style={defaultStyles.dropdown}> <div id={id} className={customStyles.dropdown} style={defaultStyles.dropdown}>
<div className={customStyles.noItems}>{noItemsMessage}</div> <div className={customStyles.noItems}>{noItemsMessage}</div>
</div> </div>
) )

View File

@@ -1,5 +1,6 @@
const styles = { const styles = {
queryContainer: { queryContainer: {
display: 'inline-block',
position: 'relative' position: 'relative'
}, },
query: { query: {
@@ -13,8 +14,9 @@ const styles = {
top: 0, top: 0,
left: 0 left: 0
}, },
x: { clearButton: {
position: 'absolute', position: 'absolute',
display: 'inline-block',
zIndex: 2 zIndex: 2
} }
} }

View File

@@ -2,6 +2,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { nanoid } from 'nanoid'
import { StateContextProvider } from './context/state' import { StateContextProvider } from './context/state'
import isUndefined from './utils/isUndefined' import isUndefined from './utils/isUndefined'
import Container from './components/container' import Container from './components/container'
@@ -9,8 +10,12 @@ import Container from './components/container'
// Set prop defaults before passing them on to components // Set prop defaults before passing them on to components
const propDefaults = { const propDefaults = {
autoFocus: false, autoFocus: false,
clearButton: false,
clearButtonAriaLabel: 'Clear contents',
clearButtonText: '×',
debounceWait: 250, debounceWait: 250,
defaultItemGroupsAreImmutable: true, defaultItemGroupsAreImmutable: true,
id: nanoid(),
isDisabled: false, isDisabled: false,
itemGroupsAreImmutable: true, itemGroupsAreImmutable: true,
maxItems: 10, maxItems: 10,
@@ -37,6 +42,9 @@ const msgOneOnly = `Both a "data" prop and an "itemGroups" prop were provided. P
const requiredPropsAreMissing = (props) => isUndefined(props.data) && isUndefined(props.itemGroups) const requiredPropsAreMissing = (props) => isUndefined(props.data) && isUndefined(props.itemGroups)
Turnstone.propTypes = { Turnstone.propTypes = {
autoFocus: PropTypes.bool, autoFocus: PropTypes.bool,
clearButton: PropTypes.bool,
clearButtonAriaLabel: PropTypes.string,
clearButtonText: PropTypes.string,
data: (props) => { data: (props) => {
if(requiredPropsAreMissing(props)) return new Error(msgBothRequired) if(requiredPropsAreMissing(props)) return new Error(msgBothRequired)
if(!isUndefined(props.data) && !isUndefined(props.itemGroups)) return new Error(msgOneOnly) if(!isUndefined(props.data) && !isUndefined(props.itemGroups)) return new Error(msgOneOnly)
@@ -51,6 +59,7 @@ Turnstone.propTypes = {
defaultItemGroups: PropTypes.array, defaultItemGroups: PropTypes.array,
defaultItemGroupsAreImmutable: PropTypes.bool, defaultItemGroupsAreImmutable: PropTypes.bool,
displayField: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), displayField: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
id: PropTypes.string,
itemGroups: (props) => { itemGroups: (props) => {
if(requiredPropsAreMissing(props)) return new Error(msgBothRequired) if(requiredPropsAreMissing(props)) return new Error(msgBothRequired)
if(!isUndefined(props.data) && !isUndefined(props.itemGroups)) return new Error(msgOneOnly) if(!isUndefined(props.data) && !isUndefined(props.itemGroups)) return new Error(msgOneOnly)

View File

@@ -24,8 +24,16 @@ describe('Turnstone', () => {
}) })
test('Turnstone component passes all props to Container component along with default props', () => { test('Turnstone component passes all props to Container component along with default props', () => {
expect(component.root.children[0].children[0].props).toEqual({ const props = {...component.root.children[0].children[0].props}
// The id prop is randomly generated so must be excluded
delete props.id
expect(props).toEqual({
autoFocus: false, autoFocus: false,
clearButton: false,
clearButtonAriaLabel: 'Clear contents',
clearButtonText: '×',
debounceWait: 250, debounceWait: 250,
defaultItemGroupsAreImmutable: true, defaultItemGroupsAreImmutable: true,
isDisabled: false, isDisabled: false,