mirror of
https://github.com/filamentphp/filament.git
synced 2026-10-28 03:44:19 +08:00
Merge branch '4.x' into 5.x
Preserve the Livewire 4 notification render hooks while merging lifecycle cleanup, and rebuild conflicted JavaScript bundles.
This commit is contained in:
@@ -123,6 +123,8 @@ jobs:
|
||||
cache: npm
|
||||
- name: Install npm dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
- name: Build JavaScript assets for browser tests
|
||||
run: npm run build:js
|
||||
- name: Install Chromium system dependencies
|
||||
run: |
|
||||
packages=(libnss3 libatk-bridge2.0-0t64 libdrm2 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libxkbcommon0 libpango-1.0-0 libcairo2 libasound2t64)
|
||||
|
||||
Generated
+3
-3
@@ -1409,9 +1409,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
|
||||
"integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
|
||||
+119
-119
@@ -7,7 +7,7 @@
|
||||
"dependencies": {
|
||||
"pixelmatch": "^7.2.0",
|
||||
"puppeteer": "^25.4.0",
|
||||
"sharp": "^0.35.4"
|
||||
"sharp": "^0.35.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
@@ -30,9 +30,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz",
|
||||
"integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==",
|
||||
"version": "0.35.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz",
|
||||
"integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -48,13 +48,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.3.3"
|
||||
"@img/sharp-libvips-darwin-arm64": "1.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz",
|
||||
"integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==",
|
||||
"version": "0.35.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz",
|
||||
"integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -70,20 +70,20 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.3.3"
|
||||
"@img/sharp-libvips-darwin-x64": "1.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-freebsd-wasm32": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz",
|
||||
"integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==",
|
||||
"version": "0.35.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz",
|
||||
"integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"dependencies": {
|
||||
"@img/sharp-wasm32": "0.35.4"
|
||||
"@img/sharp-wasm32": "0.35.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
@@ -93,9 +93,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz",
|
||||
"integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==",
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz",
|
||||
"integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -109,9 +109,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz",
|
||||
"integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==",
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz",
|
||||
"integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -125,9 +125,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz",
|
||||
"integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==",
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz",
|
||||
"integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -144,9 +144,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz",
|
||||
"integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==",
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz",
|
||||
"integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -163,9 +163,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz",
|
||||
"integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==",
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz",
|
||||
"integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -182,9 +182,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz",
|
||||
"integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==",
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz",
|
||||
"integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -201,9 +201,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz",
|
||||
"integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==",
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz",
|
||||
"integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -220,9 +220,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz",
|
||||
"integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==",
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz",
|
||||
"integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -239,9 +239,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz",
|
||||
"integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==",
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz",
|
||||
"integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -258,9 +258,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz",
|
||||
"integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==",
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz",
|
||||
"integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -277,9 +277,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz",
|
||||
"integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==",
|
||||
"version": "0.35.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz",
|
||||
"integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -298,13 +298,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.3.3"
|
||||
"@img/sharp-libvips-linux-arm": "1.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz",
|
||||
"integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==",
|
||||
"version": "0.35.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz",
|
||||
"integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -323,13 +323,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.3.3"
|
||||
"@img/sharp-libvips-linux-arm64": "1.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-ppc64": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz",
|
||||
"integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==",
|
||||
"version": "0.35.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz",
|
||||
"integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -348,13 +348,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-ppc64": "1.3.3"
|
||||
"@img/sharp-libvips-linux-ppc64": "1.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-riscv64": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz",
|
||||
"integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==",
|
||||
"version": "0.35.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz",
|
||||
"integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -373,13 +373,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-riscv64": "1.3.3"
|
||||
"@img/sharp-libvips-linux-riscv64": "1.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz",
|
||||
"integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==",
|
||||
"version": "0.35.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz",
|
||||
"integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -398,13 +398,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.3.3"
|
||||
"@img/sharp-libvips-linux-s390x": "1.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz",
|
||||
"integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==",
|
||||
"version": "0.35.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz",
|
||||
"integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -423,13 +423,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.3.3"
|
||||
"@img/sharp-libvips-linux-x64": "1.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz",
|
||||
"integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==",
|
||||
"version": "0.35.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz",
|
||||
"integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -448,13 +448,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.3.3"
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz",
|
||||
"integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==",
|
||||
"version": "0.35.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz",
|
||||
"integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -473,13 +473,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.3.3"
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz",
|
||||
"integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==",
|
||||
"version": "0.35.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz",
|
||||
"integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==",
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -493,16 +493,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-webcontainers-wasm32": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz",
|
||||
"integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==",
|
||||
"version": "0.35.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz",
|
||||
"integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@img/sharp-wasm32": "0.35.4"
|
||||
"@img/sharp-wasm32": "0.35.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
@@ -512,9 +512,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz",
|
||||
"integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==",
|
||||
"version": "0.35.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz",
|
||||
"integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -531,9 +531,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz",
|
||||
"integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==",
|
||||
"version": "0.35.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz",
|
||||
"integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -550,9 +550,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz",
|
||||
"integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==",
|
||||
"version": "0.35.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz",
|
||||
"integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -817,9 +817,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz",
|
||||
"integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==",
|
||||
"version": "0.35.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz",
|
||||
"integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@img/colour": "^1.1.0",
|
||||
@@ -833,31 +833,31 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.35.4",
|
||||
"@img/sharp-darwin-x64": "0.35.4",
|
||||
"@img/sharp-freebsd-wasm32": "0.35.4",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.3.3",
|
||||
"@img/sharp-libvips-darwin-x64": "1.3.3",
|
||||
"@img/sharp-libvips-linux-arm": "1.3.3",
|
||||
"@img/sharp-libvips-linux-arm64": "1.3.3",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.3.3",
|
||||
"@img/sharp-libvips-linux-riscv64": "1.3.3",
|
||||
"@img/sharp-libvips-linux-s390x": "1.3.3",
|
||||
"@img/sharp-libvips-linux-x64": "1.3.3",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.3.3",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.3.3",
|
||||
"@img/sharp-linux-arm": "0.35.4",
|
||||
"@img/sharp-linux-arm64": "0.35.4",
|
||||
"@img/sharp-linux-ppc64": "0.35.4",
|
||||
"@img/sharp-linux-riscv64": "0.35.4",
|
||||
"@img/sharp-linux-s390x": "0.35.4",
|
||||
"@img/sharp-linux-x64": "0.35.4",
|
||||
"@img/sharp-linuxmusl-arm64": "0.35.4",
|
||||
"@img/sharp-linuxmusl-x64": "0.35.4",
|
||||
"@img/sharp-webcontainers-wasm32": "0.35.4",
|
||||
"@img/sharp-win32-arm64": "0.35.4",
|
||||
"@img/sharp-win32-ia32": "0.35.4",
|
||||
"@img/sharp-win32-x64": "0.35.4"
|
||||
"@img/sharp-darwin-arm64": "0.35.5",
|
||||
"@img/sharp-darwin-x64": "0.35.5",
|
||||
"@img/sharp-freebsd-wasm32": "0.35.5",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.3.4",
|
||||
"@img/sharp-libvips-darwin-x64": "1.3.4",
|
||||
"@img/sharp-libvips-linux-arm": "1.3.4",
|
||||
"@img/sharp-libvips-linux-arm64": "1.3.4",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.3.4",
|
||||
"@img/sharp-libvips-linux-riscv64": "1.3.4",
|
||||
"@img/sharp-libvips-linux-s390x": "1.3.4",
|
||||
"@img/sharp-libvips-linux-x64": "1.3.4",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.3.4",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.3.4",
|
||||
"@img/sharp-linux-arm": "0.35.5",
|
||||
"@img/sharp-linux-arm64": "0.35.5",
|
||||
"@img/sharp-linux-ppc64": "0.35.5",
|
||||
"@img/sharp-linux-riscv64": "0.35.5",
|
||||
"@img/sharp-linux-s390x": "0.35.5",
|
||||
"@img/sharp-linux-x64": "0.35.5",
|
||||
"@img/sharp-linuxmusl-arm64": "0.35.5",
|
||||
"@img/sharp-linuxmusl-x64": "0.35.5",
|
||||
"@img/sharp-webcontainers-wasm32": "0.35.5",
|
||||
"@img/sharp-win32-arm64": "0.35.5",
|
||||
"@img/sharp-win32-ia32": "0.35.5",
|
||||
"@img/sharp-win32-x64": "0.35.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
"dependencies": {
|
||||
"pixelmatch": "^7.2.0",
|
||||
"puppeteer": "^25.4.0",
|
||||
"sharp": "^0.35.4"
|
||||
"sharp": "^0.35.5"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+3
-3
@@ -5232,9 +5232,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
|
||||
"integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -97,9 +97,20 @@ DatePicker::make('date_of_birth')
|
||||
<AutoScreenshot name="forms/fields/date-time-picker/javascript" alt="JavaScript-based date time picker" version="5.x" />
|
||||
|
||||
<Aside variant="info">
|
||||
The JavaScript date picker does not support full keyboard input in the same way that the native date picker does. If you require full keyboard input, you should use the native date picker.
|
||||
The JavaScript date picker supports keyboard navigation, but does not allow you to type a date directly into the field. Use the native date picker if you need direct date entry.
|
||||
</Aside>
|
||||
|
||||
### Navigating the calendar with the keyboard
|
||||
|
||||
You can open the calendar using `Enter`, `Space`, or an arrow key. Focus starts on the selected date, or the calendar's focused date when the field is empty. Moving focus does not change the field's value until you select a date:
|
||||
|
||||
- Use the left and right arrow keys to move one day, following the calendar's text direction, and the up and down arrow keys to move one week.
|
||||
- Use `Home` and `End` to move to the first and last day of the week, respecting `firstDayOfWeek()`.
|
||||
- Use `Page Up` and `Page Down` to move one month. Hold `Shift` to move one year instead. If the destination month does not contain the same day, focus moves to its last day.
|
||||
- Use `Enter` or `Space` to select the focused date. Unavailable dates can receive focus, but cannot be selected.
|
||||
- Use `Tab` and `Shift + Tab` to move between the calendar and the month, year, and time controls. Leaving the picker closes it without moving focus back.
|
||||
- Use `Escape` to close the picker and return focus to its field.
|
||||
|
||||
### Customizing the display format
|
||||
|
||||
You may customize the display format of the field, separately from the format used when it is saved in your database. For this, use the `displayFormat()` method, which also accepts a string date format, using [PHP date formatting tokens](https://www.php.net/manual/en/datetime.format.php):
|
||||
|
||||
@@ -15,10 +15,21 @@
|
||||
}
|
||||
|
||||
& .fi-fo-date-time-picker-panel {
|
||||
@apply absolute z-20 space-y-3 rounded-lg bg-white p-4 shadow-lg ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10;
|
||||
@apply absolute z-20 rounded-lg bg-white p-4 shadow-lg ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10;
|
||||
|
||||
& .fi-fo-date-time-picker-panel-header {
|
||||
@apply flex items-center justify-between;
|
||||
@apply mb-3 flex items-center justify-between gap-x-3;
|
||||
}
|
||||
}
|
||||
|
||||
& .fi-fo-date-time-picker-month-select,
|
||||
& .fi-fo-date-time-picker-year-input,
|
||||
& .fi-fo-date-time-picker-calendar-day,
|
||||
& .fi-fo-date-time-picker-time-inputs input {
|
||||
@apply rounded-lg outline-none;
|
||||
|
||||
&:focus-visible {
|
||||
@apply outline-primary-600 dark:outline-primary-500 outline-2 outline-solid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +41,12 @@
|
||||
@apply w-16 border-none bg-transparent p-0 text-right text-sm text-gray-950 focus:ring-0 dark:text-white;
|
||||
}
|
||||
|
||||
& .fi-fo-date-time-picker-calendar-description {
|
||||
@apply sr-only;
|
||||
}
|
||||
|
||||
& .fi-fo-date-time-picker-calendar-header {
|
||||
@apply grid grid-cols-7 gap-1;
|
||||
@apply mb-2 grid grid-cols-7 gap-1;
|
||||
|
||||
& .fi-fo-date-time-picker-calendar-header-day {
|
||||
@apply text-center text-xs font-medium text-gray-500 dark:text-gray-400;
|
||||
@@ -39,13 +54,25 @@
|
||||
}
|
||||
|
||||
& .fi-fo-date-time-picker-calendar {
|
||||
@apply grid grid-cols-[repeat(7,minmax(--spacing(7),1fr))] gap-1;
|
||||
@apply grid gap-1 outline-none;
|
||||
|
||||
& .fi-fo-date-time-picker-calendar-week {
|
||||
@apply grid grid-cols-[repeat(7,minmax(--spacing(7),1fr))] gap-1;
|
||||
}
|
||||
|
||||
& .fi-fo-date-time-picker-calendar-day {
|
||||
@apply rounded-full text-center text-sm leading-loose transition duration-75;
|
||||
@apply text-center text-sm leading-loose;
|
||||
|
||||
&.fi-disabled {
|
||||
@apply pointer-events-none opacity-50;
|
||||
@apply opacity-50;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
&:not(.fi-disabled):is(:hover, :focus-visible) {
|
||||
@apply bg-gray-50 dark:bg-white/5;
|
||||
}
|
||||
|
||||
&:not(.fi-disabled) {
|
||||
@@ -53,17 +80,13 @@
|
||||
}
|
||||
|
||||
&.fi-selected {
|
||||
@apply text-primary-600 dark:text-primary-400 bg-gray-50 dark:bg-white/5;
|
||||
@apply text-primary-700 dark:text-primary-400 bg-gray-50 dark:bg-white/5;
|
||||
}
|
||||
|
||||
&.fi-focused:not(.fi-selected):not(.fi-disabled) {
|
||||
@apply bg-gray-100 dark:bg-white/10;
|
||||
}
|
||||
|
||||
&.fi-fo-date-time-picker-calendar-day-today:not(.fi-focused):not(
|
||||
.fi-selected
|
||||
):not(.fi-disabled) {
|
||||
@apply text-primary-600 dark:text-primary-400;
|
||||
&.fi-fo-date-time-picker-calendar-day-today:not(.fi-selected):not(
|
||||
.fi-disabled
|
||||
) {
|
||||
@apply text-primary-700 dark:text-primary-400;
|
||||
}
|
||||
|
||||
&:not(.fi-fo-date-time-picker-calendar-day-today):not(
|
||||
@@ -71,14 +94,22 @@
|
||||
) {
|
||||
@apply text-gray-950 dark:text-white;
|
||||
}
|
||||
|
||||
&.fi-disabled:focus {
|
||||
@apply text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .fi-fo-date-time-picker-calendar + .fi-fo-date-time-picker-time-inputs {
|
||||
@apply mt-3;
|
||||
}
|
||||
|
||||
& .fi-fo-date-time-picker-time-inputs {
|
||||
@apply flex items-center justify-center rtl:flex-row-reverse;
|
||||
@apply flex items-center justify-center gap-x-1 rtl:flex-row-reverse;
|
||||
|
||||
& input {
|
||||
@apply me-1 w-10 border-none bg-transparent p-0 text-center text-sm text-gray-950 focus:ring-0 dark:text-white;
|
||||
@apply w-10 border-none bg-transparent p-0 text-center text-sm text-gray-950 focus:ring-0 dark:text-white;
|
||||
}
|
||||
|
||||
& .fi-fo-date-time-picker-time-input-separator {
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import dayjs from 'dayjs/esm'
|
||||
import {
|
||||
autoUpdate,
|
||||
computePosition,
|
||||
flip,
|
||||
offset,
|
||||
shift,
|
||||
} from '@floating-ui/dom'
|
||||
import advancedFormat from 'dayjs/plugin/advancedFormat'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
||||
import localeData from 'dayjs/plugin/localeData'
|
||||
@@ -23,12 +30,28 @@ export default function dateTimePickerFormComponent({
|
||||
shouldCloseOnDateSelection,
|
||||
state,
|
||||
}) {
|
||||
const calendarLocale = (locales[locale] ?? locales['en']).name
|
||||
|
||||
return {
|
||||
daysInFocusedMonth: [],
|
||||
weeksInFocusedMonth: [],
|
||||
|
||||
displayText: '',
|
||||
|
||||
emptyDaysInFocusedMonth: [],
|
||||
renderedMonth: null,
|
||||
|
||||
isPanelOpen: false,
|
||||
|
||||
isPanelReady: false,
|
||||
|
||||
isDestroyed: false,
|
||||
|
||||
panelGeneration: 0,
|
||||
|
||||
focusGeneration: 0,
|
||||
|
||||
positionGeneration: 0,
|
||||
|
||||
positioningCleanup: null,
|
||||
|
||||
focusedDate: null,
|
||||
|
||||
@@ -52,15 +75,21 @@ export default function dateTimePickerFormComponent({
|
||||
|
||||
dayLabels: [],
|
||||
|
||||
fullDayLabels: [],
|
||||
|
||||
months: [],
|
||||
|
||||
init() {
|
||||
dayjs.locale(locales[locale] ?? locales['en'])
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.isDestroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
const date = this.getDefaultFocusedDate()
|
||||
this.focusedDate ??= hasDate
|
||||
? (date ?? this.getToday())
|
||||
? (date ?? this.getToday()).locale(calendarLocale)
|
||||
: (date ?? dayjs()).tz(dayjs.tz.guess())
|
||||
this.focusedMonth ??= this.focusedDate.month()
|
||||
this.focusedYear ??= this.focusedDate.year()
|
||||
@@ -84,9 +113,11 @@ export default function dateTimePickerFormComponent({
|
||||
this.setDayLabels()
|
||||
|
||||
if (isAutofocused) {
|
||||
this.$nextTick(() =>
|
||||
this.togglePanelVisibility(this.$refs.button),
|
||||
)
|
||||
this.$nextTick(() => {
|
||||
if (!this.isDestroyed) {
|
||||
this.togglePanelVisibility()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.$watch('focusedMonth', () => {
|
||||
@@ -124,6 +155,9 @@ export default function dateTimePickerFormComponent({
|
||||
})
|
||||
|
||||
this.$watch('focusedDate', () => {
|
||||
const shouldFocusCalendar =
|
||||
hasDate &&
|
||||
this.$refs.calendar?.contains(document.activeElement)
|
||||
let month = this.focusedDate.month()
|
||||
let year = this.focusedDate.year()
|
||||
|
||||
@@ -136,6 +170,10 @@ export default function dateTimePickerFormComponent({
|
||||
}
|
||||
|
||||
this.setupDaysGrid()
|
||||
|
||||
if (shouldFocusCalendar) {
|
||||
this.focusCalendarDate()
|
||||
}
|
||||
})
|
||||
|
||||
this.$watch('hour', () => {
|
||||
@@ -362,8 +400,171 @@ export default function dateTimePickerFormComponent({
|
||||
this.focusedDate = this.focusedDate.add(1, 'week')
|
||||
},
|
||||
|
||||
handleTriggerKeydown(event) {
|
||||
if (
|
||||
event.altKey ||
|
||||
event.ctrlKey ||
|
||||
event.metaKey ||
|
||||
![
|
||||
'Enter',
|
||||
' ',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'ArrowUp',
|
||||
'ArrowDown',
|
||||
].includes(event.key)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
if (this.isOpen()) {
|
||||
this.focusCalendarDate()
|
||||
} else {
|
||||
this.togglePanelVisibility()
|
||||
}
|
||||
},
|
||||
|
||||
handleFocusOut(event) {
|
||||
const generation = this.panelGeneration
|
||||
|
||||
if (
|
||||
event.relatedTarget &&
|
||||
!this.$refs.calendar?.contains(event.relatedTarget)
|
||||
) {
|
||||
this.focusGeneration++
|
||||
}
|
||||
|
||||
const focusGeneration = this.focusGeneration
|
||||
|
||||
this.$nextTick(() =>
|
||||
requestAnimationFrame(() => {
|
||||
if (
|
||||
!this.isDestroyed &&
|
||||
generation === this.panelGeneration &&
|
||||
focusGeneration === this.focusGeneration &&
|
||||
this.isOpen() &&
|
||||
!this.$el.contains(document.activeElement)
|
||||
) {
|
||||
this.closePanel()
|
||||
}
|
||||
}),
|
||||
)
|
||||
},
|
||||
|
||||
handleCalendarKeydown(event) {
|
||||
if (event.altKey || event.ctrlKey || event.metaKey) {
|
||||
return
|
||||
}
|
||||
|
||||
const isRtl =
|
||||
getComputedStyle(this.$refs.calendar).direction === 'rtl'
|
||||
const weekDay = (this.focusedDate.day() - firstDayOfWeek + 7) % 7
|
||||
let focusedDate = this.focusedDate
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowLeft':
|
||||
focusedDate = focusedDate.add(isRtl ? 1 : -1, 'day')
|
||||
break
|
||||
case 'ArrowRight':
|
||||
focusedDate = focusedDate.add(isRtl ? -1 : 1, 'day')
|
||||
break
|
||||
case 'ArrowUp':
|
||||
focusedDate = focusedDate.subtract(7, 'day')
|
||||
break
|
||||
case 'ArrowDown':
|
||||
focusedDate = focusedDate.add(7, 'day')
|
||||
break
|
||||
case 'Home':
|
||||
focusedDate = focusedDate.subtract(weekDay, 'day')
|
||||
break
|
||||
case 'End':
|
||||
focusedDate = focusedDate.add(6 - weekDay, 'day')
|
||||
break
|
||||
case 'PageUp':
|
||||
case 'PageDown':
|
||||
focusedDate = focusedDate.add(
|
||||
event.key === 'PageUp' ? -1 : 1,
|
||||
event.shiftKey ? 'year' : 'month',
|
||||
)
|
||||
break
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
this.selectDate()
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
return
|
||||
case 'Tab':
|
||||
this.$refs.calendar
|
||||
.querySelector(
|
||||
`[data-date="${this.focusedDate.format('YYYY-MM-DD')}"]`,
|
||||
)
|
||||
?.focus({ preventScroll: true })
|
||||
|
||||
return
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
if (this.renderedMonth !== focusedDate.format('YYYY-MM')) {
|
||||
// Keep keyboard events in the grid while Alpine replaces the focused row.
|
||||
this.$refs.calendar.focus({ preventScroll: true })
|
||||
}
|
||||
|
||||
this.focusedDate = focusedDate
|
||||
this.focusCalendarDate()
|
||||
},
|
||||
|
||||
focusCalendarDate(generation = ++this.focusGeneration) {
|
||||
const focusDate = () => {
|
||||
if (
|
||||
this.isDestroyed ||
|
||||
!this.$el.isConnected ||
|
||||
!this.isOpen() ||
|
||||
generation !== this.focusGeneration
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$refs.calendar
|
||||
?.querySelector(
|
||||
`[data-date="${this.focusedDate.format('YYYY-MM-DD')}"]`,
|
||||
)
|
||||
?.focus({ preventScroll: true })
|
||||
}
|
||||
|
||||
this.$nextTick(() =>
|
||||
this.isPanelReady
|
||||
? focusDate()
|
||||
: requestAnimationFrame(focusDate),
|
||||
)
|
||||
},
|
||||
|
||||
getCalendarLabel() {
|
||||
return (
|
||||
this.focusedDate?.locale(calendarLocale).format('MMMM YYYY') ??
|
||||
''
|
||||
)
|
||||
},
|
||||
|
||||
getDayLabel(day) {
|
||||
return this.focusedDate
|
||||
.date(day)
|
||||
.locale(calendarLocale)
|
||||
.format('dddd, D MMMM YYYY')
|
||||
},
|
||||
|
||||
getDayLabels() {
|
||||
const labels = dayjs.weekdaysShort()
|
||||
const labels = dayjs()
|
||||
.locale(calendarLocale)
|
||||
.localeData()
|
||||
.weekdaysShort()
|
||||
|
||||
if (firstDayOfWeek === 0) {
|
||||
return labels
|
||||
@@ -417,6 +618,10 @@ export default function dateTimePickerFormComponent({
|
||||
|
||||
let date = dayjs.utc(this.state)
|
||||
|
||||
if (hasDate) {
|
||||
date = date.locale(calendarLocale)
|
||||
}
|
||||
|
||||
if (!date.isValid()) {
|
||||
return null
|
||||
}
|
||||
@@ -431,6 +636,10 @@ export default function dateTimePickerFormComponent({
|
||||
|
||||
let defaultFocusedDate = dayjs.utc(this.defaultFocusedDate)
|
||||
|
||||
if (hasDate) {
|
||||
defaultFocusedDate = defaultFocusedDate.locale(calendarLocale)
|
||||
}
|
||||
|
||||
if (!defaultFocusedDate.isValid()) {
|
||||
return null
|
||||
}
|
||||
@@ -441,11 +650,21 @@ export default function dateTimePickerFormComponent({
|
||||
getToday() {
|
||||
// Use UTC only as a neutral calendar, preserving the browser's local date.
|
||||
return hasDate
|
||||
? dayjs.utc(dayjs().format('YYYY-MM-DD'))
|
||||
? dayjs.utc(dayjs().format('YYYY-MM-DD')).locale(calendarLocale)
|
||||
: dayjs().tz(dayjs.tz.guess())
|
||||
},
|
||||
|
||||
togglePanelVisibility() {
|
||||
togglePanelVisibility(shouldFocusCalendar = true) {
|
||||
if (this.isDestroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
if (hasDate && this.isOpen()) {
|
||||
this.closePanel(true)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.isOpen()) {
|
||||
this.focusedDate =
|
||||
this.getSelectedDate() ??
|
||||
@@ -456,7 +675,90 @@ export default function dateTimePickerFormComponent({
|
||||
this.setupDaysGrid()
|
||||
}
|
||||
|
||||
this.$refs.panel.toggle(this.$refs.button)
|
||||
if (!hasDate) {
|
||||
this.$refs.panel.toggle(this.$refs.button)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.isPanelOpen = true
|
||||
const generation = ++this.panelGeneration
|
||||
const focusGeneration = ++this.focusGeneration
|
||||
|
||||
this.$nextTick(() =>
|
||||
requestAnimationFrame(() => {
|
||||
if (
|
||||
this.isDestroyed ||
|
||||
!this.$el.isConnected ||
|
||||
!this.isOpen() ||
|
||||
generation !== this.panelGeneration
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isPanelReady = true
|
||||
this.positioningCleanup = autoUpdate(
|
||||
this.$refs.button,
|
||||
this.$refs.panel,
|
||||
() => {
|
||||
const positionGeneration = ++this.positionGeneration
|
||||
|
||||
computePosition(
|
||||
this.$refs.button,
|
||||
this.$refs.panel,
|
||||
{
|
||||
placement: 'bottom-start',
|
||||
middleware: [offset(8), flip(), shift()],
|
||||
},
|
||||
).then(({ x, y }) => {
|
||||
if (
|
||||
this.isDestroyed ||
|
||||
!this.$el.isConnected ||
|
||||
!this.isOpen() ||
|
||||
generation !== this.panelGeneration ||
|
||||
positionGeneration !==
|
||||
this.positionGeneration
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
Object.assign(this.$refs.panel.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
if (
|
||||
shouldFocusCalendar &&
|
||||
focusGeneration === this.focusGeneration
|
||||
) {
|
||||
this.focusCalendarDate(focusGeneration)
|
||||
}
|
||||
}),
|
||||
)
|
||||
},
|
||||
|
||||
closePanel(shouldRestoreFocus = false) {
|
||||
if (!hasDate) {
|
||||
this.$refs.panel.close()
|
||||
} else {
|
||||
this.isPanelOpen = false
|
||||
this.isPanelReady = false
|
||||
this.panelGeneration++
|
||||
this.focusGeneration++
|
||||
this.positioningCleanup?.()
|
||||
this.positioningCleanup = null
|
||||
}
|
||||
|
||||
if (
|
||||
shouldRestoreFocus &&
|
||||
!this.isDestroyed &&
|
||||
this.$refs.button.isConnected
|
||||
) {
|
||||
this.$refs.button.focus({ preventScroll: true })
|
||||
}
|
||||
},
|
||||
|
||||
selectDate(day = null) {
|
||||
@@ -466,10 +768,18 @@ export default function dateTimePickerFormComponent({
|
||||
|
||||
this.focusedDate ??= this.getToday()
|
||||
|
||||
if (hasDate && this.dateIsDisabled(this.focusedDate)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState(this.focusedDate)
|
||||
|
||||
if (shouldCloseOnDateSelection) {
|
||||
this.togglePanelVisibility()
|
||||
if (hasDate) {
|
||||
this.closePanel(true)
|
||||
} else {
|
||||
this.togglePanelVisibility()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -502,32 +812,52 @@ export default function dateTimePickerFormComponent({
|
||||
},
|
||||
|
||||
setMonths() {
|
||||
this.months = dayjs.months()
|
||||
this.months = dayjs().locale(calendarLocale).localeData().months()
|
||||
},
|
||||
|
||||
setDayLabels() {
|
||||
this.dayLabels = this.getDayLabels()
|
||||
const labels = dayjs()
|
||||
.locale(calendarLocale)
|
||||
.localeData()
|
||||
.weekdays()
|
||||
this.fullDayLabels = [
|
||||
...labels.slice(firstDayOfWeek % 7),
|
||||
...labels.slice(0, firstDayOfWeek % 7),
|
||||
]
|
||||
},
|
||||
|
||||
setupDaysGrid() {
|
||||
this.focusedDate ??= this.getToday()
|
||||
|
||||
this.emptyDaysInFocusedMonth = Array.from(
|
||||
{
|
||||
length: this.focusedDate.date(8 - firstDayOfWeek).day(),
|
||||
},
|
||||
(_, i) => i + 1,
|
||||
)
|
||||
const month = this.focusedDate.format('YYYY-MM')
|
||||
|
||||
this.daysInFocusedMonth = Array.from(
|
||||
{
|
||||
length: this.focusedDate.daysInMonth(),
|
||||
},
|
||||
(_, i) => i + 1,
|
||||
if (this.renderedMonth === month) {
|
||||
return
|
||||
}
|
||||
|
||||
this.renderedMonth = month
|
||||
const emptyDays =
|
||||
(this.focusedDate.startOf('month').day() - firstDayOfWeek + 7) %
|
||||
7
|
||||
const daysInMonth = this.focusedDate.daysInMonth()
|
||||
|
||||
this.weeksInFocusedMonth = Array.from(
|
||||
{ length: Math.ceil((emptyDays + daysInMonth) / 7) },
|
||||
(_, week) =>
|
||||
Array.from({ length: 7 }, (_, column) => {
|
||||
const day = week * 7 + column - emptyDays + 1
|
||||
|
||||
return day > 0 && day <= daysInMonth ? day : null
|
||||
}),
|
||||
)
|
||||
},
|
||||
|
||||
setFocusedDay(day) {
|
||||
if (this.focusedDate?.date() === day) {
|
||||
return
|
||||
}
|
||||
|
||||
this.focusedDate = (this.focusedDate ?? this.getToday()).date(day)
|
||||
},
|
||||
|
||||
@@ -557,25 +887,54 @@ export default function dateTimePickerFormComponent({
|
||||
},
|
||||
|
||||
timeInputInvalid(event) {
|
||||
const el = event.target
|
||||
const input = event.target
|
||||
|
||||
if (!this.isOpen()) {
|
||||
event.preventDefault()
|
||||
this.togglePanelVisibility()
|
||||
this.togglePanelVisibility(false)
|
||||
}
|
||||
|
||||
if (hasDate && !this.isPanelReady) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
if (!this.hasValidationMessage) {
|
||||
this.hasValidationMessage = true
|
||||
const generation = this.panelGeneration
|
||||
|
||||
this.$nextTick(() => {
|
||||
el.reportValidity()
|
||||
const reportValidity = () => {
|
||||
if (
|
||||
!hasDate ||
|
||||
(!this.isDestroyed &&
|
||||
input.isConnected &&
|
||||
this.isOpen() &&
|
||||
generation === this.panelGeneration)
|
||||
) {
|
||||
input.reportValidity()
|
||||
}
|
||||
this.hasValidationMessage = false
|
||||
})
|
||||
}
|
||||
|
||||
this.$nextTick(() =>
|
||||
hasDate
|
||||
? requestAnimationFrame(reportValidity)
|
||||
: reportValidity(),
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
isOpen() {
|
||||
return this.$refs.panel?.style.display === 'block'
|
||||
return hasDate
|
||||
? this.isPanelOpen
|
||||
: this.$refs.panel?.style.display === 'block'
|
||||
},
|
||||
|
||||
destroy() {
|
||||
this.isDestroyed = true
|
||||
|
||||
if (hasDate) {
|
||||
this.closePanel()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,9 +213,17 @@ class DateTimePicker extends Field implements Contracts\HasAffixes, HasEmbeddedV
|
||||
})"
|
||||
wire:ignore
|
||||
wire:key="<?= e($livewireKey) ?>.<?= substr(md5(serialize([$disabledDates, $isDisabled, $isReadOnly, $maxDate, $minDate, $hasDate, $hasTime, $hasSeconds])), 0, 64) ?>"
|
||||
x-on:dropdown-escape="if (isOpen()) { $refs.panel.close(); $refs.button.focus(); $event.preventDefault() }"
|
||||
x-on:focusout="if (isOpen() && ! $el.contains($event.relatedTarget)) $refs.panel.close()"
|
||||
x-on:keydown.esc="isOpen() && $event.stopPropagation()"
|
||||
<?php if ($hasDate) { ?>
|
||||
x-on:dropdown-escape="if (isOpen()) { closePanel(true); $event.preventDefault() }"
|
||||
x-on:focusout="handleFocusOut($event)"
|
||||
x-on:keydown.escape="if (isOpen()) { $event.preventDefault(); $event.stopPropagation(); closePanel(true) }"
|
||||
x-on:click.outside="isOpen() && closePanel()"
|
||||
x-on:livewire:navigating.window="closePanel()"
|
||||
<?php } else { ?>
|
||||
x-on:dropdown-escape="if (isOpen()) { $refs.panel.close(); $refs.button.focus(); $event.preventDefault() }"
|
||||
x-on:focusout="if (isOpen() && ! $el.contains($event.relatedTarget)) $refs.panel.close()"
|
||||
x-on:keydown.esc="isOpen() && $event.stopPropagation()"
|
||||
<?php } ?>
|
||||
<?= $this->getExtraAlpineAttributeBag()->toHtml() ?>
|
||||
>
|
||||
<input x-ref="maxDate" type="hidden" value="<?= e($maxDate) ?>" />
|
||||
@@ -224,13 +232,20 @@ class DateTimePicker extends Field implements Contracts\HasAffixes, HasEmbeddedV
|
||||
|
||||
<input
|
||||
x-ref="button"
|
||||
x-on:keydown.enter.prevent.stop="if (<?= Js::from(! ($isDisabled || $isReadOnly)) ?>) { isOpen() ? selectDate() : togglePanelVisibility() }"
|
||||
<?php if ((! $hasDate) || $isDisabled || $isReadOnly) { ?>
|
||||
x-on:keydown.enter.prevent.stop="if (<?= Js::from(! ($isDisabled || $isReadOnly)) ?>) { isOpen() ? selectDate() : togglePanelVisibility() }"
|
||||
<?php } ?>
|
||||
<?php if (! ($isDisabled || $isReadOnly)) { ?>
|
||||
x-on:click="togglePanelVisibility()"
|
||||
x-on:keydown.arrow-left.prevent.stop="focusPreviousDay()"
|
||||
x-on:keydown.arrow-right.prevent.stop="focusNextDay()"
|
||||
x-on:keydown.arrow-up.prevent.stop="focusPreviousWeek()"
|
||||
x-on:keydown.arrow-down.prevent.stop="focusNextWeek()"
|
||||
<?php if ($hasDate) { ?>
|
||||
x-on:keydown="handleTriggerKeydown($event)"
|
||||
x-bind:aria-expanded="isPanelOpen.toString()"
|
||||
<?php } else { ?>
|
||||
x-on:keydown.arrow-left.prevent.stop="focusPreviousDay()"
|
||||
x-on:keydown.arrow-right.prevent.stop="focusNextDay()"
|
||||
x-on:keydown.arrow-up.prevent.stop="focusPreviousWeek()"
|
||||
x-on:keydown.arrow-down.prevent.stop="focusNextWeek()"
|
||||
<?php } ?>
|
||||
x-on:keydown.backspace.prevent.stop="clearState()"
|
||||
x-on:keydown.clear.prevent.stop="clearState()"
|
||||
x-on:keydown.delete.prevent.stop="clearState()"
|
||||
@@ -260,7 +275,11 @@ class DateTimePicker extends Field implements Contracts\HasAffixes, HasEmbeddedV
|
||||
role="dialog"
|
||||
aria-label="<?= e($labelText) ?>"
|
||||
x-cloak
|
||||
x-float.placement.bottom-start.offset.flip.shift="{ offset: 8 }"
|
||||
<?php if ($hasDate) { ?>
|
||||
x-show="isPanelOpen"
|
||||
<?php } else { ?>
|
||||
x-float.placement.bottom-start.offset.flip.shift="{ offset: 8 }"
|
||||
<?php } ?>
|
||||
wire:ignore
|
||||
wire:key="<?= e($livewireKey) ?>.panel"
|
||||
class="fi-fo-date-time-picker-panel"
|
||||
@@ -275,32 +294,38 @@ class DateTimePicker extends Field implements Contracts\HasAffixes, HasEmbeddedV
|
||||
<input type="number" inputmode="numeric" aria-label="<?= e(__('filament-forms::components.date_time_picker.year_input.label')) ?>" x-model.debounce="focusedYear" class="fi-fo-date-time-picker-year-input" />
|
||||
</div>
|
||||
|
||||
<div class="fi-fo-date-time-picker-calendar-header">
|
||||
<template x-for="(day, index) in dayLabels" x-bind:key="index">
|
||||
<div x-text="day" class="fi-fo-date-time-picker-calendar-header-day"></div>
|
||||
</template>
|
||||
</div>
|
||||
<div aria-live="polite" aria-atomic="true" x-text="getCalendarLabel()" class="fi-fo-date-time-picker-calendar-description"></div>
|
||||
|
||||
<div role="grid" class="fi-fo-date-time-picker-calendar">
|
||||
<template x-for="day in emptyDaysInFocusedMonth" x-bind:key="day">
|
||||
<div></div>
|
||||
</template>
|
||||
<template x-for="day in daysInFocusedMonth" x-bind:key="day">
|
||||
<div
|
||||
x-text="day"
|
||||
x-on:click="dayIsDisabled(day) || selectDate(day)"
|
||||
x-on:mousedown.prevent
|
||||
x-on:mouseenter="setFocusedDay(day)"
|
||||
role="option"
|
||||
x-bind:aria-selected="focusedDate.date() === day"
|
||||
x-bind:class="{
|
||||
'fi-fo-date-time-picker-calendar-day-today': dayIsToday(day),
|
||||
'fi-focused': focusedDate.date() === day,
|
||||
'fi-selected': dayIsSelected(day),
|
||||
'fi-disabled': dayIsDisabled(day),
|
||||
}"
|
||||
class="fi-fo-date-time-picker-calendar-day"
|
||||
></div>
|
||||
<div x-ref="calendar" role="grid" tabindex="-1" x-bind:aria-label="getCalendarLabel()" x-on:keydown="handleCalendarKeydown($event)" class="fi-fo-date-time-picker-calendar">
|
||||
<div role="row" class="fi-fo-date-time-picker-calendar-header">
|
||||
<template x-for="(day, index) in dayLabels" x-bind:key="index">
|
||||
<div role="columnheader" x-bind:aria-label="fullDayLabels[index]" x-text="day" class="fi-fo-date-time-picker-calendar-header-day"></div>
|
||||
</template>
|
||||
</div>
|
||||
<template x-for="(week, weekIndex) in weeksInFocusedMonth" x-bind:key="weekIndex">
|
||||
<div role="row" class="fi-fo-date-time-picker-calendar-week">
|
||||
<template x-for="(day, dayIndex) in week" x-bind:key="day ?? ('empty-' + dayIndex)">
|
||||
<div
|
||||
role="gridcell"
|
||||
x-text="day"
|
||||
x-bind:data-date="day ? focusedDate.date(day).format('YYYY-MM-DD') : null"
|
||||
x-bind:aria-label="day ? getDayLabel(day) : null"
|
||||
x-bind:aria-selected="day && dayIsSelected(day) ? 'true' : 'false'"
|
||||
x-bind:aria-current="day && dayIsToday(day) ? 'date' : null"
|
||||
x-bind:aria-disabled="day && dayIsDisabled(day) ? 'true' : 'false'"
|
||||
x-bind:tabindex="day ? (focusedDate.date() === day ? 0 : -1) : null"
|
||||
x-on:focus="day && setFocusedDay(day)"
|
||||
x-on:click="if (day) { $el.focus(); selectDate(day) }"
|
||||
x-bind:class="{
|
||||
'fi-fo-date-time-picker-calendar-day': day,
|
||||
'fi-fo-date-time-picker-calendar-day-today': day && dayIsToday(day),
|
||||
'fi-focused': day && focusedDate.date() === day,
|
||||
'fi-selected': day && dayIsSelected(day),
|
||||
'fi-disabled': day && dayIsDisabled(day),
|
||||
}"
|
||||
></div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -28,6 +28,10 @@ export default (Alpine) => {
|
||||
|
||||
unsubscribeLivewireHook: null,
|
||||
|
||||
transitionEffect: null,
|
||||
|
||||
isDestroyed: false,
|
||||
|
||||
init() {
|
||||
this.computedStyle = window.getComputedStyle(this.$el)
|
||||
|
||||
@@ -186,7 +190,13 @@ export default (Alpine) => {
|
||||
},
|
||||
)
|
||||
|
||||
Alpine.effect(() => toggle(this.isShown))
|
||||
this.transitionEffect = Alpine.effect(() => {
|
||||
if (this.isDestroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
toggle(this.isShown)
|
||||
})
|
||||
},
|
||||
|
||||
configureAnimations() {
|
||||
@@ -208,17 +218,25 @@ export default (Alpine) => {
|
||||
// Calling `el.getBoundingClientRect()` from outside `requestAnimationFrame()` can
|
||||
// occasionally cause the page to scroll to the top.
|
||||
requestAnimationFrame(() => {
|
||||
if (this.isDestroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
const getTop = () =>
|
||||
this.$el.getBoundingClientRect().top
|
||||
const oldTop = getTop()
|
||||
|
||||
onSuccess(({ onRender }) => {
|
||||
if (this.isDestroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
// `onRender` runs once the DOM has been morphed, inside a
|
||||
// `requestAnimationFrame()` before the browser paints, so the
|
||||
// new position can be measured and the animation started
|
||||
// without the notification flashing in its final position.
|
||||
onRender(() => {
|
||||
if (!this.isShown) {
|
||||
if (this.isDestroyed || !this.isShown) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -304,12 +322,18 @@ export default (Alpine) => {
|
||||
return
|
||||
}
|
||||
|
||||
const livewireRoot = this.$el.closest('[wire\\:id]')
|
||||
|
||||
this.isShown = false
|
||||
|
||||
this.closeTimeout = setTimeout(
|
||||
dispatchClosedEvent,
|
||||
this.transitionDuration,
|
||||
)
|
||||
this.closeTimeout = setTimeout(() => {
|
||||
// Do not deliver an old dismissal to a new host after navigation.
|
||||
if (livewireRoot && !livewireRoot.isConnected) {
|
||||
return
|
||||
}
|
||||
|
||||
dispatchClosedEvent()
|
||||
}, this.transitionDuration)
|
||||
},
|
||||
|
||||
dismiss() {
|
||||
@@ -350,12 +374,14 @@ export default (Alpine) => {
|
||||
},
|
||||
|
||||
destroy() {
|
||||
clearTimeout(this.closeTimeout)
|
||||
this.isDestroyed = true
|
||||
// Keep the pending `notificationClosed` event so a dismissal is not
|
||||
// lost if Livewire removes this notification before its transition ends.
|
||||
clearTimeout(this.durationTimeout)
|
||||
this.closeTimeout = null
|
||||
this.durationTimeout = null
|
||||
this.unsubscribeLivewireHook?.()
|
||||
this.unsubscribeLivewireHook = null
|
||||
Alpine.release(this.transitionEffect)
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -43,7 +43,7 @@
|
||||
@apply flex flex-col items-center;
|
||||
|
||||
& .fi-logo {
|
||||
@apply mb-4;
|
||||
@apply mb-4 text-center text-balance;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,10 @@
|
||||
|
||||
.fi-tenant-menu-item {
|
||||
@apply min-w-0;
|
||||
|
||||
& .fi-dropdown-list-item-image {
|
||||
@apply rounded-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.fi-sidebar {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -49,6 +49,8 @@ const releaseScrollLock = () => {
|
||||
export default ({ id, isScrollLocked = true, shouldRestoreFocus = true }) => ({
|
||||
isOpen: false,
|
||||
|
||||
isDestroyed: false,
|
||||
|
||||
isWindowVisible: false,
|
||||
|
||||
isTrapActive: false,
|
||||
@@ -67,6 +69,10 @@ export default ({ id, isScrollLocked = true, shouldRestoreFocus = true }) => ({
|
||||
|
||||
init() {
|
||||
this.$nextTick(() => {
|
||||
if (this.isDestroyed || !this.$el.isConnected) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isWindowVisible = this.isOpen
|
||||
|
||||
this.setUpTextSelectionClosePrevention()
|
||||
@@ -197,6 +203,10 @@ export default ({ id, isScrollLocked = true, shouldRestoreFocus = true }) => ({
|
||||
|
||||
this.restorePreviouslyFocusedElement()
|
||||
|
||||
if (!this.$el.isConnected) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$dispatch('modal-closed', { id })
|
||||
},
|
||||
|
||||
@@ -206,6 +216,10 @@ export default ({ id, isScrollLocked = true, shouldRestoreFocus = true }) => ({
|
||||
|
||||
open() {
|
||||
this.$nextTick(() => {
|
||||
if (this.isDestroyed || !this.$el.isConnected) {
|
||||
return
|
||||
}
|
||||
|
||||
if (shouldRestoreFocus) {
|
||||
this.previouslyFocusedElement = document.activeElement
|
||||
}
|
||||
@@ -230,6 +244,8 @@ export default ({ id, isScrollLocked = true, shouldRestoreFocus = true }) => ({
|
||||
},
|
||||
|
||||
destroy() {
|
||||
this.isDestroyed = true
|
||||
|
||||
// Release in case the modal is removed while still holding the lock.
|
||||
this.releaseScrollLock()
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -25,6 +25,11 @@ Chart.defaults.plugins.legend.position = 'bottom'
|
||||
|
||||
export default function chart({ cachedData, options, type }) {
|
||||
return {
|
||||
isDestroyed: false,
|
||||
chartDataEventTarget: null,
|
||||
chartDataListener: null,
|
||||
themeEffect: null,
|
||||
|
||||
userBackgroundColor: options?.backgroundColor,
|
||||
userBorderColor: options?.borderColor,
|
||||
userTextColor: options?.color,
|
||||
@@ -50,11 +55,19 @@ export default function chart({ cachedData, options, type }) {
|
||||
userPointRadius: options?.pointRadius,
|
||||
|
||||
init() {
|
||||
this.$wire.$on('updateChartData', ({ data }) =>
|
||||
this.updateChartData(data),
|
||||
this.chartDataEventTarget = this.$wire.$el
|
||||
this.chartDataListener = ({ detail: { data } }) =>
|
||||
this.updateChartData(data)
|
||||
this.chartDataEventTarget.addEventListener(
|
||||
'updateChartData',
|
||||
this.chartDataListener,
|
||||
)
|
||||
|
||||
Alpine.effect(() => {
|
||||
this.themeEffect = Alpine.effect(() => {
|
||||
if (this.isDestroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
Alpine.store('theme')
|
||||
|
||||
this.$nextTick(() => this.updateChartTheme())
|
||||
@@ -80,6 +93,10 @@ export default function chart({ cachedData, options, type }) {
|
||||
// exits early on that first run; otherwise the effect would tear down and
|
||||
// recreate the chart on every mount.
|
||||
this.$nextTick(() => {
|
||||
if (this.isDestroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
this.initChart()
|
||||
|
||||
this.resizeObserver = new ResizeObserver(() =>
|
||||
@@ -298,7 +315,7 @@ export default function chart({ cachedData, options, type }) {
|
||||
},
|
||||
|
||||
getChart() {
|
||||
if (!this.$refs.canvas) {
|
||||
if (this.isDestroyed || !this.$refs.canvas) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -421,6 +438,14 @@ export default function chart({ cachedData, options, type }) {
|
||||
},
|
||||
|
||||
destroy() {
|
||||
const chart = this.getChart()
|
||||
this.isDestroyed = true
|
||||
|
||||
this.chartDataEventTarget.removeEventListener(
|
||||
'updateChartData',
|
||||
this.chartDataListener,
|
||||
)
|
||||
Alpine.release(this.themeEffect)
|
||||
this.resizeObserver?.disconnect()
|
||||
this.dprChangeHandler &&
|
||||
window.removeEventListener('resize', this.dprChangeHandler)
|
||||
@@ -428,7 +453,7 @@ export default function chart({ cachedData, options, type }) {
|
||||
'change',
|
||||
this.systemThemeListener,
|
||||
)
|
||||
this.getChart()?.destroy()
|
||||
chart?.destroy()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,17 +20,34 @@ Chart.register(
|
||||
)
|
||||
|
||||
export default function statsOverviewStatChart({ key, labels, values }) {
|
||||
let isDestroyed = false
|
||||
|
||||
return {
|
||||
key,
|
||||
|
||||
chartDataUpdateEventTarget: null,
|
||||
|
||||
chartDataUpdateListener: null,
|
||||
|
||||
themeEffect: null,
|
||||
|
||||
init() {
|
||||
this.$wire.$on('updateStatsOverviewChartData', (event) => {
|
||||
if (event.key === this.key) {
|
||||
this.updateChartData(event.data)
|
||||
this.chartDataUpdateEventTarget = this.$wire.$el
|
||||
this.chartDataUpdateListener = (event) => {
|
||||
if (event.detail.key === this.key) {
|
||||
this.updateChartData(event.detail.data)
|
||||
}
|
||||
})
|
||||
}
|
||||
this.chartDataUpdateEventTarget.addEventListener(
|
||||
'updateStatsOverviewChartData',
|
||||
this.chartDataUpdateListener,
|
||||
)
|
||||
|
||||
this.themeEffect = Alpine.effect(() => {
|
||||
if (isDestroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
Alpine.effect(() => {
|
||||
Alpine.store('theme')
|
||||
|
||||
this.$nextTick(() => this.updateChartTheme())
|
||||
@@ -40,7 +57,7 @@ export default function statsOverviewStatChart({ key, labels, values }) {
|
||||
'(prefers-color-scheme: dark)',
|
||||
)
|
||||
this.systemThemeListener = () => {
|
||||
if (Alpine.store('theme') !== 'system') {
|
||||
if (isDestroyed || Alpine.store('theme') !== 'system') {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -58,6 +75,10 @@ export default function statsOverviewStatChart({ key, labels, values }) {
|
||||
},
|
||||
|
||||
initChart() {
|
||||
if (isDestroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
!this.$refs.canvas ||
|
||||
!this.$refs.backgroundColorElement ||
|
||||
@@ -120,6 +141,10 @@ export default function statsOverviewStatChart({ key, labels, values }) {
|
||||
},
|
||||
|
||||
updateChartData(newValues) {
|
||||
if (isDestroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
const chart = this.getChart()
|
||||
|
||||
if (!chart) {
|
||||
@@ -132,6 +157,10 @@ export default function statsOverviewStatChart({ key, labels, values }) {
|
||||
},
|
||||
|
||||
updateChartTheme() {
|
||||
if (isDestroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
const chart = this.getChart()
|
||||
|
||||
if (!chart) {
|
||||
@@ -187,6 +216,12 @@ export default function statsOverviewStatChart({ key, labels, values }) {
|
||||
},
|
||||
|
||||
destroy() {
|
||||
isDestroyed = true
|
||||
Alpine.release(this.themeEffect)
|
||||
this.chartDataUpdateEventTarget?.removeEventListener(
|
||||
'updateStatsOverviewChartData',
|
||||
this.chartDataUpdateListener,
|
||||
)
|
||||
this.systemThemeMediaQuery?.removeEventListener(
|
||||
'change',
|
||||
this.systemThemeListener,
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
<x-filament-panels::page>
|
||||
{{ $this->form }}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
wire:click="simulateCreateAnother"
|
||||
data-testid="simulate-create-another"
|
||||
>
|
||||
Create & create another
|
||||
</button>
|
||||
</x-filament-panels::page>
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
<x-filament-panels::page>
|
||||
{{ $this->form }}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
wire:click="simulateCreateAnother"
|
||||
data-testid="simulate-create-another"
|
||||
>
|
||||
Create & create another
|
||||
</button>
|
||||
</x-filament-panels::page>
|
||||
@@ -12,6 +12,13 @@
|
||||
>
|
||||
Reload saved time
|
||||
</x-filament::button>
|
||||
<x-filament::button
|
||||
type="button"
|
||||
wire:click="$toggle('hasDisabledDates')"
|
||||
data-testid="replace-calendar"
|
||||
>
|
||||
Toggle unavailable date
|
||||
</x-filament::button>
|
||||
</form>
|
||||
|
||||
<output data-testid="saved-timed">{{ $saved['field'] ?? '' }}</output>
|
||||
|
||||
@@ -5,14 +5,6 @@
|
||||
|
||||
<x-filament-panels::page>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="sendTimedNotification(1600)"
|
||||
data-testid="send-timed-notification"
|
||||
>
|
||||
Send timed notification
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
wire:click="sendTimedNotification"
|
||||
|
||||
@@ -159,7 +159,7 @@ describe('browser interactions', function (): void {
|
||||
$assertValidationBehavior($browser);
|
||||
});
|
||||
|
||||
it('locks page scroll and restores focus and scroll position after closing a standalone modal', function (): void {
|
||||
it('restores standalone modal state on close', function (): void {
|
||||
retry(10, function (): void {
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
@@ -183,7 +183,13 @@ describe('browser interactions', function (): void {
|
||||
->assertPresent('[data-testid="standalone-trigger"]:focus')
|
||||
->assertScript('window.scrollY === window.modalTestScrollY', true)
|
||||
->assertScript('document.documentElement.style.overflow', '')
|
||||
->assertNoSmoke();
|
||||
->assertNoSmoke()
|
||||
->assertNoAccessibilityIssues();
|
||||
|
||||
// `inDarkMode()` requires a fresh browser context.
|
||||
visit('/modal-browser-test')
|
||||
->inDarkMode()
|
||||
->assertNoAccessibilityIssues();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Tests\Fixtures\Pages;
|
||||
|
||||
use BackedEnum;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
|
||||
class AutofocusAfterCreateAnotherTabsBrowserTest extends Page
|
||||
{
|
||||
protected string $view = 'pages.autofocus-after-create-another-tabs-browser-test';
|
||||
|
||||
protected static string | BackedEnum | null $navigationIcon = Heroicon::OutlinedCursorArrowRays;
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
public ?array $data = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->form->fill();
|
||||
}
|
||||
|
||||
public function form(Schema $form): Schema
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Tabs::make('Tabs')
|
||||
->tabs([
|
||||
Tab::make('First Tab')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->autofocus(),
|
||||
TextInput::make('email'),
|
||||
]),
|
||||
Tab::make('Second Tab')
|
||||
->schema([
|
||||
TextInput::make('phone'),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
->statePath('data');
|
||||
}
|
||||
|
||||
public function simulateCreateAnother(): void
|
||||
{
|
||||
$this->form->fill();
|
||||
|
||||
$hydratedDefaultState = null;
|
||||
$this->form->hydrateState($hydratedDefaultState, shouldCallHydrationHooks: false);
|
||||
|
||||
$this->form->dispatchClientSideStateReset();
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Tests\Fixtures\Pages;
|
||||
|
||||
use BackedEnum;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Schemas\Components\Wizard;
|
||||
use Filament\Schemas\Components\Wizard\Step;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
|
||||
class AutofocusAfterCreateAnotherWizardBrowserTest extends Page
|
||||
{
|
||||
protected string $view = 'pages.autofocus-after-create-another-wizard-browser-test';
|
||||
|
||||
protected static string | BackedEnum | null $navigationIcon = Heroicon::OutlinedCursorArrowRays;
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
public ?array $data = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->form->fill();
|
||||
}
|
||||
|
||||
public function form(Schema $form): Schema
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Wizard::make([
|
||||
Step::make('First Step')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->autofocus(),
|
||||
TextInput::make('email'),
|
||||
]),
|
||||
Step::make('Second Step')
|
||||
->schema([
|
||||
TextInput::make('phone'),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
->statePath('data');
|
||||
}
|
||||
|
||||
public function simulateCreateAnother(): void
|
||||
{
|
||||
$this->form->fill();
|
||||
|
||||
$hydratedDefaultState = null;
|
||||
$this->form->hydrateState($hydratedDefaultState, shouldCallHydrationHooks: false);
|
||||
|
||||
$this->form->dispatchClientSideStateReset();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace Filament\Tests\Fixtures\Pages;
|
||||
use BackedEnum;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\TimePicker;
|
||||
use Filament\Pages\Page;
|
||||
@@ -47,6 +48,20 @@ class DateTimePickerTest extends Page
|
||||
|
||||
public bool $hasZonedDisabledDate = false;
|
||||
|
||||
public bool $hasDisabledDates = false;
|
||||
|
||||
public string $calendarLocale = 'en';
|
||||
|
||||
public int $calendarWeekStart = 1;
|
||||
|
||||
public bool $overlaysParentCalendarModal = false;
|
||||
|
||||
public bool $closesOnDateSelection = false;
|
||||
|
||||
public int $hourStep = 1;
|
||||
|
||||
public int $minuteStep = 1;
|
||||
|
||||
public array $saved = [];
|
||||
|
||||
public int $saveCount = 0;
|
||||
@@ -68,6 +83,13 @@ class DateTimePickerTest extends Page
|
||||
$this->calendarState = request()->query('calendar-state');
|
||||
$this->displayFormat = request()->query('display-format');
|
||||
$this->hasZonedDisabledDate = request()->boolean('zoned-disabled-date');
|
||||
$this->hasDisabledDates = request()->boolean('disabled-dates');
|
||||
$this->calendarLocale = request()->string('locale', 'en')->toString();
|
||||
$this->calendarWeekStart = request()->integer('week-start', 1);
|
||||
$this->overlaysParentCalendarModal = request()->boolean('overlay');
|
||||
$this->closesOnDateSelection = request()->boolean('close-on-selection');
|
||||
$this->hourStep = request()->integer('hour-step', 1);
|
||||
$this->minuteStep = request()->integer('minute-step', 1);
|
||||
$this->form->fill($this->calendarState !== null ? ['field' => $this->calendarState] : ($this->hasDstLimits ? ['field' => '01:45:07'] : []));
|
||||
}
|
||||
|
||||
@@ -80,6 +102,12 @@ class DateTimePickerTest extends Page
|
||||
->disabled($this->isDisabled)
|
||||
->readOnly($this->isReadOnly)
|
||||
->autofocus($this->isAutofocused)
|
||||
->locale($this->calendarLocale)
|
||||
->firstDayOfWeek($this->calendarWeekStart)
|
||||
->closeOnDateSelection($this->closesOnDateSelection)
|
||||
->hoursStep($this->hourStep)
|
||||
->minutesStep($this->minuteStep)
|
||||
->disabledDates($this->hasDisabledDates ? ['2025-07-16'] : [])
|
||||
->defaultFocusedDate('2025-07-15 13:24:37')
|
||||
->placeholder('Choose a date or time')
|
||||
->seconds($this->hasSeconds)
|
||||
@@ -123,6 +151,31 @@ class DateTimePickerTest extends Page
|
||||
->statePath('data');
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('calendarModal')
|
||||
->schema([
|
||||
DateTimePicker::make('parentDate')->native(false),
|
||||
])
|
||||
->modalSubmitAction(false)
|
||||
->extraAttributes(['data-testid' => 'calendar-modal-trigger'])
|
||||
->extraModalWindowAttributes(['data-testid' => 'calendar-modal'])
|
||||
->extraModalFooterActions([
|
||||
Action::make('nestedCalendarModal')
|
||||
->schema([
|
||||
DateTimePicker::make('nestedDate')
|
||||
->native(false)
|
||||
->extraTriggerAttributes(['data-testid' => 'nested-calendar-trigger']),
|
||||
])
|
||||
->overlayParentActions($this->overlaysParentCalendarModal)
|
||||
->modalSubmitAction(false)
|
||||
->extraAttributes(['data-testid' => 'nested-calendar-modal-trigger'])
|
||||
->extraModalWindowAttributes(['data-testid' => 'nested-calendar-modal']),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->saved = $this->form->getState();
|
||||
|
||||
@@ -23,10 +23,6 @@ class FiltersResetActionBrowserTest extends Page implements HasTable
|
||||
{
|
||||
use Tables\Concerns\InteractsWithTable;
|
||||
|
||||
public bool $areFiltersDeferred = false;
|
||||
|
||||
public int $applyActionVersion = 0;
|
||||
|
||||
public string $focusScenario = 'default';
|
||||
|
||||
public bool $hasFocusLifecycleFilters = false;
|
||||
@@ -37,7 +33,6 @@ class FiltersResetActionBrowserTest extends Page implements HasTable
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->areFiltersDeferred = request()->boolean('deferred');
|
||||
$this->focusScenario = request()->string('focusScenario')->toString();
|
||||
$this->hasFocusLifecycleFilters = request()->boolean('focus');
|
||||
}
|
||||
@@ -112,15 +107,7 @@ class FiltersResetActionBrowserTest extends Page implements HasTable
|
||||
'selectFirst' => [$detailsFilter, $publishedFilter],
|
||||
default => $this->hasFocusLifecycleFilters ? [$publishedFilter, $detailsFilter] : [$publishedFilter],
|
||||
})
|
||||
->deferFilters($this->areFiltersDeferred)
|
||||
->filtersApplyAction(
|
||||
fn (Action $action) => $action
|
||||
->action('applyTableFiltersAndUpdateAction')
|
||||
->extraAttributes(fn (): array => [
|
||||
'data-testid' => 'filters-apply-action',
|
||||
'id' => "filters-apply-action-{$this->applyActionVersion}",
|
||||
]),
|
||||
)
|
||||
->deferFilters(false)
|
||||
->filtersTriggerAction(
|
||||
static fn (Action $action) => $action
|
||||
->extraAttributes(['data-testid' => 'filters-trigger']),
|
||||
@@ -134,13 +121,6 @@ class FiltersResetActionBrowserTest extends Page implements HasTable
|
||||
);
|
||||
}
|
||||
|
||||
public function applyTableFiltersAndUpdateAction(): void
|
||||
{
|
||||
$this->applyTableFilters();
|
||||
|
||||
$this->applyActionVersion++;
|
||||
}
|
||||
|
||||
public function content(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
|
||||
@@ -24,11 +24,11 @@ class NotificationBrowserTest extends Page
|
||||
$this->isInlineNotificationShown = true;
|
||||
}
|
||||
|
||||
public function sendTimedNotification(int $duration = 800): void
|
||||
public function sendTimedNotification(): void
|
||||
{
|
||||
Notification::make('timed-notification')
|
||||
->title('Timed notification')
|
||||
->duration($duration)
|
||||
->duration(800)
|
||||
->send();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,7 @@ use Filament\Tests\Fixtures\Clusters\WithoutSubNavigationCluster\Pages\Clustered
|
||||
use Filament\Tests\Fixtures\Pages\Actions;
|
||||
use Filament\Tests\Fixtures\Pages\AfterStateUpdatedJsTest;
|
||||
use Filament\Tests\Fixtures\Pages\AuthorizableSettings;
|
||||
use Filament\Tests\Fixtures\Pages\AutofocusAfterCreateAnotherTabsBrowserTest;
|
||||
use Filament\Tests\Fixtures\Pages\AutofocusAfterCreateAnotherTabsModalBrowserTest;
|
||||
use Filament\Tests\Fixtures\Pages\AutofocusAfterCreateAnotherWizardBrowserTest;
|
||||
use Filament\Tests\Fixtures\Pages\AutofocusBasicBrowserTest;
|
||||
use Filament\Tests\Fixtures\Pages\AutofocusBrowserTest;
|
||||
use Filament\Tests\Fixtures\Pages\AutofocusSecondTabBrowserTest;
|
||||
@@ -140,9 +138,7 @@ class AdminPanelProvider extends PanelProvider
|
||||
Actions::class,
|
||||
AfterStateUpdatedJsTest::class,
|
||||
AuthorizableSettings::class,
|
||||
AutofocusAfterCreateAnotherTabsBrowserTest::class,
|
||||
AutofocusAfterCreateAnotherTabsModalBrowserTest::class,
|
||||
AutofocusAfterCreateAnotherWizardBrowserTest::class,
|
||||
AutofocusBasicBrowserTest::class,
|
||||
AutofocusBrowserTest::class,
|
||||
AutofocusSecondTabBrowserTest::class,
|
||||
|
||||
@@ -14,7 +14,9 @@ class StatsOverviewWidgetWithPlaceholder extends StatsOverviewWidget
|
||||
protected function getStats(): array
|
||||
{
|
||||
return [
|
||||
Stat::make('Total orders', 0),
|
||||
Stat::make('Total orders', 0)
|
||||
->chart([13, 4, 21, 9])
|
||||
->extraAttributes(['data-testid' => 'orders-stat']),
|
||||
Stat::make('Conversion rate', null)
|
||||
->placeholder('Not available'),
|
||||
];
|
||||
|
||||
@@ -53,50 +53,6 @@ it('only autofocuses a text input after its tab becomes active', function (): vo
|
||||
});
|
||||
});
|
||||
|
||||
it('resets to the first tab and refocuses an `autofocus()` field after `create another`', function (): void {
|
||||
retry(10, function (): void {
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
visit('/autofocus-after-create-another-tabs-browser-test')
|
||||
->assertVisible('input[wire\\:model="data.name"]')
|
||||
->assertScript('document.activeElement === document.querySelector("[autofocus]")', true)
|
||||
->click('input[wire\\:model="data.email"]')
|
||||
->wait(0.1)
|
||||
->assertScript('document.activeElement === document.querySelector("[autofocus]")', false)
|
||||
->click('[data-testid="simulate-create-another"]')
|
||||
->wait(0.5)
|
||||
->assertScript('document.activeElement === document.querySelector("[autofocus]")', true)
|
||||
->click('.fi-tabs-item >> text=Second Tab')
|
||||
->wait(0.3)
|
||||
->assertScript('document.activeElement === document.querySelector("[autofocus]")', false)
|
||||
->click('[data-testid="simulate-create-another"]')
|
||||
->wait(0.5)
|
||||
->assertScript('document.activeElement === document.querySelector("[autofocus]")', true);
|
||||
});
|
||||
});
|
||||
|
||||
it('resets to the first wizard step and refocuses an `autofocus()` field after `create another`', function (): void {
|
||||
retry(10, function (): void {
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
visit('/autofocus-after-create-another-wizard-browser-test')
|
||||
->assertVisible('input[wire\\:model="data.name"]')
|
||||
->assertScript('document.activeElement === document.querySelector("[autofocus]")', true)
|
||||
->click('input[wire\\:model="data.email"]')
|
||||
->wait(0.1)
|
||||
->assertScript('document.activeElement === document.querySelector("[autofocus]")', false)
|
||||
->click('[data-testid="simulate-create-another"]')
|
||||
->wait(0.5)
|
||||
->assertScript('document.activeElement === document.querySelector("[autofocus]")', true)
|
||||
->click('button >> text=Next')
|
||||
->wait(0.3)
|
||||
->assertScript('document.activeElement === document.querySelector("[autofocus]")', false)
|
||||
->click('[data-testid="simulate-create-another"]')
|
||||
->wait(0.5)
|
||||
->assertScript('document.activeElement === document.querySelector("[autofocus]")', true);
|
||||
});
|
||||
});
|
||||
|
||||
it('refocuses an `autofocus()` field after `create another` inside a `CreateAction` modal that contains tabs', function (): void {
|
||||
retry(10, function (): void {
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
@@ -1149,64 +1149,6 @@ it('can search blocks in the picker in the browser', function (bool $isDarkMode)
|
||||
->assertNoAccessibilityIssues();
|
||||
})->with(['light' => false, 'dark' => true]);
|
||||
|
||||
it('focuses the block picker search when its lazy component initializes after opening', function (bool $isDarkMode): void {
|
||||
Artisan::call('filament:assets');
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$addBlockAction = '[data-testid="add-block"]';
|
||||
$searchInput = '[data-testid="builder"] .fi-fo-builder-block-picker-search-ctn input';
|
||||
$page = visit('/builder-searchable-test');
|
||||
|
||||
if ($isDarkMode) {
|
||||
$page = $page->inDarkMode();
|
||||
}
|
||||
|
||||
$removeDropdownOpenedListener = <<<'JS'
|
||||
const searchInput = document.querySelector('[data-testid="builder"] .fi-fo-builder-block-picker-search-ctn input')
|
||||
const picker = Alpine.$data(searchInput.closest('[x-data]'))
|
||||
|
||||
picker.dropdownPanel.removeEventListener('dropdown-opened', picker.dropdownOpenedListener)
|
||||
picker.dropdownOpenedListener = null
|
||||
JS;
|
||||
|
||||
$page->script($removeDropdownOpenedListener);
|
||||
|
||||
$page
|
||||
->click($addBlockAction)
|
||||
->assertVisible($searchInput)
|
||||
->assertScript('document.activeElement.closest(\'[data-testid="add-block"]\') !== null', true);
|
||||
|
||||
$page->script(<<<'JS'
|
||||
const searchInput = document.querySelector('[data-testid="builder"] .fi-fo-builder-block-picker-search-ctn input')
|
||||
|
||||
Alpine.$data(searchInput.closest('[x-data]')).setUpDropdownAutofocus()
|
||||
JS);
|
||||
|
||||
$page
|
||||
->assertScript('document.activeElement.matches(\'.fi-fo-builder-block-picker-search-ctn input\')', true)
|
||||
->click($addBlockAction);
|
||||
|
||||
$page->script($removeDropdownOpenedListener);
|
||||
|
||||
$page
|
||||
->click($addBlockAction)
|
||||
->assertVisible($searchInput)
|
||||
->click($addBlockAction)
|
||||
->assertMissing($searchInput);
|
||||
|
||||
$page->script(<<<'JS'
|
||||
const picker = Alpine.$data(document.querySelector('[data-testid="builder"] [x-data="builderBlockPickerFormComponent()"]'))
|
||||
|
||||
picker.setUpDropdownAutofocus()
|
||||
JS);
|
||||
|
||||
$page
|
||||
->assertScript('document.activeElement.closest(\'[data-testid="add-block"]\') !== null', true)
|
||||
->assertNoSmoke()
|
||||
->assertNoAccessibilityIssues();
|
||||
})->with(['light' => false, 'dark' => true]);
|
||||
|
||||
it('clears a debounced block picker search with `Escape` before the debounce elapses', function (): void {
|
||||
Artisan::call('filament:assets');
|
||||
|
||||
|
||||
@@ -440,51 +440,6 @@ it('can render `ColorPicker` in the browser', function (): void {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not run delayed callbacks after `ColorPicker` is destroyed', function (): void {
|
||||
retry(10, function (): void {
|
||||
Artisan::call('filament:assets');
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit('/color-picker-test');
|
||||
|
||||
$page->assertScript('document.querySelector(\'[data-testid="color-picker"] [x-data]:not([x-ignore])\') !== null', true);
|
||||
|
||||
$page->script(<<<'JS'
|
||||
window.colorPickerCommitCount = 0
|
||||
window.removeColorPickerCommitHook = Livewire.hook('commit', () => window.colorPickerCommitCount++)
|
||||
|
||||
const colorPicker = document.querySelector('[data-testid="color-picker"] [x-data]')
|
||||
const input = colorPicker.querySelector('input')
|
||||
|
||||
input.value = '#123456'
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
|
||||
colorPicker.querySelector('[x-ref="panel"]').dispatchEvent(new CustomEvent('color-changed', {
|
||||
bubbles: true,
|
||||
detail: { value: '#123456' },
|
||||
}))
|
||||
|
||||
setTimeout(() => colorPicker.remove())
|
||||
JS);
|
||||
|
||||
$page
|
||||
->wait(2)
|
||||
->assertMissing('[data-testid="color-picker"] [x-data]')
|
||||
->assertScript('window.colorPickerCommitCount', 0)
|
||||
->assertNoSmoke();
|
||||
|
||||
$page->script('window.removeColorPickerCommitHook()');
|
||||
|
||||
visit('/color-picker-test')
|
||||
->assertNoAccessibilityIssues();
|
||||
|
||||
visit('/color-picker-test')
|
||||
->inDarkMode()
|
||||
->assertNoAccessibilityIssues();
|
||||
});
|
||||
});
|
||||
|
||||
class RenderColorPickerWithHsl extends Livewire
|
||||
{
|
||||
public function form(Schema $form): Schema
|
||||
|
||||
@@ -788,7 +788,10 @@ it('saves boundary dates and round-trips epoch-backed and model-backed dates wit
|
||||
|
||||
$page->fill('[data-testid="timestamp-date"]', '2025-07-16')
|
||||
->click('[data-testid="custom-timestamp-date"]')
|
||||
->keys('[data-testid="custom-timestamp-date"]', ['ArrowRight', 'Enter', 'Escape'])
|
||||
->assertPresent('[data-date="2025-07-15"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'ArrowRight')
|
||||
->assertPresent('[data-date="2025-07-16"]:focus')
|
||||
->keys('[data-date="2025-07-16"]', ['Enter', 'Escape'])
|
||||
->assertValue('[data-testid="custom-timestamp-date"]', '2025-07-16')
|
||||
->click('[data-testid="save-dates"]')
|
||||
->assertScript('document.querySelector(\'[data-testid="saved-timestamp-date"]\').textContent.trim()', '1752591600')
|
||||
|
||||
@@ -1360,15 +1360,20 @@ it('preserves field calendar components through browser timezone gaps and folds,
|
||||
->assertScript("{$picker}.state", $state);
|
||||
}
|
||||
|
||||
$page->keys($trigger, ['ArrowLeft', 'Enter'])
|
||||
$page->keys($trigger, 'Enter')
|
||||
->assertPresent('[role="gridcell"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'ArrowLeft')
|
||||
->assertPresent('[data-date="' . Carbon::parse($state, 'UTC')->subDay()->toDateString() . '"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'Enter')
|
||||
->assertScript("{$picker}.state", $state)
|
||||
->keys($trigger, 'Escape')
|
||||
->keys('[role="gridcell"]:focus', 'Escape')
|
||||
->assertScript("{$picker}.isOpen()", false)
|
||||
->keys($trigger, 'Enter')
|
||||
->assertScript("{$picker}.isOpen()", true)
|
||||
->keys($trigger, 'Enter')
|
||||
->assertPresent('[role="gridcell"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'Enter')
|
||||
->assertScript("{$picker}.state", $state)
|
||||
->keys($trigger, 'Escape')
|
||||
->keys('[role="gridcell"]:focus', 'Escape')
|
||||
->assertScript("{$picker}.isOpen()", false)
|
||||
->assertScript("{$picker}.state", $state);
|
||||
|
||||
@@ -1382,26 +1387,36 @@ it('preserves field calendar components through browser timezone gaps and folds,
|
||||
}
|
||||
|
||||
$page->click($trigger)
|
||||
->keys($trigger, ['ArrowRight', 'Enter'])
|
||||
->assertPresent('[data-date="' . Carbon::parse($internal, 'UTC')->toDateString() . '"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'ArrowRight')
|
||||
->assertPresent('[data-date="' . Carbon::parse($internal, 'UTC')->addDay()->toDateString() . '"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'Enter')
|
||||
->assertScript("{$picker}.state", $internal)
|
||||
->keys($trigger, 'Escape')
|
||||
->keys('[role="gridcell"]:focus', 'Escape')
|
||||
->assertScript("{$picker}.isOpen()", false)
|
||||
->keys($trigger, 'Enter')
|
||||
->assertScript("{$picker}.isOpen()", true)
|
||||
->keys($trigger, 'ArrowRight')
|
||||
->assertPresent('[data-date="' . Carbon::parse($internal, 'UTC')->toDateString() . '"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'ArrowRight')
|
||||
->assertPresent('[data-date="' . Carbon::parse($internal, 'UTC')->addDay()->toDateString() . '"]:focus')
|
||||
->assertScript("{$picker}.focusedDate.format('YYYY-MM-DD')", Carbon::parse($internal, 'UTC')->addDay()->toDateString())
|
||||
->keys($trigger, 'ArrowRight')
|
||||
->keys('[role="gridcell"]:focus', 'ArrowRight')
|
||||
->assertPresent('[data-date="' . Carbon::parse($lastDate, 'UTC')->toDateString() . '"]:focus')
|
||||
->assertScript("{$picker}.focusedDate.format('YYYY-MM-DD')", Carbon::parse($lastDate, 'UTC')->toDateString())
|
||||
->keys($trigger, 'Enter')
|
||||
->keys('[role="gridcell"]:focus', 'Enter')
|
||||
->assertScript("{$picker}.state", $lastDate)
|
||||
->keys($trigger, 'Escape')
|
||||
->keys('[role="gridcell"]:focus', 'Escape')
|
||||
->click('[data-testid="save-timed"]')
|
||||
->assertScript('document.querySelector(\'[data-testid="saved-timed"]\').textContent.trim()', $lastStored)
|
||||
->click('[data-testid="reload-timed"]')
|
||||
->assertScript('document.querySelector(\'[data-testid="timed-save-count"]\').dataset.reloadCount', '3')
|
||||
->click($trigger)
|
||||
->keys($trigger, ['ArrowRight', 'Enter'])
|
||||
->assertPresent('[data-date="' . Carbon::parse($lastDate, 'UTC')->toDateString() . '"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'ArrowRight')
|
||||
->assertPresent('[data-date="' . Carbon::parse($lastDate, 'UTC')->addDay()->toDateString() . '"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'Enter')
|
||||
->assertScript("{$picker}.state", $lastDate)
|
||||
->keys('[role="gridcell"]:focus', 'Escape')
|
||||
->keys($trigger, 'Backspace')
|
||||
->assertScript("{$picker}.state", null)
|
||||
->keys($trigger, 'Escape')
|
||||
@@ -1417,12 +1432,15 @@ it('preserves field calendar components through browser timezone gaps and folds,
|
||||
$page->assertScript("{$picker}.dateIsDisabled(dayjs.utc('{$state}'))", true)
|
||||
->assertScript("{$picker}.dateIsDisabled(dayjs.utc('{$state}').add(1, 'day'))", false)
|
||||
->click($trigger)
|
||||
->keys('[role="gridcell"]:focus', 'Shift+Tab')
|
||||
->keys('input[aria-label="Year"]', 'Shift+Tab')
|
||||
->assertPresent('select[aria-label="Month"]:focus')
|
||||
->select('select[aria-label="Month"]', '1')
|
||||
->fill('input[aria-label="Year"]', '2024')
|
||||
->assertScript("{$picker}.focusedDate.format('YYYY-MM')", '2024-02')
|
||||
->assertScript("{$picker}.daysInFocusedMonth.length", 29)
|
||||
->assertScript('document.querySelectorAll(\'[data-testid="date-time-picker"] [role="gridcell"][data-date]\').length', 29)
|
||||
->assertScript("{$picker}.state", null)
|
||||
->keys($trigger, 'Escape');
|
||||
->keys('input[aria-label="Year"]', 'Escape');
|
||||
$page->script('new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))).then(() => Promise.all(document.getAnimations().filter(animation => animation.effect.getComputedTiming().iterations !== Infinity).map(animation => animation.finished.catch(() => {}))))');
|
||||
$page->assertNoSmoke()->assertNoAccessibilityIssues();
|
||||
}
|
||||
@@ -1502,50 +1520,6 @@ it('preserves browser-local disabled-day checks when editing an initially empty
|
||||
}
|
||||
})->with([true, false]);
|
||||
|
||||
it('initializes an empty time-only calendar without applying the browser timezone twice during a fold', function (): void {
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
foreach ([false, true] as $isDarkMode) {
|
||||
$page = visit('/date-time-picker-test?native=0&date=0')->withTimezone('America/Nuuk');
|
||||
|
||||
if ($isDarkMode) {
|
||||
$page = $page->inDarkMode();
|
||||
}
|
||||
|
||||
$page->assertScript('typeof Alpine.$data(document.querySelector(\'[data-testid="timed-trigger"]\')).init', 'function')
|
||||
->assertValue('[data-testid="timed-trigger"]', '')
|
||||
->assertScript(<<<'JS'
|
||||
(() => {
|
||||
const OriginalDate = Date;
|
||||
try {
|
||||
window.Date = class extends OriginalDate {
|
||||
constructor(...parameters) { super(...(parameters.length ? parameters : ['2025-10-26T01:30:00Z'])); }
|
||||
};
|
||||
const component = {
|
||||
...document.querySelector('[data-testid="timed-trigger"]').closest('[x-data]')._x_dataStack[0],
|
||||
$nextTick: callback => callback(),
|
||||
$watch: () => {},
|
||||
$refs: { disabledDates: { value: '["2025-10-25"]' } },
|
||||
defaultFocusedDate: null,
|
||||
focusedDate: null,
|
||||
focusedMonth: null,
|
||||
focusedYear: null,
|
||||
state: null,
|
||||
};
|
||||
component.init();
|
||||
const focus = component.focusedDate.format('YYYY-MM-DD HH:mm Z');
|
||||
component.hour = 2;
|
||||
component.setState(component.focusedDate.hour(2));
|
||||
return { focus, state: component.state };
|
||||
} finally {
|
||||
window.Date = OriginalDate;
|
||||
}
|
||||
})()
|
||||
JS, ['focus' => '2025-10-25 23:30 -03:00', 'state' => null])
|
||||
->assertNoSmoke()->assertNoAccessibilityIssues();
|
||||
}
|
||||
});
|
||||
|
||||
it('compares custom time-only clocks independently of browser DST dates and preserves hidden seconds on reload', function (): void {
|
||||
$this->actingAs(User::factory()->create());
|
||||
$accessibilityFailures = [];
|
||||
@@ -1603,7 +1577,7 @@ it('compares custom time-only clocks independently of browser DST dates and pres
|
||||
it('uses one labelled input to open, select, copy and clear a custom picker with the keyboard', function (bool $hasDate, bool $hasTime): void {
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit('/date-time-picker-test?native=0&date=' . (int) $hasDate . '&time=' . (int) $hasTime)->withTimezone('UTC');
|
||||
$page = visit('/date-time-picker-test?native=0&close-on-selection=1&date=' . (int) $hasDate . '&time=' . (int) $hasTime)->withTimezone('UTC');
|
||||
$trigger = '[data-testid="timed-trigger"]';
|
||||
$picker = 'Alpine.$data(document.querySelector(\'[data-testid="timed-trigger"]\'))';
|
||||
|
||||
@@ -1619,23 +1593,37 @@ it('uses one labelled input to open, select, copy and clear a custom picker with
|
||||
->assertAttribute($trigger, 'aria-expanded', 'false')
|
||||
->assertNoAccessibilityIssues()
|
||||
->click('label[for="' . $page->attribute($trigger, 'id') . '"]')
|
||||
->assertPresent($trigger . ':focus')
|
||||
->assertVisible('[role="dialog"]');
|
||||
|
||||
// Wait for the forwarded label click to open the popup before testing `Escape`.
|
||||
$page->keys($trigger, 'Escape')
|
||||
$page->keys($hasDate ? '[role="gridcell"]:focus' : $trigger, 'Escape')
|
||||
->assertPresent($trigger . ':focus')
|
||||
->assertScript("{$picker}.isOpen()", false)
|
||||
->keys($trigger, 'Enter')
|
||||
->assertAttribute($trigger, 'aria-expanded', 'true')
|
||||
->assertVisible('[role="dialog"]')
|
||||
->assertAttributeMissing('[role="dialog"]', 'aria-modal')
|
||||
->assertPresent($trigger . ':focus');
|
||||
->assertNoAccessibilityIssues();
|
||||
|
||||
if ($hasDate) {
|
||||
$page->keys($trigger, ['ArrowRight', 'ArrowDown', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'Enter'])
|
||||
->assertScript("{$picker}.getSelectedDate().format('YYYY-MM-DD')", '2025-07-16');
|
||||
$page->assertPresent('[data-date="2025-07-15"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'ArrowRight')
|
||||
->assertPresent('[data-date="2025-07-16"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'ArrowDown')
|
||||
->assertPresent('[data-date="2025-07-23"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'ArrowLeft')
|
||||
->assertPresent('[data-date="2025-07-22"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'ArrowUp')
|
||||
->assertPresent('[data-date="2025-07-15"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'ArrowRight')
|
||||
->assertPresent('[data-date="2025-07-16"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'Enter')
|
||||
->assertScript("{$picker}.getSelectedDate().format('YYYY-MM-DD')", '2025-07-16')
|
||||
->assertPresent($trigger . ':focus')
|
||||
->assertAttribute($trigger, 'aria-expanded', 'false');
|
||||
} else {
|
||||
$page->fill('input[aria-label="Minute"]', '31')
|
||||
$page->assertPresent($trigger . ':focus')
|
||||
->fill('input[aria-label="Minute"]', '31')
|
||||
->assertScript("{$picker}.minute", 31)
|
||||
->assertNoAccessibilityIssues();
|
||||
}
|
||||
@@ -1662,10 +1650,358 @@ it('uses one labelled input to open, select, copy and clear a custom picker with
|
||||
'time only' => [false, true],
|
||||
]);
|
||||
|
||||
it('does not open or clear a disabled or read-only custom picker, including with `autofocus()`', function (string $mode): void {
|
||||
it('navigates month and year boundaries without changing the selected date until activation', function (): void {
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit("/date-time-picker-test?native=0&date=0&dst=1&autofocus=1&{$mode}=1");
|
||||
$page = visit('/date-time-picker-test?native=0&time=0')->withTimezone('UTC');
|
||||
$trigger = '[data-testid="timed-trigger"]';
|
||||
$picker = 'Alpine.$data(document.querySelector(\'[data-testid="timed-trigger"]\'))';
|
||||
|
||||
foreach ([false, true] as $isDarkMode) {
|
||||
if ($isDarkMode) {
|
||||
$page = $page->inDarkMode();
|
||||
}
|
||||
|
||||
$page->assertScript("{$picker}.focusedDate.format('YYYY-MM-DD')", '2025-07-15');
|
||||
$page->script("{$picker}.state = '2024-01-31 00:00:00'");
|
||||
$page->keys($trigger, 'ArrowDown')
|
||||
->assertPresent('[data-date="2024-01-31"]:focus')
|
||||
->assertAttribute('[data-date="2024-01-31"]', 'aria-selected', 'true')
|
||||
->keys('[role="gridcell"]:focus', 'PageDown')
|
||||
->assertScript("{$picker}.focusedDate.format('YYYY-MM-DD')", '2024-02-29')
|
||||
->assertPresent('[data-date="2024-02-29"]:focus')
|
||||
->assertAttribute('[data-date="2024-02-29"]', 'aria-selected', 'false')
|
||||
->assertScript("{$picker}.state", '2024-01-31 00:00:00')
|
||||
->keys('[role="gridcell"]:focus', ['Shift+PageDown'])
|
||||
->assertPresent('[data-date="2025-02-28"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'ArrowRight')
|
||||
->assertPresent('[data-date="2025-03-01"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'Home')
|
||||
->assertPresent('[data-date="2025-02-24"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'End')
|
||||
->assertPresent('[data-date="2025-03-02"]:focus')
|
||||
->keys('[role="gridcell"]:focus', ['ArrowUp', 'ArrowDown', 'PageUp'])
|
||||
->assertPresent('[data-date="2025-02-02"]:focus')
|
||||
->keys('[role="gridcell"]:focus', ['Shift+PageUp'])
|
||||
->assertPresent('[data-date="2024-02-02"]:focus')
|
||||
->assertScript('document.querySelectorAll(\'[role="gridcell"][tabindex="0"]\').length', 1)
|
||||
->assertScript('Array.from(document.querySelectorAll(\'[role="grid"] [role="row"]\')).every(row => row.querySelectorAll(\'[role="gridcell"], [role="columnheader"]\').length === 7)', true)
|
||||
->assertNoAccessibilityIssues()
|
||||
->keys('[role="gridcell"]:focus', ' ')
|
||||
->assertScript("{$picker}.state", '2024-02-02 00:00:00')
|
||||
->assertPresent('[data-date="2024-02-02"]:focus')
|
||||
->assertAttribute('[data-date="2024-02-02"]', 'aria-selected', 'true')
|
||||
->keys('[role="gridcell"]:focus', 'Escape')
|
||||
->assertPresent($trigger . ':focus')
|
||||
->keys($trigger, 'Enter')
|
||||
->assertPresent('[data-date="2024-02-02"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'Escape')
|
||||
->keys($trigger, 'Delete')
|
||||
->keys($trigger, 'Enter')
|
||||
->assertPresent('[data-date="2024-02-02"]:focus')
|
||||
->assertScript("{$picker}.state", null)
|
||||
->assertScript('document.querySelectorAll(\'[role="gridcell"][aria-selected="true"]\').length', 0)
|
||||
->assertNoAccessibilityIssues()
|
||||
->keys('[role="gridcell"]:focus', 'Escape')
|
||||
->assertNoSmoke();
|
||||
|
||||
$page->script("{$picker}.state = '2024-12-31 00:00:00'");
|
||||
$page->keys($trigger, 'Enter')
|
||||
->assertPresent('[data-date="2024-12-31"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'ArrowRight')
|
||||
->assertPresent('[data-date="2025-01-01"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'ArrowLeft')
|
||||
->assertPresent('[data-date="2024-12-31"]:focus')
|
||||
->assertScript("{$picker}.state", '2024-12-31 00:00:00')
|
||||
->withKeyDown('ArrowRight', static fn () => null)
|
||||
->withKeyDown('ArrowRight', static fn () => null)
|
||||
->withKeyDown('Enter', static fn () => null)
|
||||
->assertScript("{$picker}.state", '2025-01-02 00:00:00')
|
||||
->assertPresent('[data-date="2025-01-02"]:focus')
|
||||
->assertNoAccessibilityIssues()
|
||||
->withKeyDown('PageUp', static fn () => null)
|
||||
->withKeyDown('Tab', static fn () => null)
|
||||
->assertPresent('[data-testid="save-timed"]:focus')
|
||||
->assertAttribute($trigger, 'aria-expanded', 'false')
|
||||
->assertScript("{$picker}.state", '2025-01-02 00:00:00');
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps unavailable dates focusable but rejects activation, even in a wholly unavailable month', function (): void {
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit('/date-time-picker-test?native=0&bounds=1&disabled-dates=1')->withTimezone('UTC');
|
||||
$trigger = '[data-testid="timed-trigger"]';
|
||||
$picker = 'Alpine.$data(document.querySelector(\'[data-testid="timed-trigger"]\'))';
|
||||
|
||||
foreach ([false, true] as $isDarkMode) {
|
||||
if ($isDarkMode) {
|
||||
$page = $page->inDarkMode();
|
||||
}
|
||||
|
||||
$page->assertScript("{$picker}.focusedDate.format('YYYY-MM-DD')", '2025-07-15');
|
||||
$page->script("{$picker}.state = '2025-07-15 13:24:37'");
|
||||
$page->assertScript("{$picker}.state", '2025-07-15 13:24:37');
|
||||
$page->click($trigger)
|
||||
->keys('[role="gridcell"]:focus', 'ArrowRight')
|
||||
->assertPresent('[data-date="2025-07-16"]:focus')
|
||||
->assertAttribute('[data-date="2025-07-16"]', 'aria-disabled', 'true')
|
||||
->keys('[role="gridcell"]:focus', ['Enter', ' '])
|
||||
->assertScript("{$picker}.state", '2025-07-15 13:24:37')
|
||||
->assertAttribute($trigger, 'aria-expanded', 'true')
|
||||
->assertAttribute('[data-date="2025-07-14"]', 'aria-disabled', 'true')
|
||||
->assertAttribute('[data-date="2025-07-15"]', 'aria-disabled', 'false')
|
||||
->assertAttribute('[data-date="2025-07-17"]', 'aria-disabled', 'false')
|
||||
->assertAttribute('[data-date="2025-07-18"]', 'aria-disabled', 'true')
|
||||
->assertNoAccessibilityIssues()
|
||||
->keys('[role="gridcell"]:focus', 'PageDown')
|
||||
->assertPresent('[data-date="2025-08-16"]:focus')
|
||||
->assertScript('Array.from(document.querySelectorAll(\'[role="gridcell"][data-date]\')).every(cell => cell.getAttribute("aria-disabled") === "true")', true)
|
||||
->keys('[role="gridcell"]:focus', 'Enter')
|
||||
->assertScript("{$picker}.state", '2025-07-15 13:24:37')
|
||||
->assertNoAccessibilityIssues()
|
||||
->keys('[role="gridcell"]:focus', 'Escape')
|
||||
->assertPresent($trigger . ':focus')
|
||||
->assertNoSmoke();
|
||||
}
|
||||
});
|
||||
|
||||
it('uses localized date names, a Sunday week start and RTL horizontal navigation', function (): void {
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit('/date-time-picker-test?native=0&time=0&locale=fr&week-start=7')->withTimezone('UTC');
|
||||
$trigger = '[data-testid="timed-trigger"]';
|
||||
|
||||
foreach ([false, true] as $isDarkMode) {
|
||||
if ($isDarkMode) {
|
||||
$page = $page->inDarkMode();
|
||||
}
|
||||
|
||||
$page->assertScript('Alpine.$data(document.querySelector(\'[data-testid="timed-trigger"]\')).focusedDate.format("YYYY-MM-DD")', '2025-07-15');
|
||||
$page->script('document.querySelector(\'[data-testid="date-time-picker"]\').dir = "rtl"');
|
||||
$page->keys($trigger, 'Enter')
|
||||
->assertPresent('[data-date="2025-07-15"]:focus')
|
||||
->assertAttribute('[data-date="2025-07-15"]', 'aria-label', 'mardi, 15 juillet 2025')
|
||||
->assertAttribute('[role="grid"]', 'aria-label', 'juillet 2025')
|
||||
->assertAttribute('[role="columnheader"]:first-of-type', 'aria-label', 'dimanche')
|
||||
->keys('[role="gridcell"]:focus', 'ArrowRight')
|
||||
->assertPresent('[data-date="2025-07-14"]:focus')
|
||||
->keys('[role="gridcell"]:focus', ['ArrowLeft', 'Home'])
|
||||
->assertPresent('[data-date="2025-07-13"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'End')
|
||||
->assertPresent('[data-date="2025-07-19"]:focus')
|
||||
->assertNoAccessibilityIssues()
|
||||
->keys('[role="gridcell"]:focus', ['Shift+Tab'])
|
||||
->assertPresent('input[aria-label="Year"]:focus')
|
||||
->keys('input[aria-label="Year"]', 'Escape')
|
||||
->assertPresent($trigger . ':focus')
|
||||
->keys($trigger, 'Enter')
|
||||
->keys('[role="gridcell"]:focus', 'Tab')
|
||||
->assertPresent('[data-testid="save-timed"]:focus')
|
||||
->assertAttribute($trigger, 'aria-expanded', 'false')
|
||||
->assertNoSmoke();
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
it('closes the calendar before its nested action modal and restores each focus owner in turn', function (bool $overlaysParentModal): void {
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit('/date-time-picker-test?native=0&overlay=' . (int) $overlaysParentModal);
|
||||
|
||||
foreach ([false, true] as $isDarkMode) {
|
||||
if ($isDarkMode) {
|
||||
$page = $page->inDarkMode();
|
||||
}
|
||||
|
||||
$page->keys('[data-testid="calendar-modal-trigger"]', 'Enter')
|
||||
->assertVisible('[data-testid="calendar-modal"]')
|
||||
->assertAttributeMissing('[data-testid="nested-calendar-modal-trigger"]', 'aria-disabled');
|
||||
$page->script('document.querySelector(\'[data-testid="nested-calendar-modal-trigger"]\').focus()');
|
||||
$page->assertPresent('[data-testid="nested-calendar-modal-trigger"]:focus')
|
||||
->keys('[data-testid="nested-calendar-modal-trigger"]', 'Enter')
|
||||
->assertVisible('[data-testid="nested-calendar-modal"]')
|
||||
->click('[data-testid="nested-calendar-trigger"]')
|
||||
->assertPresent('[role="gridcell"]:focus');
|
||||
$page->script('new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))).then(() => Promise.all(document.getAnimations().filter(animation => animation.effect.getComputedTiming().iterations !== Infinity).map(animation => animation.finished.catch(() => {}))))');
|
||||
$page->assertNoAccessibilityIssues()
|
||||
->keys('[role="gridcell"]:focus', 'Escape')
|
||||
->assertAttribute('[data-testid="nested-calendar-trigger"]', 'aria-expanded', 'false')
|
||||
->assertPresent('[data-testid="nested-calendar-trigger"]:focus')
|
||||
->assertVisible('[data-testid="nested-calendar-modal"]')
|
||||
->keys('[data-testid="nested-calendar-trigger"]', 'Escape')
|
||||
->assertMissing('[data-testid="nested-calendar-modal"]')
|
||||
->assertPresent('[data-testid="nested-calendar-modal-trigger"]:focus')
|
||||
->keys('[data-testid="nested-calendar-modal-trigger"]', 'Escape')
|
||||
->assertMissing('[data-testid="calendar-modal"]')
|
||||
->assertPresent('[data-testid="calendar-modal-trigger"]:focus')
|
||||
->assertNoSmoke();
|
||||
}
|
||||
})->with([
|
||||
'replacing parent modal' => [false],
|
||||
'overlaying parent modal' => [true],
|
||||
]);
|
||||
|
||||
it('preserves control focus and the selected date when browsing before editing time', function (): void {
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit('/date-time-picker-test?native=0')->withTimezone('UTC');
|
||||
$trigger = '[data-testid="timed-trigger"]';
|
||||
$picker = 'Alpine.$data(document.querySelector(\'[data-testid="timed-trigger"]\'))';
|
||||
|
||||
foreach ([false, true] as $isDarkMode) {
|
||||
if ($isDarkMode) {
|
||||
$page = $page->inDarkMode();
|
||||
}
|
||||
|
||||
$page->assertScript("{$picker}.focusedDate.format('YYYY-MM-DD')", '2025-07-15');
|
||||
$page->script("{$picker}.state = '2024-03-31 13:24:37'");
|
||||
$page->click($trigger)
|
||||
->assertPresent('[data-date="2024-03-31"]:focus')
|
||||
->keys('[role="gridcell"]:focus', ['Shift+Tab'])
|
||||
->fill('input[aria-label="Year"]', '2025')
|
||||
->assertScript("{$picker}.focusedDate.format('YYYY-MM-DD')", '2025-03-31')
|
||||
->assertPresent('input[aria-label="Year"]:focus')
|
||||
->keys('input[aria-label="Year"]', ['Shift+Tab'])
|
||||
->select('select[aria-label="Month"]', '1')
|
||||
->assertScript("{$picker}.focusedDate.format('YYYY-MM-DD')", '2025-02-28')
|
||||
->assertScript("{$picker}.state", '2024-03-31 13:24:37')
|
||||
->assertPresent('select[aria-label="Month"]:focus')
|
||||
->keys('select[aria-label="Month"]', 'Tab')
|
||||
->keys('input[aria-label="Year"]', 'Tab')
|
||||
->assertPresent('[data-date="2025-02-28"]:focus')
|
||||
->keys('[role="gridcell"]:focus', 'Tab')
|
||||
->fill('input[aria-label="Minute"]', '31')
|
||||
->assertScript("{$picker}.state", '2024-03-31 13:31:37')
|
||||
->assertPresent('input[aria-label="Minute"]:focus')
|
||||
->assertNoAccessibilityIssues()
|
||||
->keys('input[aria-label="Minute"]', 'Escape')
|
||||
->keys($trigger, 'Delete')
|
||||
->keys($trigger, 'Enter')
|
||||
->assertPresent('[data-date="2025-02-28"]:focus')
|
||||
->fill('input[aria-label="Minute"]', '19')
|
||||
->assertScript("{$picker}.state", '2025-02-28 00:19:00')
|
||||
->assertNoAccessibilityIssues()
|
||||
->keys('input[aria-label="Minute"]', 'Escape')
|
||||
->assertNoSmoke();
|
||||
|
||||
$page->script("{$picker}.state = '2024-12-31 13:24:37'");
|
||||
$page->keys($trigger, 'Enter')
|
||||
->assertPresent('[data-date="2024-12-31"]:focus')
|
||||
->withKeyDown('ArrowRight', static fn () => null)
|
||||
->withKeyDown('Tab', static fn () => null)
|
||||
->assertPresent('input[aria-label="Hour"]:focus')
|
||||
->assertScript("{$picker}.state", '2024-12-31 13:24:37')
|
||||
->keys('input[aria-label="Hour"]', 'Shift+Tab')
|
||||
->assertPresent('[data-date="2025-01-01"]:focus')
|
||||
->assertNoAccessibilityIssues()
|
||||
->keys('[role="gridcell"]:focus', 'Escape')
|
||||
->assertNoSmoke();
|
||||
}
|
||||
});
|
||||
|
||||
it('opens for stepped time validation without stealing focus and cancels stale opening requests', function (): void {
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit('/date-time-picker-test?native=0&hour-step=2&minute-step=5');
|
||||
$trigger = '[data-testid="timed-trigger"]';
|
||||
$picker = 'Alpine.$data(document.querySelector(\'[data-testid="timed-trigger"]\'))';
|
||||
|
||||
foreach ([false, true] as $isDarkMode) {
|
||||
if ($isDarkMode) {
|
||||
$page = $page->inDarkMode();
|
||||
}
|
||||
|
||||
$page->assertScript("{$picker}.focusedDate.format('YYYY-MM-DD')", '2025-07-15')
|
||||
->click($trigger)
|
||||
->assertPresent('[role="gridcell"]:focus')
|
||||
->fill('input[aria-label="Hour"]', '14')
|
||||
->assertScript("{$picker}.hour", 14)
|
||||
->fill('input[aria-label="Minute"]', '31')
|
||||
->assertScript("{$picker}.minute", 31)
|
||||
->keys('input[aria-label="Minute"]', 'Escape')
|
||||
->click('[data-testid="save-timed"]')
|
||||
->assertPresent('input[aria-label="Minute"]:focus')
|
||||
->assertAttribute($trigger, 'aria-expanded', 'true')
|
||||
->assertScript('document.querySelector(\'input[aria-label="Minute"]\').validity.stepMismatch', true)
|
||||
->assertNoAccessibilityIssues()
|
||||
->fill('input[aria-label="Minute"]', '30')
|
||||
->keys('input[aria-label="Minute"]', 'Escape');
|
||||
|
||||
$page->script("{$picker}.togglePanelVisibility(); document.querySelector('[data-testid=\"timed-trigger\"]').dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, cancelable: true }))");
|
||||
$page->assertPresent('[role="gridcell"]:focus')
|
||||
->assertNoAccessibilityIssues()
|
||||
->keys('[role="gridcell"]:focus', 'Escape');
|
||||
$page->script('window.invalidReports = []; document.querySelector(\'input[aria-label="Hour"]\').value = "13"; document.querySelector(\'input[aria-label="Minute"]\').value = "31"; document.querySelectorAll(\'input[aria-label="Hour"], input[aria-label="Minute"]\').forEach(input => input.addEventListener("invalid", event => invalidReports.push(event.defaultPrevented)))');
|
||||
$page->click('[data-testid="save-timed"]')
|
||||
->assertPresent('input[aria-label="Hour"]:focus')
|
||||
->assertScript('invalidReports.slice(0, 2)', [true, true])
|
||||
->assertNoAccessibilityIssues()
|
||||
->fill('input[aria-label="Hour"]', '14')
|
||||
->fill('input[aria-label="Minute"]', '30')
|
||||
->keys('input[aria-label="Minute"]', 'Escape');
|
||||
|
||||
$page->script("new Promise(resolve => { {$picker}.togglePanelVisibility(); {$picker}.\$nextTick(() => requestAnimationFrame(() => { document.querySelector('input[aria-label=\"Year\"]').focus(); resolve(); })); })");
|
||||
$page->assertPresent('input[aria-label="Year"]:focus')
|
||||
->assertNoAccessibilityIssues()
|
||||
->keys('input[aria-label="Year"]', 'Escape');
|
||||
$page->script("{$picker}.togglePanelVisibility(); {$picker}.closePanel(); document.querySelector('[data-testid=\"save-timed\"]').focus()");
|
||||
$page->assertAttribute($trigger, 'aria-expanded', 'false')
|
||||
->assertPresent('[data-testid="save-timed"]:focus')
|
||||
->assertScript("{$picker}.positioningCleanup", null)
|
||||
->assertNoSmoke();
|
||||
}
|
||||
});
|
||||
|
||||
it('preserves ignored calendars across Livewire refreshes and disposes replaced calendars', function (): void {
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit('/date-time-picker-test?native=0');
|
||||
$trigger = '[data-testid="timed-trigger"]';
|
||||
$picker = 'Alpine.$data(document.querySelector(\'[data-testid="timed-trigger"]\'))';
|
||||
|
||||
foreach ([false, true] as $isDarkMode) {
|
||||
if ($isDarkMode) {
|
||||
$page = $page->inDarkMode();
|
||||
}
|
||||
|
||||
$page->assertScript("{$picker}.focusedDate.format('YYYY-MM-DD')", '2025-07-15')
|
||||
->click($trigger)
|
||||
->assertPresent('[data-date="2025-07-15"]:focus');
|
||||
$page->script("window.originalCalendar = {$picker}; {$picker}.\$wire.\$refresh()");
|
||||
$page->assertScript("{$picker}.isDestroyed", false)
|
||||
->assertPresent('[data-date="2025-07-15"]:focus')
|
||||
->assertNoAccessibilityIssues()
|
||||
->click('[data-testid="replace-calendar"]')
|
||||
->assertScript('window.originalCalendar.isDestroyed', true)
|
||||
->assertScript('window.originalCalendar.positioningCleanup', null)
|
||||
->assertAttribute($trigger, 'aria-expanded', 'false')
|
||||
->assertPresent('[data-testid="replace-calendar"]:focus')
|
||||
->click($trigger)
|
||||
->assertPresent('[data-date="2025-07-15"]:focus')
|
||||
->assertAttribute('[data-date="2025-07-16"]', 'aria-disabled', 'true')
|
||||
->assertNoAccessibilityIssues();
|
||||
|
||||
$page->script("window.replacedCalendar = {$picker}; {$picker}.focusCalendarDate(); Alpine.destroyTree({$picker}.\$el); {$picker}.\$el.remove(); document.querySelector('[data-testid=\"save-timed\"]').focus()");
|
||||
$page->assertScript('window.replacedCalendar.isDestroyed', true)
|
||||
->assertScript('window.replacedCalendar.positioningCleanup', null)
|
||||
->assertPresent('[data-testid="save-timed"]:focus')
|
||||
->assertNoSmoke();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not open or clear a disabled or read-only custom picker, including with `autofocus()`', function (string $mode, bool $hasDate, bool $hasTime): void {
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit('/date-time-picker-test?' . http_build_query([
|
||||
'native' => 0,
|
||||
'date' => (int) $hasDate,
|
||||
'time' => (int) $hasTime,
|
||||
'dst' => 1,
|
||||
'autofocus' => 1,
|
||||
'calendar-state' => '2025-07-15 01:45:07',
|
||||
$mode => 1,
|
||||
]));
|
||||
$trigger = '[data-testid="timed-trigger"]';
|
||||
$picker = 'Alpine.$data(document.querySelector(\'[data-testid="timed-trigger"]\'))';
|
||||
|
||||
@@ -1692,7 +2028,11 @@ it('does not open or clear a disabled or read-only custom picker, including with
|
||||
$page->script('new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))).then(() => Promise.all(document.getAnimations().filter(animation => animation.effect.getComputedTiming().iterations !== Infinity).map(animation => animation.finished.catch(() => {}))))');
|
||||
$page->assertNoAccessibilityIssues();
|
||||
}
|
||||
})->with(['disabled', 'readonly']);
|
||||
})->with(['disabled', 'readonly'])->with([
|
||||
'time-only' => [false, true],
|
||||
'date-only' => [true, false],
|
||||
'date-and-time' => [true, true],
|
||||
]);
|
||||
|
||||
class RenderDateTimePickerWithTimeDisabled extends Livewire
|
||||
{
|
||||
|
||||
@@ -130,8 +130,6 @@ describe('`toEmbeddedHtml()` output', function (): void {
|
||||
expect($html)->toContain('fi-fo-rich-editor-dropdown-tool');
|
||||
expect($html)->toContain('fi-fo-rich-editor-dropdown-tool-menu');
|
||||
expect($html)->toContain('aria-haspopup="menu"');
|
||||
expect($html)->toContain('$event.preventDefault(); $event.stopPropagation()');
|
||||
expect($html)->toContain('x-on:focusout="if (open && ! $el.contains($event.relatedTarget)) open = false"');
|
||||
expect($html)->toContain('Format');
|
||||
});
|
||||
|
||||
|
||||
@@ -12,30 +12,6 @@ beforeEach(function (): void {
|
||||
$this->actingAs(User::factory()->create());
|
||||
});
|
||||
|
||||
it('pauses the remaining lifetime while hovered or focused', function (): void {
|
||||
$notification = '.fi-no-notification';
|
||||
$closeButton = "{$notification} .fi-no-notification-close-btn";
|
||||
$origin = '[data-testid="send-timed-notification"]';
|
||||
|
||||
$page = visit('/notification-browser-test')
|
||||
->click($origin)
|
||||
->assertVisible($notification)
|
||||
->wait(0.9)
|
||||
->hover($notification)
|
||||
->wait(1.4)
|
||||
->assertVisible($notification)
|
||||
->assertScript("(document.querySelector('{$closeButton}').focus(), document.activeElement.matches('{$closeButton}'))", true)
|
||||
->hover($origin)
|
||||
->wait(1.1)
|
||||
->assertVisible($notification)
|
||||
->assertScript("(document.querySelector('{$origin}').focus(), document.activeElement.matches('{$origin}'))", true)
|
||||
->wait(0.35)
|
||||
->assertVisible($notification)
|
||||
->wait(0.8);
|
||||
|
||||
expect($page->script("document.querySelector('{$notification}') === null"))->toBeTrue();
|
||||
});
|
||||
|
||||
it('keeps finite duration behavior for inline notifications', function (): void {
|
||||
visit('/notification-browser-test')
|
||||
->assertScript("(sessionStorage.setItem('inlineNotificationClosedCount', '0'), window.addEventListener('notificationClosed', (event) => event.detail.id === 'inline-timed-notification' && sessionStorage.setItem('inlineNotificationClosedCount', String(Number(sessionStorage.getItem('inlineNotificationClosedCount')) + 1))), true)")
|
||||
@@ -185,64 +161,12 @@ it('supports keyboard navigation in an action group menu', function (): void {
|
||||
->assertScript("document.activeElement.matches('{$groupTrigger}')", true)
|
||||
->click($groupTrigger)
|
||||
->assertScript("document.activeElement.matches('{$groupTrigger}')", true)
|
||||
->assertScript(<<<'JS'
|
||||
(() => {
|
||||
const action = document.querySelector('[data-testid="post-action"]')
|
||||
action.replaceWith(action.cloneNode(true))
|
||||
|
||||
return true
|
||||
})()
|
||||
JS, true)
|
||||
->assertScript("document.activeElement.matches('{$groupTrigger}')", true)
|
||||
->click($groupTrigger)
|
||||
->keys($groupTrigger, 'ArrowUp')
|
||||
->assertScript("document.activeElement.matches('{$postAction}')", true)
|
||||
->keys($postAction, 'Tab')
|
||||
->assertMissing($groupedAction)
|
||||
->assertScript("!document.activeElement.matches('{$groupTrigger}, {$postAction}')", true)
|
||||
->keys($groupTrigger, 'Enter')
|
||||
->keys($groupedAction, 'ArrowDown')
|
||||
->keys($disabledAction, 'ArrowDown')
|
||||
->assertScript("document.activeElement.matches('{$linkAction}')", true)
|
||||
->assertScript(<<<'JS'
|
||||
(() => {
|
||||
const link = document.querySelector('[data-testid="link-action"]')
|
||||
link.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }))
|
||||
setTimeout(() => document.activeElement.dispatchEvent(new KeyboardEvent('keyup', { key: ' ', bubbles: true })), 200)
|
||||
|
||||
return true
|
||||
})()
|
||||
JS, true)
|
||||
->wait(0.4)
|
||||
->assertMissing($groupedAction)
|
||||
->assertScript("window.location.hash === '#link-action'", true)
|
||||
->assertScript("document.activeElement.matches('{$groupTrigger}')", true)
|
||||
->assertScript(<<<'JS'
|
||||
(() => {
|
||||
const dropdown = document.querySelector('[data-testid="action-group"]').closest('.fi-dropdown')
|
||||
const data = Alpine.$data(dropdown)
|
||||
const trigger = data.getTrigger()
|
||||
const panel = data.$refs.panel
|
||||
|
||||
window.destroyedActionMenuCloseCount = 0
|
||||
const close = data.close
|
||||
data.close = (...parameters) => {
|
||||
window.destroyedActionMenuCloseCount++
|
||||
|
||||
return close.call(data, ...parameters)
|
||||
}
|
||||
|
||||
Alpine.destroyTree(dropdown)
|
||||
trigger.setAttribute('aria-controls', 'destroyed')
|
||||
document.dispatchEvent(new Event('livewire:navigate'))
|
||||
panel.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
|
||||
|
||||
return true
|
||||
})()
|
||||
JS, true)
|
||||
->wait(0.1)
|
||||
->assertAttribute($groupTrigger, 'aria-controls', 'destroyed')
|
||||
->assertScript('window.destroyedActionMenuCloseCount', 0);
|
||||
->assertScript("!document.activeElement.matches('{$groupTrigger}, {$postAction}')", true);
|
||||
});
|
||||
|
||||
it('preserves keyboard activation of explicitly styled nested action group triggers', function (): void {
|
||||
@@ -313,6 +237,59 @@ it('cleans up an expiring notification when navigating away', function (): void
|
||||
->assertScript("sessionStorage.getItem('notificationClosedCount')", '0');
|
||||
});
|
||||
|
||||
it('preserves a pending dismissal when the notification is destroyed', function (): void {
|
||||
foreach ([false, true] as $isDarkMode) {
|
||||
$browser = visit('/notification-browser-test');
|
||||
|
||||
if ($isDarkMode) {
|
||||
$browser->inDarkMode();
|
||||
}
|
||||
|
||||
$page = $browser
|
||||
->click('[data-testid="send-persistent-notification"]')
|
||||
->assertPresent('[x-data^="notificationComponent"]')
|
||||
->assertNoAccessibilityIssues();
|
||||
$page->script(<<<'JS'
|
||||
window.dismissalCount = 0
|
||||
window.addEventListener('notificationClosed', (event) => {
|
||||
if (event.detail.id === 'persistent-notification') window.dismissalCount++
|
||||
})
|
||||
const element = document.querySelector('[x-data^="notificationComponent"]')
|
||||
const component = Alpine.$data(element)
|
||||
component.close()
|
||||
Alpine.destroyTree(element)
|
||||
element.remove()
|
||||
JS);
|
||||
$page->assertScript('window.dismissalCount', 1)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoAccessibilityIssues();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not dispatch a pending dismissal after its Livewire host is removed', function (): void {
|
||||
$page = visit('/notification-browser-test')
|
||||
->click('[data-testid="send-persistent-notification"]')
|
||||
->assertPresent('[x-data^="notificationComponent"]');
|
||||
|
||||
$page->script(<<<'JS'
|
||||
window.dismissalCount = 0
|
||||
window.addEventListener('notificationClosed', (event) => {
|
||||
if (event.detail.id === 'persistent-notification') window.dismissalCount++
|
||||
})
|
||||
const element = document.querySelector('[x-data^="notificationComponent"]')
|
||||
const host = element.closest('[wire\\:id]')
|
||||
const component = Alpine.$data(element)
|
||||
component.transitionDuration = 300
|
||||
component.close()
|
||||
Alpine.destroyTree(host)
|
||||
host.remove()
|
||||
JS);
|
||||
|
||||
$page->wait(0.5)
|
||||
->assertScript('window.dismissalCount', 0)
|
||||
->assertNoJavaScriptErrors();
|
||||
});
|
||||
|
||||
it('has no accessibility issues in light and dark modes', function (bool $isDarkMode): void {
|
||||
$page = visit('/notification-browser-test');
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
use Filament\Tests\Fixtures\Models\User;
|
||||
use Filament\Tests\TestCase;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
uses(TestCase::class);
|
||||
|
||||
it('does not run queued `init()` or `open()` work after modal teardown', function (): void {
|
||||
Artisan::call('filament:assets');
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$browser = visit('/modal-browser-test');
|
||||
|
||||
foreach ([false, true] as $isDarkMode) {
|
||||
if ($isDarkMode) {
|
||||
$browser->inDarkMode();
|
||||
}
|
||||
|
||||
$results = $browser->script(<<<'JS'
|
||||
(async () => {
|
||||
const results = []
|
||||
const original = document.querySelector('[data-testid="standalone-modal"]').closest('[data-fi-modal-id]')
|
||||
const trigger = document.querySelector('[data-testid="standalone-trigger"]')
|
||||
|
||||
for (const teardown of ['destroy-connected', 'destroy-removed', 'disconnected']) {
|
||||
for (const shouldSettleInitialization of [false, true]) {
|
||||
if ((teardown === 'disconnected') && shouldSettleInitialization) {
|
||||
continue
|
||||
}
|
||||
|
||||
const element = original.cloneNode(true)
|
||||
element.id = 'modal-lifecycle-test'
|
||||
|
||||
Alpine.mutateDom(() => {
|
||||
document.body.append(element)
|
||||
Alpine.initTree(element)
|
||||
})
|
||||
|
||||
const state = Alpine.$data(element)
|
||||
|
||||
if (shouldSettleInitialization) {
|
||||
await Alpine.nextTick()
|
||||
}
|
||||
|
||||
let openedEventCount = 0
|
||||
const openedHandler = () => openedEventCount++
|
||||
document.addEventListener('x-modal-opened', openedHandler)
|
||||
|
||||
trigger.focus({ preventScroll: true })
|
||||
const overflow = document.documentElement.style.overflow
|
||||
const paddingRight = document.documentElement.style.paddingRight
|
||||
|
||||
state.open()
|
||||
|
||||
Alpine.mutateDom(() => {
|
||||
if (teardown !== 'disconnected') {
|
||||
Alpine.destroyTree(element)
|
||||
}
|
||||
|
||||
if (teardown !== 'destroy-connected') {
|
||||
element.remove()
|
||||
}
|
||||
})
|
||||
|
||||
await Alpine.nextTick()
|
||||
|
||||
results.push({
|
||||
isOpen: state.isOpen,
|
||||
isTrapActive: state.isTrapActive,
|
||||
isHoldingScrollLock: state.isHoldingScrollLock,
|
||||
hasSelectionHandlers: !!(state.textSelectionClosePreventionMouseDownHandler || state.textSelectionClosePreventionMouseUpHandler || state.textSelectionClosePreventionClickHandler),
|
||||
openedEventCount,
|
||||
isScrollRestored: document.documentElement.style.overflow === overflow && document.documentElement.style.paddingRight === paddingRight,
|
||||
isFocusPreserved: document.activeElement === trigger,
|
||||
})
|
||||
|
||||
document.removeEventListener('x-modal-opened', openedHandler)
|
||||
|
||||
Alpine.mutateDom(() => {
|
||||
Alpine.destroyTree(element)
|
||||
element.remove()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
})()
|
||||
JS);
|
||||
|
||||
expect($results)->toHaveCount(5);
|
||||
|
||||
foreach ($results as $result) {
|
||||
expect($result)->toBe([
|
||||
'isOpen' => false,
|
||||
'isTrapActive' => false,
|
||||
'isHoldingScrollLock' => false,
|
||||
'hasSelectionHandlers' => false,
|
||||
'openedEventCount' => 0,
|
||||
'isScrollRestored' => true,
|
||||
'isFocusPreserved' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
$browser
|
||||
->assertNoSmoke()
|
||||
->assertNoAccessibilityIssues();
|
||||
}
|
||||
});
|
||||
@@ -196,7 +196,8 @@ test('`hasNestedClickEventHandler` forces Alpine navigation when SPA mode is ena
|
||||
FilamentView::spa(true, false);
|
||||
|
||||
expect(generate_href_html('http://localhost/page', hasNestedClickEventHandler: true))
|
||||
->toHtml()->toBe('href="http://localhost/page" x-on:click="if (! ($event.altKey || $event.ctrlKey || $event.metaKey || $event.shiftKey)) { $event.preventDefault(); Alpine.navigate($el.getAttribute(\'href\')) }"');
|
||||
->toHtml()->toContain('href="http://localhost/page"', 'x-on:click=')
|
||||
->not->toContain('wire:navigate');
|
||||
|
||||
expect(generate_href_html('http://localhost/page', hasNestedClickEventHandler: false))
|
||||
->toHtml()->toBe('href="http://localhost/page" wire:navigate');
|
||||
@@ -236,7 +237,8 @@ test('`hasNestedClickEventHandler` works with `shouldOpenInSpaMode` parameter ov
|
||||
FilamentView::spa(false);
|
||||
|
||||
expect(generate_href_html('http://localhost/page', shouldOpenInSpaMode: true, hasNestedClickEventHandler: true))
|
||||
->toHtml()->toBe('href="http://localhost/page" x-on:click="if (! ($event.altKey || $event.ctrlKey || $event.metaKey || $event.shiftKey)) { $event.preventDefault(); Alpine.navigate($el.getAttribute(\'href\')) }"');
|
||||
->toHtml()->toContain('href="http://localhost/page"', 'x-on:click=')
|
||||
->not->toContain('wire:navigate');
|
||||
|
||||
expect(generate_href_html('http://localhost/page', shouldOpenInSpaMode: true, hasNestedClickEventHandler: false))
|
||||
->toHtml()->toBe('href="http://localhost/page" wire:navigate');
|
||||
|
||||
@@ -484,77 +484,6 @@ it('contains filters `Escape` handling inside an enclosing modal', function (boo
|
||||
});
|
||||
})->with(['light' => false, 'dark' => true]);
|
||||
|
||||
it('preserves deferred filter state through focus lifecycle actions', function (bool $isDarkMode): void {
|
||||
retry(10, function () use ($isDarkMode): void {
|
||||
Artisan::call('filament:assets');
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$filtersTrigger = '[data-testid="filters-trigger"]';
|
||||
$publishedFilter = '[data-testid="published-filter"]';
|
||||
|
||||
$page = visit('/filters-reset-action-browser-test?focus=1&deferred=1');
|
||||
|
||||
if ($isDarkMode) {
|
||||
$page = $page->inDarkMode();
|
||||
}
|
||||
|
||||
$page
|
||||
->keys($filtersTrigger, 'Enter')
|
||||
->click($publishedFilter)
|
||||
->assertAttribute($publishedFilter, 'aria-checked', 'true');
|
||||
|
||||
$page->assertAttribute('[data-testid="filters-apply-action"]', 'id', 'filters-apply-action-0');
|
||||
|
||||
$page->script(<<<'JS'
|
||||
const originalFetch = window.fetch
|
||||
|
||||
window.fetch = (...arguments) => new Promise((resolve, reject) => {
|
||||
window.releasePendingFilterRequest = () => {
|
||||
window.fetch = originalFetch
|
||||
originalFetch(...arguments).then(resolve, reject)
|
||||
}
|
||||
})
|
||||
|
||||
document.querySelector('[data-testid="filters-trigger"]').closest('[wire\\:id]').__livewire.$wire.$refresh()
|
||||
undefined
|
||||
JS);
|
||||
|
||||
$page->wait(0.25);
|
||||
|
||||
$page->script(<<<'JS'
|
||||
const applyAction = document.querySelector('[data-testid="filters-apply-action"]')
|
||||
|
||||
applyAction.focus()
|
||||
applyAction.click()
|
||||
JS);
|
||||
|
||||
$page->script('window.releasePendingFilterRequest()');
|
||||
|
||||
$page
|
||||
->wait(0.5)
|
||||
->assertVisible($publishedFilter)
|
||||
->assertAttribute('[data-testid="filters-apply-action"]', 'id', 'filters-apply-action-1')
|
||||
->assertScript('document.activeElement.closest(\'[data-testid="filters-apply-action"]\') !== null', true)
|
||||
->keys('[data-testid="filters-reset-action"]', 'Enter')
|
||||
->wait(0.5)
|
||||
->assertAttribute($publishedFilter, 'aria-checked', 'false')
|
||||
->assertScript('document.activeElement.closest(\'[data-testid="filters-reset-action"]\') !== null', true)
|
||||
->click($publishedFilter)
|
||||
->keys($publishedFilter, 'Escape')
|
||||
->assertMissing($publishedFilter)
|
||||
->assertScript('document.activeElement.closest(\'[data-testid="filters-trigger"]\') !== null', true)
|
||||
->click($filtersTrigger)
|
||||
->wait(0.5)
|
||||
->assertScript('document.activeElement.closest(\'.fi-ta-filters-dropdown\') !== null', true)
|
||||
->assertAttribute($publishedFilter, 'aria-checked', 'true')
|
||||
->keys($publishedFilter, 'Escape')
|
||||
->assertScript('document.activeElement.closest(\'[data-testid="filters-trigger"]\') !== null', true)
|
||||
->wait(0.5)
|
||||
->assertNoSmoke();
|
||||
});
|
||||
})->with(['light' => false, 'dark' => true]);
|
||||
|
||||
it('can use a custom attribute for the `SelectFilter`', function (): void {
|
||||
$posts = Post::factory()->count(10)->create();
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ it('keeps the accessible name and alternative synchronized when the chart data i
|
||||
->assertSee('The monthly sales total for 2023 is 60.');
|
||||
});
|
||||
|
||||
it('renders an accessible chart alternative in the browser', function (): void {
|
||||
it('keeps chart alternatives accessible through updates and chart replacement', function (): void {
|
||||
Artisan::call('filament:assets');
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
@@ -220,6 +220,47 @@ it('renders an accessible chart alternative in the browser', function (): void {
|
||||
->assertScript('document.querySelector(".fi-wi-chart-assistive-content").querySelector("[aria-live], [role=alert], [role=status]")', null)
|
||||
->assertNoAccessibilityIssues();
|
||||
|
||||
$page->assertScript('Boolean(Alpine.$data(document.querySelector("[role=img] canvas").parentElement).getChart())', true);
|
||||
|
||||
$page->script(<<<'JS'
|
||||
const element = document.querySelector('[role=img] canvas').parentElement
|
||||
const component = Alpine.$data(element)
|
||||
const updateChartTheme = component.updateChartTheme.bind(component)
|
||||
window.chartLifecycle = { element, component, chart: component.getChart(), oldDataUpdates: 0, themeUpdates: 0 }
|
||||
component.updateChartTheme = () => {
|
||||
updateChartTheme()
|
||||
window.chartLifecycle.themeUpdates++
|
||||
}
|
||||
component.$wire.$dispatchSelf('updateChartData', {
|
||||
data: { labels: ['April', 'May'], datasets: [{ label: 'Sales', data: [9, 4] }] },
|
||||
})
|
||||
JS);
|
||||
|
||||
$page->assertScript('window.chartLifecycle.chart.data.datasets[0].data', [9, 4]);
|
||||
$page->script('Alpine.store("theme", "dark")');
|
||||
$page->assertScript('window.chartLifecycle.themeUpdates > 0', true)
|
||||
->assertScript('window.chartLifecycle.component.getChart() === window.chartLifecycle.chart', true)
|
||||
->assertScript('window.chartLifecycle.chart.data.datasets[0].data', [9, 4]);
|
||||
|
||||
$page->script(<<<'JS'
|
||||
const { element, component } = window.chartLifecycle
|
||||
Alpine.destroyTree(element)
|
||||
component.updateChartData = () => window.chartLifecycle.oldDataUpdates++
|
||||
Alpine.initTree(element)
|
||||
JS);
|
||||
|
||||
$page->assertScript('Boolean(Alpine.$data(window.chartLifecycle.element).getChart())', true)
|
||||
->assertScript('window.chartLifecycle.chart.canvas', null);
|
||||
$page->script(<<<'JS'
|
||||
Alpine.$data(window.chartLifecycle.element).$wire.$dispatchSelf('updateChartData', {
|
||||
data: { labels: ['June', 'July'], datasets: [{ label: 'Sales', data: [2, 7] }] },
|
||||
})
|
||||
JS);
|
||||
$page->assertScript('Alpine.$data(window.chartLifecycle.element).getChart().data.datasets[0].data', [2, 7])
|
||||
->assertScript('window.chartLifecycle.oldDataUpdates', 0)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoAccessibilityIssues();
|
||||
|
||||
visit('/chart-widget-browser-test', ['reducedMotion' => 'reduce'])
|
||||
->inDarkMode()
|
||||
->assertAttribute('.fi-wi-chart-image', 'aria-label', 'Sales. Sales for 2024')
|
||||
|
||||
@@ -130,17 +130,22 @@ it('renders `placeholder()` for values considered blank by `blank()`', function
|
||||
'whitespace-only string' => [' '],
|
||||
]);
|
||||
|
||||
it('has no accessibility issues in light and dark modes', function (): void {
|
||||
retry(10, function (): void {
|
||||
$this->actingAs(User::factory()->create());
|
||||
it('renders accessible charts in light and dark modes', function (): void {
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
visit('/stats-overview-widget-browser-test')
|
||||
->assertNoAccessibilityIssues();
|
||||
visit('/stats-overview-widget-browser-test')
|
||||
->assertVisible('[data-testid="orders-stat"] canvas')
|
||||
->assertScript("!!Alpine.\$data(document.querySelector('[data-testid=\"orders-stat\"] canvas').closest('[x-data]')).getChart()", true)
|
||||
->assertNoSmoke()
|
||||
->assertNoAccessibilityIssues();
|
||||
|
||||
visit('/stats-overview-widget-browser-test')
|
||||
->inDarkMode()
|
||||
->assertNoAccessibilityIssues();
|
||||
});
|
||||
// `inDarkMode()` requires a fresh browser context.
|
||||
visit('/stats-overview-widget-browser-test')
|
||||
->inDarkMode()
|
||||
->assertVisible('[data-testid="orders-stat"] canvas')
|
||||
->assertScript("!!Alpine.\$data(document.querySelector('[data-testid=\"orders-stat\"] canvas').closest('[x-data]')).getChart()", true)
|
||||
->assertNoSmoke()
|
||||
->assertNoAccessibilityIssues();
|
||||
});
|
||||
|
||||
class TestStatsOverviewWidgetDefault extends StatsOverviewWidget
|
||||
|
||||
Reference in New Issue
Block a user