diff --git a/package-lock.json b/package-lock.json index 8cffa2bb3..3d53d65e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { + "@awesome.me/webawesome": "^3.9.0", "@lit/context": "^1.1.6", "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", @@ -179,6 +180,138 @@ "dev": true, "license": "MIT" }, + "node_modules/@awesome.me/webawesome": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@awesome.me/webawesome/-/webawesome-3.9.0.tgz", + "integrity": "sha512-doKgUCglb/E1d/dYSzXQQthlYmyndFOtG/Vr5HyHjIFqNwn/tbutMsIMYJrYFEjq07vPwcOHKeAulbiqoslRoQ==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "4.1.0", + "@floating-ui/dom": "^1.6.13", + "@konnorr/qr-creator": "^1.0.1", + "@lit-labs/ssr": "^4.1.0", + "@lit-labs/ssr-client": "^1.1.8", + "@lit/context": "^1.1.6", + "@lit/react": "^1.0.8", + "@shoelace-style/animations": "^1.2.0", + "@shoelace-style/localize": "^3.2.2", + "composed-offset-position": "^0.0.6", + "lit": "^3.2.1", + "marked": "^11.2.0", + "nanoid": "^5.1.5" + }, + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/@awesome.me/webawesome/node_modules/@lit-labs/ssr": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr/-/ssr-4.1.0.tgz", + "integrity": "sha512-m0zymVVlHB1ddJQ1lastsV8ROW3whFOiHJhVPQWd04MnGTkTlUUVLQctux1QlyD9BtLXNN6iASxv388vhgKMFg==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-client": "^1.1.7", + "@lit-labs/ssr-dom-shim": "^1.6.0", + "@lit/reactive-element": "^2.0.4", + "@parse5/tools": "^0.3.0", + "enhanced-resolve": "^5.10.0", + "lit": "^3.1.2", + "lit-element": "^4.0.4", + "lit-html": "^3.1.2", + "node-fetch": "^3.2.8", + "parse5": "^7.1.1" + }, + "engines": { + "node": ">=13.9.0" + }, + "peerDependencies": { + "@types/node": ">=20.0.0 <25.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@awesome.me/webawesome/node_modules/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@awesome.me/webawesome/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@awesome.me/webawesome/node_modules/nanoid": { + "version": "5.1.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.15.tgz", + "integrity": "sha512-kBg3RpGtIe+RpTbyXwoI6pk5yD7KUiI3sygUqgeBMRst42KmhB4RZC7eiO9Wa1HIpaCCtpE2DJ6OI4Wi5ebwFw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@awesome.me/webawesome/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/@awesome.me/webawesome/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/@awesome.me/webawesome/node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/@babel/cli": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.29.7.tgz", @@ -2072,6 +2205,15 @@ "node": ">=20.19.0" } }, + "node_modules/@ctrl/tinycolor": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.1.0.tgz", + "integrity": "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/@digitalbazaar/http-client": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@digitalbazaar/http-client/-/http-client-4.3.0.tgz", @@ -2807,6 +2949,31 @@ } } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, "node_modules/@frogcat/ttl2jsonld": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/@frogcat/ttl2jsonld/-/ttl2jsonld-0.0.10.tgz", @@ -2987,6 +3154,23 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@konnorr/qr-creator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@konnorr/qr-creator/-/qr-creator-1.0.1.tgz", + "integrity": "sha512-EmRR9rny1ENBtQy7TLOguO/79h1EpzXUmqIdMTGp2BW8NkZtRRPr5hmQcE+n9QXYo4T8NjcaJ14hnZg+s/+K+A==", + "license": "MIT" + }, + "node_modules/@lit-labs/ssr-client": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-client/-/ssr-client-1.1.8.tgz", + "integrity": "sha512-PjGh81oKsoI64m3IDjTqqjhC7dr2uC/o0jrllUb5gRAyp/RlAHxapgJrjq9kWz97faCHLQ8jUlTi6tGm+8fgyA==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.0.4", + "lit": "^3.1.2", + "lit-html": "^3.1.2" + } + }, "node_modules/@lit-labs/ssr-dom-shim": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.6.0.tgz", @@ -3002,6 +3186,15 @@ "@lit/reactive-element": "^1.6.2 || ^2.1.0" } }, + "node_modules/@lit/react": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@lit/react/-/react-1.0.8.tgz", + "integrity": "sha512-p2+YcF+JE67SRX3mMlJ1TKCSTsgyOVdAwd/nxp3NuV1+Cb6MWALbN6nT7Ld4tpmYofcE5kcaSY1YBB9erY+6fw==", + "license": "BSD-3-Clause", + "peerDependencies": { + "@types/react": "17 || 18 || 19" + } + }, "node_modules/@lit/reactive-element": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", @@ -3818,6 +4011,39 @@ "win32" ] }, + "node_modules/@parse5/tools": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@parse5/tools/-/tools-0.3.0.tgz", + "integrity": "sha512-zxRyTHkqb7WQMV8kTNBKWb1BeOFUKXBXTBWuxg9H9hfvQB3IwP6Iw2U75Ia5eyRxPNltmY7E8YAlz6zWwUnjKg==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + } + }, + "node_modules/@parse5/tools/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@parse5/tools/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/@rdfjs/types": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@rdfjs/types/-/types-2.0.1.tgz", @@ -4223,6 +4449,22 @@ "dev": true, "license": "MIT" }, + "node_modules/@shoelace-style/animations": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@shoelace-style/animations/-/animations-1.2.0.tgz", + "integrity": "sha512-avvo1xxkLbv2dgtabdewBbqcJfV0e0zCwFqkPMnHFGbJbBHorRFfMAHh1NG9ymmXn0jW95ibUVH03E1NYXD6Gw==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/claviska" + } + }, + "node_modules/@shoelace-style/localize": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@shoelace-style/localize/-/localize-3.2.2.tgz", + "integrity": "sha512-h3+2/cFWGaw3KQUwintkP4Cy3PtrVW//ysr9DM5nOfIXYekgrHwsELk/nyxc8hmjVP/Kcon7KCzvUtSwUBipfQ==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -4933,7 +5175,6 @@ "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -6618,6 +6859,15 @@ "dev": true, "license": "MIT" }, + "node_modules/composed-offset-position": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/composed-offset-position/-/composed-offset-position-0.0.6.tgz", + "integrity": "sha512-Q7dLompI6lUwd7LWyIcP66r4WcS9u7AL2h8HaeipiRfCRPLMWqRx8fYsjb4OHi6UQFifO7XtNC2IlEJ1ozIFxw==", + "license": "MIT", + "peerDependencies": { + "@floating-ui/utils": "^0.2.5" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -6771,10 +7021,18 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT", "peer": true }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-urls": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", @@ -7100,7 +7358,6 @@ "version": "5.21.6", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -8202,6 +8459,29 @@ } } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -8292,6 +8572,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -8754,7 +9046,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/gry": { @@ -10537,6 +10828,18 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/marked": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-11.2.0.tgz", + "integrity": "sha512-HR0m3bvu0jAPYiIvLUUQtdg1g6D247//lvcekpHO1WMvbwDlwSkZAX9Lw4F4YHE1T0HaaNve0tuAWuV1UJ6vtw==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -10914,6 +11217,26 @@ "node": ">=18.20.0 <20 || >=20.12.1" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-exports-info": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", @@ -13092,7 +13415,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -14445,6 +14767,15 @@ "node": ">=18" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", diff --git a/package.json b/package.json index fa8218c34..2acf572b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "solid-ui", - "version": "3.1.3-5", + "version": "3.1.3-6", "description": "UI library for Solid applications", "main": "dist/index.cjs.js", "types": "dist/index.d.ts", @@ -85,6 +85,7 @@ }, "homepage": "https://github.com/SolidOS/solid-ui", "dependencies": { + "@awesome.me/webawesome": "^3.9.0", "@lit/context": "^1.1.6", "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", diff --git a/src/components/account/Account.ts b/src/components/account/Account.ts index 8c2804bff..a891084e4 100644 --- a/src/components/account/Account.ts +++ b/src/components/account/Account.ts @@ -8,7 +8,6 @@ import '@/components/button' import '@/components/login-button' import '@/components/logout-button' import '@/components/menu-item' -import '@/components/menu-items' import '@/components/menu' import '@/components/signup-button' import '~icons/lucide/chevron-down' @@ -61,20 +60,18 @@ export default class Account extends WebComponent { } return html` - + - - - - - Sign out - - - + + + + Sign out + + ` } diff --git a/src/components/combobox/Combobox.ts b/src/components/combobox/Combobox.ts index b7d58ca2e..ec5534de2 100644 --- a/src/components/combobox/Combobox.ts +++ b/src/components/combobox/Combobox.ts @@ -1,7 +1,8 @@ +import { customElement, WebComponent } from '@/lib/components' import { html } from 'lit' import { property, query, state } from 'lit/decorators.js' -import { generateId, WebComponent, customElement } from '@/lib/components' -import ComboboxOption from '@/components/combobox-option/ComboboxOption' +import InputTrait from '@/lib/components/traits/InputTrait' +import type ComboboxOption from '@/components/combobox-option/ComboboxOption' import '~icons/lucide/chevron-down' @@ -15,9 +16,18 @@ export default class Combobox extends WebComponent { @property({ type: String, reflect: true }) accessor label = '' + @property({ type: String, reflect: true }) + accessor name = '' + @property({ type: String }) accessor value = '' + @property({ type: String, reflect: true }) + accessor placeholder = '' + + @property({ type: Boolean, reflect: true }) + accessor required = false + @query('[popover]') private accessor popoverElement: HTMLDivElement | null = null @@ -27,42 +37,67 @@ export default class Combobox extends WebComponent { @state() private accessor filter = '' - private inputId = `combobox-${generateId()}` + private inputTrait: InputTrait - protected render () { - const options = this.getOptions().filter(option => option.toLowerCase().includes(this.filter)) + constructor () { + super() - return html` - ${this.label ? html`` : ''} -
- - -
-
- ${options.map(option => html`
this.onOptionClick(option)}>${option}
`)} -
- ` + this.inputTrait = this.addTrait( + new InputTrait(this, { + getInputElement: () => this.inputElement, + getInternals: () => this.getInternals(), + onValueChanged: (value) => { + this.filter = value.toLowerCase() + }, + }) + ) } - private getOptions (): string[] { - const options = this.querySelectorAll('solid-ui-combobox-option') + protected render () { + const options = this.getOptions().filter((option) => + option.label.toLowerCase().includes(this.filter) + ) - return Array.from(options).map(option => option.value) + return html` + ${this.inputTrait.renderLabel()} +
+ this.inputTrait.onInput()} + /> + +
+
+ ${options.map( + (option) => + html`
this.onOptionClick(option.value)} + > + ${option.label} +
` + )} +
+ ` } - private setValue (value: string) { - this.filter = value.toLowerCase() - this.value = value + private getOptions (): { value: string; label: string }[] { + const options = this.querySelectorAll( + 'solid-ui-combobox-option' + ) - this.getInternals().setFormValue(value) - this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })) + return Array.from(options).map((option) => ({ + value: option.value, + label: option.textContent, + })) } private onInputKeyDown (e: KeyboardEvent) { @@ -75,7 +110,7 @@ export default class Combobox extends WebComponent { if (!this.popoverElement?.matches(':popover-open')) { e.preventDefault() - this.getInternals().form?.requestSubmit() + this.inputTrait.onSubmit() } break } @@ -87,12 +122,8 @@ export default class Combobox extends WebComponent { this.popoverElement?.showPopover() } - private onInputChange () { - this.setValue(this.inputElement?.value ?? '') - } - private onOptionClick (option: string) { - this.setValue(option) + this.inputTrait.setValue(option) this.popoverElement?.hidePopover() } diff --git a/src/components/dialog/Dialog.ts b/src/components/dialog/Dialog.ts index 0e3b4dda2..c5014564d 100644 --- a/src/components/dialog/Dialog.ts +++ b/src/components/dialog/Dialog.ts @@ -1,9 +1,9 @@ +import { customElement, WebComponent } from '@/lib/components' import { consume } from '@lit/context' import { html, nothing } from 'lit' import { property, query } from 'lit/decorators.js' -import { customElement, WebComponent } from '@/lib/components' -import { DialogContext, dialogContext } from '../../lib/dialogs/context' -import { CloseDialogEvent } from '../../lib/dialogs/events/close-dialog' +import { DialogContext, dialogContext, DEFAULT_DIALOG_CONTEXT } from '@/lib/dialogs/context' +import DialogTrait from '@/lib/components/traits/DialogTrait' import '~icons/lucide/x' import '@/components/dialog-header' @@ -22,14 +22,20 @@ export default class Dialog extends WebComponent { private accessor nativeDialog: HTMLDialogElement | null = null @consume({ context: dialogContext, subscribe: true }) - private accessor context!: DialogContext + private accessor context: DialogContext = DEFAULT_DIALOG_CONTEXT - public close () { - if (!this.context) { - throw new Error('Dialog context missing') - } + private dialogTrait: DialogTrait + + constructor () { + super() + + this.dialogTrait = this.addTrait(new DialogTrait(this, { + getContext: () => this.context + })) + } - window.dispatchEvent(new CloseDialogEvent(this.context.id)) + public close (data?: unknown) { + this.dialogTrait.close(data) } protected firstUpdated () { diff --git a/src/components/dialogs-root/DialogsRoot.ts b/src/components/dialogs-root/DialogsRoot.ts index 1c468864b..931d970c6 100644 --- a/src/components/dialogs-root/DialogsRoot.ts +++ b/src/components/dialogs-root/DialogsRoot.ts @@ -24,7 +24,10 @@ export default class DialogsRoot extends WebComponent { this.addGlobalEventListener(CloseDialogEvent.eventName, (event) => { event.stopImmediatePropagation() + const dialog = this.dialogs.find(dialog => dialog.id === event.id) this.dialogs = this.dialogs.filter(dialog => dialog.id !== event.id) + + dialog?.closed(event.data) }) } diff --git a/src/components/input/Input.stories.ts b/src/components/input/Input.stories.ts new file mode 100644 index 000000000..cc5bfa265 --- /dev/null +++ b/src/components/input/Input.stories.ts @@ -0,0 +1,36 @@ +import { html } from 'lit' + +import './Input' +import { defineStoryRender } from '@/storybook' + +const meta = { + title: 'Input', + args: { + label: 'Name', + value: '', + placeholder: 'Enter your name', + type: 'text', + }, + argTypes: { + label: { control: 'text' }, + value: { control: 'text' }, + placeholder: { control: 'text' }, + type: { + control: 'select', + options: ['text', 'email', 'password', 'search', 'url'], + }, + }, +} as const + +const render = defineStoryRender(({ label, value, placeholder, type }) => html` + +`) + +export default meta + +export const Primary = { render } diff --git a/src/components/input/Input.styles.css b/src/components/input/Input.styles.css new file mode 100644 index 000000000..cd29288a8 --- /dev/null +++ b/src/components/input/Input.styles.css @@ -0,0 +1,27 @@ +:host { + display: inline-flex; + flex-direction: column; + align-items: flex-start; + gap: 5px; + + label { + color: var(--solid-ui-color-gray-600); + font-size: var(--solid-ui-font-size-sm); + font-weight: 400; + } + + .input-wrapper { + position: relative; + width: 100%; + + input { + border: 1px solid var(--solid-ui-color-gray-400); + border-radius: 5px; + padding: 10px; + width: 100%; + background: white; + color: var(--solid-ui-color-gray-700); + font-size: inherit; + } + } +} diff --git a/src/components/input/Input.ts b/src/components/input/Input.ts new file mode 100644 index 000000000..44d333f37 --- /dev/null +++ b/src/components/input/Input.ts @@ -0,0 +1,71 @@ +import { customElement, WebComponent } from '@/lib/components' +import { html } from 'lit' +import { property, query } from 'lit/decorators.js' +import InputTrait from '@/lib/components/traits/InputTrait' + +import styles from './Input.styles.css' + +@customElement('solid-ui-input') +export default class Input extends WebComponent { + static styles = styles + static formAssociated = true + + @property({ type: String, reflect: true }) + accessor label = ''; + + @property({ type: String, reflect: true }) + accessor name = ''; + + @property({ type: String }) + accessor value = ''; + + @property({ type: String, reflect: true }) + accessor type = 'text'; + + @property({ type: String, reflect: true }) + accessor placeholder = ''; + + @property({ type: Boolean, reflect: true }) + accessor required = false; + + @query('input') + private accessor inputElement: HTMLInputElement | null = null; + + private inputTrait: InputTrait + + constructor () { + super() + + this.inputTrait = this.addTrait(new InputTrait(this, { + getInputElement: () => this.inputElement, + getInternals: () => this.getInternals(), + })) + } + + protected render () { + return html` + ${this.inputTrait.renderLabel()} + +
+ this.inputTrait.onInput()} + @keydown=${this.onKeyDown} + /> +
+ ` + } + + private onKeyDown (e: KeyboardEvent) { + if (e.key === 'Enter') { + e.preventDefault() + + this.inputTrait.onSubmit() + } + } +} diff --git a/src/components/input/index.ts b/src/components/input/index.ts new file mode 100644 index 000000000..140393dde --- /dev/null +++ b/src/components/input/index.ts @@ -0,0 +1,4 @@ +import Input from './Input' + +export { Input } +export default Input diff --git a/src/components/login-button/LoginButton.ts b/src/components/login-button/LoginButton.ts index dce18cda3..9ae2234fa 100644 --- a/src/components/login-button/LoginButton.ts +++ b/src/components/login-button/LoginButton.ts @@ -22,9 +22,13 @@ export default class LoginButton extends WebComponent { ` } + click () { + this.auth.login() + } + private onClick (e: MouseEvent) { e.preventDefault() - this.auth.login() + this.click() } } diff --git a/src/components/logout-button/LogoutButton.ts b/src/components/logout-button/LogoutButton.ts index 8d66af8e4..65f7d110a 100644 --- a/src/components/logout-button/LogoutButton.ts +++ b/src/components/logout-button/LogoutButton.ts @@ -22,9 +22,13 @@ export default class LogoutButton extends WebComponent { ` } + click () { + this.auth.logout() + } + private onClick (e: MouseEvent) { e.preventDefault() - this.auth.logout() + this.click() } } diff --git a/src/components/menu-item/MenuItem.styles.css b/src/components/menu-item/MenuItem.styles.css index 17b08c56e..de38e1ed8 100644 --- a/src/components/menu-item/MenuItem.styles.css +++ b/src/components/menu-item/MenuItem.styles.css @@ -1,6 +1,7 @@ -a, button { +:host { width: 100%; display: flex; + position: relative; justify-content: flex-start; align-items: center; gap: 5px; @@ -18,4 +19,13 @@ a, button { width: var(--solid-ui-font-size-xl); height: var(--solid-ui-font-size-xl); } + + a::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } } diff --git a/src/components/menu-item/MenuItem.ts b/src/components/menu-item/MenuItem.ts index dd22453bc..6feb0f438 100644 --- a/src/components/menu-item/MenuItem.ts +++ b/src/components/menu-item/MenuItem.ts @@ -1,5 +1,5 @@ import { customElement, WebComponent } from '@/lib/components' -import { property } from 'lit/decorators.js' +import { property, query } from 'lit/decorators.js' import { html } from 'lit' import styles from './MenuItem.styles.css' @@ -8,9 +8,12 @@ import styles from './MenuItem.styles.css' export default class MenuItem extends WebComponent { static styles = styles - @property({ type: String }) + @property({ type: String, reflect: true }) accessor href: string | undefined + @query('a') + private accessor anchor: HTMLAnchorElement | null = null + render () { if (this.href) { return html` @@ -23,11 +26,13 @@ export default class MenuItem extends WebComponent { } return html` - + + + ` } + + click () { + this.anchor?.click() + } } diff --git a/src/components/menu-items/MenuItems.styles.css b/src/components/menu-items/MenuItems.styles.css deleted file mode 100644 index 31bb18294..000000000 --- a/src/components/menu-items/MenuItems.styles.css +++ /dev/null @@ -1,28 +0,0 @@ -:host { - position: fixed; - top: calc(anchor(bottom) + 5px); - right: anchor(right); - left: auto; - bottom: auto; - overflow: visible; - position-try-fallbacks: flip-block, flip-inline; - - /* Also set in JS for global tree-scoped reference */ - position-anchor: --menu-anchor; - - /* [popover] resets */ - border: none; - background: transparent; - - div { - min-width: 300px; - background: var(--solid-ui-color-slate-50); - border: 1px solid var(--solid-ui-color-slate-200); - border-radius: 5px; - box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.35); - padding: 10px 5px; - display: flex; - flex-direction: column; - gap: 5px; - } -} diff --git a/src/components/menu-items/MenuItems.ts b/src/components/menu-items/MenuItems.ts deleted file mode 100644 index cb31b610a..000000000 --- a/src/components/menu-items/MenuItems.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { customElement, WebComponent } from '@/lib/components' -import { consume } from '@lit/context' -import { html } from 'lit' -import { menuContext, MenuContext } from '@/lib/menus/context' - -import styles from './MenuItems.styles.css' - -@customElement('solid-ui-menu-items') -export default class MenuItems extends WebComponent { - static styles = styles - - @consume({ context: menuContext, subscribe: true }) - private accessor context!: MenuContext - - connectedCallback () { - super.connectedCallback() - - this.setAttribute('popover', 'auto') - this.setAttribute('role', 'menu') - - this.style.positionAnchor = '--menu-anchor' - - if (this.context) { - this.id = `${this.context.id}-menu-items` - } - } - - render () { - return html` -
- -
- ` - } -} diff --git a/src/components/menu-items/index.ts b/src/components/menu-items/index.ts deleted file mode 100644 index 7faaca136..000000000 --- a/src/components/menu-items/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import MenuItems from './MenuItems' - -export { MenuItems } -export default MenuItems diff --git a/src/components/menu/Menu.stories.ts b/src/components/menu/Menu.stories.ts index d616c0fde..04a136da1 100644 --- a/src/components/menu/Menu.stories.ts +++ b/src/components/menu/Menu.stories.ts @@ -3,7 +3,6 @@ import { defineStoryRender } from '@/storybook' import '@/components/button' import '@/components/menu-item' -import '@/components/menu-items' import './Menu' @@ -17,11 +16,9 @@ const render = defineStoryRender(() => html` Open Menu - - alert('Clicked One!')}>One - alert('Clicked Two!')}>Two - alert('Clicked Three!')}>Three - + alert('Selected One!')}>One + alert('Selected Two!')}>Two + External Link
`) diff --git a/src/components/menu/Menu.styles.css b/src/components/menu/Menu.styles.css new file mode 100644 index 000000000..3a6ec70aa --- /dev/null +++ b/src/components/menu/Menu.styles.css @@ -0,0 +1,8 @@ +wa-dropdown::part(menu) { + min-width: 300px; + box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.35); +} + +wa-dropdown-item { + padding: 0px; +} diff --git a/src/components/menu/Menu.ts b/src/components/menu/Menu.ts index c1d14070f..c8657a466 100644 --- a/src/components/menu/Menu.ts +++ b/src/components/menu/Menu.ts @@ -1,80 +1,125 @@ -import { customElement, WebComponent, generateId } from '@/lib/components' -import { provide } from '@lit/context' +import { customElement, WebComponent } from '@/lib/components' import { html } from 'lit' -import { queryAssignedElements } from 'lit/decorators.js' -import { MenuContext, menuContext } from '@/lib/menus/context' -import type MenuItems from '@/components/menu-items' +import { property, query, state } from 'lit/decorators.js' +import type WaDropdown from '@awesome.me/webawesome/dist/components/dropdown/dropdown.js' +import type WaDropdownItem from '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js' +import type { WaDropdownSelectEvent } from '@awesome.me/webawesome' + +import '@awesome.me/webawesome/dist/components/dropdown/dropdown.js' +import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js' + +import styles from './Menu.styles.css' @customElement('solid-ui-menu') export default class Menu extends WebComponent { - @queryAssignedElements({ slot: 'trigger' }) - private accessor triggers!: HTMLButtonElement[] + static styles = styles - @queryAssignedElements({ selector: '[popover]' }) - private accessor popovers!: MenuItems[] + @property({ type: String, reflect: true }) + accessor placement: WaDropdown['placement'] = 'bottom-start'; - @provide({ context: menuContext }) - // @ts-ignore - private accessor context: MenuContext = { id: generateId() } + @property({ type: Number, reflect: true }) + accessor distance: number = 0; - private triggerClickedWhilstOpen = false + @query('wa-dropdown') + private accessor dropdown: WaDropdown | null = null; - protected updated () { - const trigger = this.triggers?.[0] - const popover = this.popovers?.[0] + @state() + private accessor items: { slot: string; }[] = [] - if (!trigger || !popover) { - return - } + private observer: MutationObserver = new MutationObserver(() => this.syncItems()) + + connectedCallback () { + super.connectedCallback() - trigger.setAttribute('aria-haspopup', 'menu') - trigger.setAttribute('aria-controls', popover.id) - trigger.style.anchorName = '--menu-anchor' + this.syncItems() + this.observer.observe(this, { childList: true }) + } + + disconnectedCallback () { + super.disconnectedCallback() + + this.observer.disconnect() } protected render () { return html` - - + + + + ${this.items.map( + (item) => + html` + + ` + )} + ` } - private onTriggerPointerDown (e: PointerEvent) { - e.preventDefault() + private syncItems (): void { + const items = Array.from(this.children).filter( + (child) => !child.hasAttribute('slot') + ) + + this.items = items.map((item, index) => { + const slotName = `menu-item-${index}` - this.triggerClickedWhilstOpen = this.popovers?.[0]?.matches(':popover-open') ?? false + if (item.getAttribute('slot') !== slotName) { + item.setAttribute('slot', slotName) + } + + return { slot: slotName } + }) } - private onTriggerClick (e: MouseEvent) { - e.preventDefault() + private onItemClick (event: Event) { + const waItem = event.currentTarget as WaDropdownItem - if (this.triggerClickedWhilstOpen) { - this.triggerClickedWhilstOpen = false + event.stopPropagation() + if (waItem.disabled) { return } - this.popovers?.[0]?.togglePopover() + const selectedEvent = this.dispatchSelectEvent(waItem) + + if (selectedEvent.defaultPrevented || !this.dropdown) { + return + } + + this.dropdown.open = false } - private onMenuClick (e: MouseEvent) { - const target = e.target - const targetIsClickable = - target instanceof HTMLElement && ( - target.tagName === 'BUTTON' || - target.tagName === 'A' || - target.tagName === 'SOLID-UI-MENU-ITEM' || - target.closest('button, a, [role="menuitem"]') - ) - - if (!targetIsClickable) { + private onWaSelect (event: WaDropdownSelectEvent) { + const selectedEvent = this.dispatchSelectEvent(event.detail.item) + + if (selectedEvent.defaultPrevented) { + event.preventDefault() + return } - this.popovers?.[0]?.hidePopover() + const slotName = event.detail.item.children[0].getAttribute('name') + const item = this.querySelector<{ click?: () => void } & Element>(`[slot="${slotName}"]`) + + item?.click?.() + } + + private dispatchSelectEvent (waItem: WaDropdownItem) { + const slotName = waItem.children[0].getAttribute('name') + const item = this.querySelector(`[slot="${slotName}"]`) + const event = new CustomEvent('solid-ui-select', { + bubbles: true, + composed: true, + cancelable: true, + }) + + item?.dispatchEvent(event) + + return event } } diff --git a/src/components/select-option/SelectOption.ts b/src/components/select-option/SelectOption.ts new file mode 100644 index 000000000..de7de683c --- /dev/null +++ b/src/components/select-option/SelectOption.ts @@ -0,0 +1,13 @@ +import { customElement, WebComponent } from '@/lib/components' +import { html, nothing } from 'lit' +import { property } from 'lit/decorators.js' + +@customElement('solid-ui-select-option') +export default class SelectOption extends WebComponent { + @property({ type: String, reflect: true }) + accessor value = '' + + protected render () { + return html`${nothing}` + } +} diff --git a/src/components/select-option/index.ts b/src/components/select-option/index.ts new file mode 100644 index 000000000..b8172ad87 --- /dev/null +++ b/src/components/select-option/index.ts @@ -0,0 +1,4 @@ +import SelectOption from './SelectOption' + +export { SelectOption } +export default SelectOption diff --git a/src/components/select/Select.stories.ts b/src/components/select/Select.stories.ts new file mode 100644 index 000000000..58a55d2c4 --- /dev/null +++ b/src/components/select/Select.stories.ts @@ -0,0 +1,32 @@ +import { html } from 'lit' +import { defineStoryRender } from '@/storybook' + +import '@/components/select-option' + +import './Select' + +const meta = { + title: 'Select', + args: { + label: 'What is the best food?', + options: 'Pizza, Ramen, Tacos' + }, + argTypes: { + label: { control: 'text' }, + options: { control: 'text' }, + }, +} as const + +const render = defineStoryRender(({ label, options }) => { + const parsedOptions = options.split(',').map(option => option.trim()) + + return html` + + ${parsedOptions.map(option => html`${option}`)} + + ` +}) + +export default meta + +export const Primary = { render } diff --git a/src/components/select/Select.styles.css b/src/components/select/Select.styles.css new file mode 100644 index 000000000..4e409db14 --- /dev/null +++ b/src/components/select/Select.styles.css @@ -0,0 +1,41 @@ +:host { + display: inline-flex; + flex-direction: column; + align-items: flex-start; + gap: 5px; + + label { + color: var(--solid-ui-color-gray-600); + font-size: var(--solid-ui-font-size-sm); + font-weight: 400; + } + + .input-wrapper { + position: relative; + width: 100%; + + select { + appearance: none; + border: 1px solid var(--solid-ui-color-gray-400); + border-radius: 5px; + padding: 10px; + padding-right: 40px; + width: 100%; + background: white; + color: var(--solid-ui-color-gray-700); + font-size: inherit; + cursor: pointer; + } + + icon-lucide-chevron-down { + position: absolute; + right: 15px; + top: 50%; + transform: translateY(-50%); + color: var(--solid-ui-color-gray-500); + width: var(--solid-ui-font-size-lg); + height: var(--solid-ui-font-size-lg); + pointer-events: none; + } + } +} diff --git a/src/components/select/Select.ts b/src/components/select/Select.ts new file mode 100644 index 000000000..4aacf4cb7 --- /dev/null +++ b/src/components/select/Select.ts @@ -0,0 +1,78 @@ +import { customElement, WebComponent } from '@/lib/components' +import { html } from 'lit' +import { property, query } from 'lit/decorators.js' +import InputTrait from '@/lib/components/traits/InputTrait' +import type SelectOption from '@/components/select-option/SelectOption' + +import '~icons/lucide/chevron-down' + +import styles from './Select.styles.css' + +@customElement('solid-ui-select') +export default class Select extends WebComponent { + static styles = styles + static formAssociated = true + + @property({ type: String, reflect: true }) + accessor label = ''; + + @property({ type: String, reflect: true }) + accessor name = ''; + + @property({ type: String }) + accessor value = ''; + + @property({ type: Boolean, reflect: true }) + accessor required = false; + + @query('select') + accessor inputElement: HTMLSelectElement | null = null; + + private inputTrait: InputTrait + + constructor () { + super() + + this.inputTrait = this.addTrait(new InputTrait(this, { + getInputElement: () => this.inputElement, + getInternals: () => this.getInternals(), + })) + } + + protected render () { + return html` + ${this.inputTrait.renderLabel()} + +
+ + +
+ ` + } + + private getOptions (): { value: string; label: string }[] { + const options = this.querySelectorAll( + 'solid-ui-select-option' + ) + + return Array.from(options).map((option) => ({ + value: option.value, + label: option.textContent, + })) + } +} diff --git a/src/components/select/index.ts b/src/components/select/index.ts index 16b000c48..d8e6efe32 100644 --- a/src/components/select/index.ts +++ b/src/components/select/index.ts @@ -1 +1,4 @@ -export * from '../../v2/components/forms/select' +import Select from './Select' + +export { Select } +export default Select diff --git a/src/components/signup-button/SignupButton.ts b/src/components/signup-button/SignupButton.ts index e51069a5e..1155f9fa4 100644 --- a/src/components/signup-button/SignupButton.ts +++ b/src/components/signup-button/SignupButton.ts @@ -22,9 +22,13 @@ export default class SignupButton extends WebComponent { ` } + click () { + this.auth.signup() + } + private onClick (e: MouseEvent) { e.preventDefault() - this.auth.signup() + this.click() } } diff --git a/src/index.ts b/src/index.ts index 4449133b0..537060d84 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,4 +66,3 @@ export type { CreateContext, NewAppInstanceOptions } from './create/types' export * from './lib/auth' export * from './lib/components' export * from './lib/dialogs' -export * from './lib/menus' diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts index bfaab7422..807b1e89b 100644 --- a/src/lib/components/index.ts +++ b/src/lib/components/index.ts @@ -1,3 +1,4 @@ export * from './decorators' export * from './ids' export * from './web-component' +export * from './traits' diff --git a/src/lib/components/traits/DialogTrait.ts b/src/lib/components/traits/DialogTrait.ts new file mode 100644 index 000000000..752feca92 --- /dev/null +++ b/src/lib/components/traits/DialogTrait.ts @@ -0,0 +1,23 @@ +import { WebComponent } from '@/lib/components' +import { CloseDialogEvent } from '@/lib/dialogs/events/close-dialog' +import type { DialogContext } from '@/lib/dialogs/context' + +import { WebComponentTrait } from './WebComponentTrait' + +export interface DialogTraitConfig { + getContext: () => DialogContext +} + +export default class DialogTrait implements WebComponentTrait { + public target: WebComponent + private config: DialogTraitConfig + + constructor (target: WebComponent, config: DialogTraitConfig) { + this.target = target + this.config = config + } + + close (data?: T) { + window.dispatchEvent(new CloseDialogEvent(this.config.getContext().id, data)) + } +} diff --git a/src/lib/components/traits/InputTrait.ts b/src/lib/components/traits/InputTrait.ts new file mode 100644 index 000000000..73a5374cd --- /dev/null +++ b/src/lib/components/traits/InputTrait.ts @@ -0,0 +1,85 @@ +import { WebComponent } from '@/lib/components' +import { html, nothing } from 'lit' + +import { generateId } from '@/lib/components' + +import type { WebComponentTrait } from './WebComponentTrait' + +export type InputTraitTarget = WebComponent & { + name: string; + label: string; + required: boolean; + value: string; +} + +export interface InputTraitConfig { + getInputElement(): HTMLInputElement | HTMLSelectElement | null + getInternals(): ElementInternals + onValueChanged?: (value: string) => void +} + +export default class InputTrait implements WebComponentTrait { + public inputId: string + public target: InputTraitTarget + private config: InputTraitConfig + + constructor (target: InputTraitTarget, config: InputTraitConfig) { + this.config = config + this.inputId = `input-${generateId()}` + this.target = target + } + + firstUpdated () { + this.config.getInternals().setFormValue(this.target.value) + this.updateValidity() + } + + updated (changedProperties: Map) { + if (changedProperties.has('value') || changedProperties.has('required')) { + this.updateValidity() + } + } + + formResetCallback () { + this.target.value = '' + this.config.getInternals().setFormValue('') + this.updateValidity() + this.config.onValueChanged?.('') + } + + renderLabel () { + return this.target.label + ? html`` + : nothing + } + + onInput () { + this.setValue(this.config.getInputElement()?.value ?? '') + } + + onSubmit () { + this.config.getInternals().form?.requestSubmit() + } + + setValue (value: string) { + this.target.value = value + + this.config.getInternals().setFormValue(this.target.value) + this.target.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })) + this.config.onValueChanged?.(this.target.value) + } + + private updateValidity () { + const internals = this.config.getInternals() + + if (this.target.required && this.target.value === '') { + internals.setValidity( + { valueMissing: true }, + 'Please fill out this field.', + this.config.getInputElement() ?? undefined + ) + } else { + internals.setValidity({}) + } + } +} diff --git a/src/lib/components/traits/WebComponentTrait.ts b/src/lib/components/traits/WebComponentTrait.ts new file mode 100644 index 000000000..6e3fafb3e --- /dev/null +++ b/src/lib/components/traits/WebComponentTrait.ts @@ -0,0 +1,12 @@ +import { WebComponent } from '@/lib/components' + +export type WebComponentTraitMethodKey = NonNullable<{ + [K in keyof WebComponentTrait]: NonNullable extends (...args: any[]) => any ? K : never +}[keyof WebComponentTrait]> + +export interface WebComponentTrait { + target: WebComponent + firstUpdated?(): void; + updated?(changedProperties: Map): void; + formResetCallback?(): void; +} diff --git a/src/lib/components/traits/index.ts b/src/lib/components/traits/index.ts new file mode 100644 index 000000000..cc3226a1f --- /dev/null +++ b/src/lib/components/traits/index.ts @@ -0,0 +1,6 @@ +export * from './DialogTrait' +export * from './InputTrait' +export * from './WebComponentTrait' + +export { default as DialogTrait } from './DialogTrait' +export { default as InputTrait } from './InputTrait' diff --git a/src/lib/components/web-component/WebComponent.ts b/src/lib/components/web-component/WebComponent.ts index 407755ae4..8539ab73b 100644 --- a/src/lib/components/web-component/WebComponent.ts +++ b/src/lib/components/web-component/WebComponent.ts @@ -1,4 +1,5 @@ import { html, LitElement, type CSSResultGroup } from 'lit' +import type { WebComponentTrait, WebComponentTraitMethodKey } from '@/lib/components/traits/WebComponentTrait' import styles from './WebComponent.styles.css' @@ -13,6 +14,7 @@ export default abstract class WebComponent extends LitElement { protected internals?: ElementInternals protected globalListeners: [type: string, listener: EventListener][] = [] + protected traits: WebComponentTrait[] = [] disconnectedCallback (): void { super.disconnectedCallback() @@ -24,6 +26,24 @@ export default abstract class WebComponent extends LitElement { this.globalListeners = [] } + protected addTrait(trait: T): T { + this.traits.push(trait) + + return trait + } + + protected firstUpdated () { + this.forwardMethodCall('firstUpdated') + } + + protected updated (changedProperties: Map) { + this.forwardMethodCall('updated', changedProperties) + } + + protected formResetCallback () { + this.forwardMethodCall('formResetCallback') + } + protected willUpdate (changedProperties: Map) { super.willUpdate(changedProperties) @@ -65,4 +85,10 @@ export default abstract class WebComponent extends LitElement { private static (): typeof WebComponent { return this.constructor as typeof WebComponent } + + private forwardMethodCall(method: T, ...args: Parameters[T]>) { + for (const trait of this.traits) { + (trait[method] as Function | undefined)?.(...args) + } + } } diff --git a/src/lib/dialogs/Dialog.ts b/src/lib/dialogs/Dialog.ts index 85e30b181..6c3471530 100644 --- a/src/lib/dialogs/Dialog.ts +++ b/src/lib/dialogs/Dialog.ts @@ -1,15 +1,21 @@ import { TemplateResult } from 'lit' import { generateId } from '@/lib/components' +export interface DialogConfig { + onClose?(data?: unknown): void +} + export default class Dialog { public readonly id: string public readonly template: TemplateResult public readonly element: Promise + public readonly config: DialogConfig private _resolveElement!: (element: HTMLElement) => void - constructor (template: TemplateResult) { + constructor (template: TemplateResult, config: DialogConfig = {}) { this.id = generateId() this.template = template + this.config = config this.element = new Promise(resolve => { this._resolveElement = resolve }) @@ -18,4 +24,8 @@ export default class Dialog { setElement (element: HTMLElement) { this._resolveElement(element) } + + closed (data?: unknown) { + this.config.onClose?.(data) + } } diff --git a/src/lib/dialogs/context.ts b/src/lib/dialogs/context.ts index 5b10f53f3..878dd351d 100644 --- a/src/lib/dialogs/context.ts +++ b/src/lib/dialogs/context.ts @@ -1,7 +1,9 @@ import { createContext } from '@lit/context' +import { generateId } from '@/lib/components/ids' export interface DialogContext { readonly id: string } +export const DEFAULT_DIALOG_CONTEXT = { id: `noop-${generateId()}` } satisfies DialogContext export const dialogContext = createContext(Symbol('dialog')) diff --git a/src/lib/dialogs/events/close-dialog.ts b/src/lib/dialogs/events/close-dialog.ts index 07ff43168..2ba4ae28d 100644 --- a/src/lib/dialogs/events/close-dialog.ts +++ b/src/lib/dialogs/events/close-dialog.ts @@ -3,7 +3,7 @@ const EVENT_NAME = 'solid-ui:close-dialog' as const export class CloseDialogEvent extends Event { static readonly eventName = EVENT_NAME - constructor (public id: string) { + constructor (public id: string, public data?: unknown) { super(CloseDialogEvent.eventName, { bubbles: true, composed: true }) } } diff --git a/src/lib/dialogs/helpers.ts b/src/lib/dialogs/helpers.ts index a0e7dc1f4..b2b7211e5 100644 --- a/src/lib/dialogs/helpers.ts +++ b/src/lib/dialogs/helpers.ts @@ -1,9 +1,9 @@ import { TemplateResult } from 'lit' import { ShowDialogEvent } from './events/show-dialog' -import Dialog from './Dialog' +import Dialog, { DialogConfig } from './Dialog' -export function showDialog (template: TemplateResult): Promise { - const dialog = new Dialog(template) +export function showDialog (template: TemplateResult, config: DialogConfig = {}): Promise { + const dialog = new Dialog(template, config) document.dispatchEvent(new ShowDialogEvent(dialog)) diff --git a/src/lib/menus/context.ts b/src/lib/menus/context.ts deleted file mode 100644 index 26ceb0109..000000000 --- a/src/lib/menus/context.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createContext } from '@lit/context' - -export interface MenuContext { - readonly id: string -} - -export const menuContext = createContext(Symbol('menu')) diff --git a/src/lib/menus/index.ts b/src/lib/menus/index.ts deleted file mode 100644 index 630b4d7e7..000000000 --- a/src/lib/menus/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './context' diff --git a/src/styles/theme.css b/src/styles/theme.css index 1382bd5ac..5ebb46b1f 100644 --- a/src/styles/theme.css +++ b/src/styles/theme.css @@ -1,3 +1,5 @@ +@import "@awesome.me/webawesome/dist/styles/webawesome.css"; + :root { --solid-ui-color-primary: #7c4dff; --solid-ui-color-tertiary: #083575; diff --git a/src/types/declarations.d.ts b/src/types/shims.d.ts similarity index 100% rename from src/types/declarations.d.ts rename to src/types/shims.d.ts diff --git a/src/types/webawesome.d.ts b/src/types/webawesome.d.ts new file mode 100644 index 000000000..038f81fbf --- /dev/null +++ b/src/types/webawesome.d.ts @@ -0,0 +1,9 @@ +declare module '@awesome.me/webawesome' { + import type WaDropdownItem from '@awesome.me/webawesome/dist/components/dropdown/dropdown-item.js' + + export interface WaDropdownSelectEvent extends Event { + detail: { + item: WaDropdownItem; + }; + } +} diff --git a/test/helpers/setup.ts b/test/helpers/setup.ts index 7452da210..767ef80e4 100644 --- a/test/helpers/setup.ts +++ b/test/helpers/setup.ts @@ -1,5 +1,7 @@ import '@/types/env.d.ts' import '@/types/polyfills.d.ts' +import '@/types/shims.d.ts' +import '@/types/webawesome.d.ts' import { expect, vi } from 'vitest' import { toContainGraph } from '../custom-matchers/toContainGraph' diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index 31af3a3f3..067545236 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -10,8 +10,11 @@ describe('Index', () => { 'Account', 'CloseDialogEvent', 'DEFAULT_AUTH_CONTEXT', + 'DEFAULT_DIALOG_CONTEXT', 'DEFAULT_SIGNUP_URL', 'Dialog', + 'DialogTrait', + 'InputTrait', 'NoopAuth', 'ShowDialogEvent', 'SolidAuth', @@ -34,7 +37,6 @@ describe('Index', () => { 'login', 'matrix', 'media', - 'menuContext', 'messageArea', 'ns', 'pad',