diff --git a/.storybook/index.css b/.storybook/index.css index 0929232a..f76c9b11 100644 --- a/.storybook/index.css +++ b/.storybook/index.css @@ -7,10 +7,18 @@ html, body, #root, .App { - font-family: 'Inter', sans-serif; height: 100%; } +@supports (font-variation-settings: normal) { + html, + body, + #root, + .App { + font-family: 'Inter var', sans-serif; + } +} + /* Storybook styles */ .sb-show-main { diff --git a/.storybook/utils.tsx b/.storybook/utils.tsx new file mode 100644 index 00000000..d8d62de3 --- /dev/null +++ b/.storybook/utils.tsx @@ -0,0 +1,35 @@ +import { createUseStyles } from 'react-jss' +import React from 'react' +import { Story } from '@storybook/react/types-6-0' +import { styleguide, themes, ThemeType } from '../src/components/assets/styles' + +const { spacing } = styleguide + +const { dark, light } = ThemeType + +const useStyles = createUseStyles({ + decorator: { + background: themes[light].background.secondary, + height: `calc(100vh - ${spacing.m * 2}px)`, + padding: spacing.l, + width: '100%' + }, + // eslint-disable-next-line sort-keys + '@global': { + [`.${dark}`]: { + '& $decorator': { + background: themes[dark].background.secondary + } + } + } +}) + +export const SecondaryBgDecorator = (CompStory: Story) => { + const classes = useStyles() + + return ( +
+ +
+ ) +} diff --git a/package-lock.json b/package-lock.json index f0772255..7b626304 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@dassana-io/web-components", - "version": "0.8.0", + "version": "0.8.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1416,9 +1416,9 @@ "integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==" }, "@dassana-io/web-utils": { - "version": "0.7.2", - "resolved": "https://npm.pkg.github.com/download/@dassana-io/web-utils/0.7.2/411c8a46f25abbc037a133a1ceffa8c421580dcd36976627cefb9ff85262d7bf", - "integrity": "sha512-5+0sTM60qHtXH4s9jsS7fuFrtRlxw2wsKjw1WF6fd5ok0fdVjCWPhJPIplPHUmkZ6TRPxqOWqh+WsCQ0JySnuA==", + "version": "0.7.4", + "resolved": "https://npm.pkg.github.com/download/@dassana-io/web-utils/0.7.4/d1d45a2810ffe01b36f680c84369bd78932933aba7d0e030478c4bbd86c3680c", + "integrity": "sha512-NjwIrH84aEnev7KDea47T4dniQK4lJOXW52FzSWUOy8Y8od1dTLNShIU6nWrB3yCSCkavFDA4WtASY5bReu+Og==", "requires": { "@testing-library/react-hooks": "^3.4.1", "@types/jest": "^24.9.1", @@ -1446,9 +1446,9 @@ } }, "@types/node": { - "version": "12.19.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.5.tgz", - "integrity": "sha512-Wgdl27uw/jUYUFyajUGKSjDNGxmJrZi9sjeG6UJImgUtKbJoO9aldx+1XODN1EpNDX9DirvbvHHmTsNlb8GwMA==" + "version": "12.19.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.12.tgz", + "integrity": "sha512-UwfL2uIU9arX/+/PRcIkT08/iBadGN2z6ExOROA2Dh5mAuWTBj6iJbQX4nekiV5H8cTrEG569LeX+HRco9Cbxw==" }, "axios": { "version": "0.20.0", @@ -1459,9 +1459,9 @@ } }, "prettier": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz", - "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==" + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", + "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==" } } }, @@ -1603,9 +1603,9 @@ } }, "@fortawesome/react-fontawesome": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.13.tgz", - "integrity": "sha512-/HrLnIft5Ks2511Pz6TxHBIctC9QalVscAC64sufQ4sJH/sXaQlG3uR9LCu6VpEwkBemgcBLrz/QPNP/ddbjDg==", + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.14.tgz", + "integrity": "sha512-4wqNb0gRLVaBm/h+lGe8UfPPivcbuJ6ecI4hIgW0LjI7kzpYB9FkN0L9apbVzg+lsBdcTf0AlBtODjcSX5mmKA==", "requires": { "prop-types": "^15.7.2" } @@ -2417,13 +2417,13 @@ } }, "@rollup/plugin-image": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@rollup/plugin-image/-/plugin-image-2.0.5.tgz", - "integrity": "sha512-R+yGLJjLN1won2JlZPZmdlyZGLZwwsW8V/RYu3mTcRq8Aqd9GC2fo4Zi892bFteM5LolfbpxK8Y9QQcAhbBwSQ==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@rollup/plugin-image/-/plugin-image-2.0.6.tgz", + "integrity": "sha512-bB+spXogbPiFjhBS7i8ajUOgOnVwWK3bnJ6VroxKey/q8/EPRkoSh+4O1qPCw97qMIDspF4TlzXVBhZ7nojIPw==", "dev": true, "requires": { - "@rollup/pluginutils": "^3.0.4", - "mini-svg-data-uri": "^1.1.3" + "@rollup/pluginutils": "^3.1.0", + "mini-svg-data-uri": "^1.2.3" } }, "@rollup/plugin-node-resolve": { @@ -13734,6 +13734,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz", "integrity": "sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==", + "dev": true, "requires": { "es-abstract": "^1.17.4", "has-symbols": "^1.0.1", @@ -13747,7 +13748,8 @@ "isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true } } }, @@ -15000,9 +15002,9 @@ } }, "framer-motion": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-2.9.4.tgz", - "integrity": "sha512-Bvgdwpu5UO6VnEEwenJEmnGeo9ILRRWh9f3iIX+71NiM5X60Qi6KlkBFGZc9DGbdIUAn0AYgaxVhTKL39OOYng==", + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-2.9.5.tgz", + "integrity": "sha512-epSX4Co1YbDv0mjfHouuY0q361TpHE7WQzCp/xMTilxy4kXd+Z23uJzPVorfzbm1a/9q1Yu8T5bndaw65NI4Tg==", "requires": { "@emotion/is-prop-valid": "^0.8.2", "framesync": "^4.1.0", @@ -15218,9 +15220,9 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-intrinsic": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", - "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.2.tgz", + "integrity": "sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==", "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -16253,9 +16255,9 @@ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" }, "is-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.0.tgz", - "integrity": "sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.1.tgz", + "integrity": "sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg==" }, "is-binary-path": { "version": "2.1.0", @@ -16268,7 +16270,8 @@ "is-boolean-object": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.1.tgz", - "integrity": "sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==" + "integrity": "sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==", + "dev": true }, "is-buffer": { "version": "1.1.6", @@ -16571,14 +16574,60 @@ } }, "is-typed-array": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.3.tgz", - "integrity": "sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.4.tgz", + "integrity": "sha512-ILaRgn4zaSrVNXNGtON6iFNotXW3hAPF3+0fB1usg2jFlWqo5fEDdmJkz0zBfoi7Dgskr8Khi2xZ8cXqZEfXNA==", "requires": { - "available-typed-arrays": "^1.0.0", - "es-abstract": "^1.17.4", + "available-typed-arrays": "^1.0.2", + "call-bind": "^1.0.0", + "es-abstract": "^1.18.0-next.1", "foreach": "^2.0.5", "has-symbols": "^1.0.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + } } }, "is-typedarray": { @@ -17792,19 +17841,20 @@ }, "dependencies": { "deep-equal": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.4.tgz", - "integrity": "sha512-BUfaXrVoCfgkOQY/b09QdO9L3XNoF2XH0A3aY9IQwQL/ZjLOe8FQgCNVl1wiolhsFo8kFdO9zdPViCPbmaJA5w==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.5.tgz", + "integrity": "sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw==", "requires": { - "es-abstract": "^1.18.0-next.1", - "es-get-iterator": "^1.1.0", + "call-bind": "^1.0.0", + "es-get-iterator": "^1.1.1", + "get-intrinsic": "^1.0.1", "is-arguments": "^1.0.4", "is-date-object": "^1.0.2", "is-regex": "^1.1.1", "isarray": "^2.0.5", - "object-is": "^1.1.3", + "object-is": "^1.1.4", "object-keys": "^1.1.1", - "object.assign": "^4.1.1", + "object.assign": "^4.1.2", "regexp.prototype.flags": "^1.3.0", "side-channel": "^1.0.3", "which-boxed-primitive": "^1.0.1", @@ -17812,30 +17862,21 @@ "which-typed-array": "^1.1.2" } }, - "es-abstract": { - "version": "1.18.0-next.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", - "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "es-get-iterator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.1.tgz", + "integrity": "sha512-qorBw8Y7B15DVLaJWy6WdEV/ZkieBcu6QCq/xzWzGOKJqgG1j754vXRfZ3NY7HSShneqU43mPB4OkQBTkvHhFw==", "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.1", "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-negative-zero": "^2.0.0", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" + "is-arguments": "^1.0.4", + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-string": "^1.0.5", + "isarray": "^2.0.5" } }, - "is-callable": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", - "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" - }, "is-regex": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", @@ -17850,12 +17891,12 @@ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" }, "object-is": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.3.tgz", - "integrity": "sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.4.tgz", + "integrity": "sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg==", "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1" + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" } }, "object.assign": { @@ -19803,9 +19844,9 @@ "dev": true }, "npm": { - "version": "6.14.8", - "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.8.tgz", - "integrity": "sha512-HBZVBMYs5blsj94GTeQZel7s9odVuuSUHy1+AlZh7rPVux1os2ashvEGLy/STNK7vUjbrCg5Kq9/GXisJgdf6A==", + "version": "6.14.11", + "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.11.tgz", + "integrity": "sha512-1Zh7LjuIoEhIyjkBflSSGzfjuPQwDlghNloppjruOH5bmj9midT9qcNT0tRUZRR04shU9ekrxNy9+UTBrqeBpQ==", "requires": { "JSONStream": "^1.3.5", "abbrev": "~1.1.1", @@ -19844,7 +19885,7 @@ "infer-owner": "^1.0.4", "inflight": "~1.0.6", "inherits": "^2.0.4", - "ini": "^1.3.5", + "ini": "^1.3.8", "init-package-json": "^1.10.3", "is-cidr": "^3.0.0", "json-parse-better-errors": "^1.0.2", @@ -19887,10 +19928,10 @@ "npm-pick-manifest": "^3.0.2", "npm-profile": "^4.0.4", "npm-registry-fetch": "^4.0.7", - "npm-user-validate": "~1.0.0", + "npm-user-validate": "^1.0.1", "npmlog": "~4.1.2", "once": "~1.4.0", - "opener": "^1.5.1", + "opener": "^1.5.2", "osenv": "^0.1.5", "pacote": "^9.5.12", "path-is-inside": "~1.0.2", @@ -19962,17 +20003,6 @@ "humanize-ms": "^1.2.1" } }, - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - } - }, "ansi-align": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", @@ -20303,11 +20333,6 @@ "mkdirp": "~0.5.0" } }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -20756,11 +20781,6 @@ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" - }, "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", @@ -21081,12 +21101,31 @@ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", - "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", + "version": "5.1.5", + "bundled": true, "requires": { - "ajv": "^5.3.0", + "ajv": "^6.12.3", "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "bundled": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "bundled": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "bundled": true + } } }, "has": { @@ -21210,8 +21249,7 @@ }, "ini": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + "bundled": true }, "init-package-json": { "version": "1.10.3", @@ -21373,11 +21411,6 @@ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" - }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -22019,6 +22052,10 @@ "path-key": "^2.0.0" } }, + "npm-user-validate": { + "version": "1.0.1", + "bundled": true + }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -22068,9 +22105,8 @@ } }, "opener": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz", - "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==" + "version": "1.5.2", + "bundled": true }, "os-homedir": { "version": "1.0.2", @@ -23019,6 +23055,19 @@ "xdg-basedir": "^3.0.0" } }, + "uri-js": { + "version": "4.4.0", + "bundled": true, + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "bundled": true + } + } + }, "url-parse-lax": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", @@ -23321,11 +23370,6 @@ "path-key": "^2.0.0" } }, - "npm-user-validate": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-user-validate/-/npm-user-validate-1.0.1.tgz", - "integrity": "sha512-uQwcd/tY+h1jnEaze6cdX/LrhWhoBxfSknxentoqmIuStxUExxjWd3ULMLFPiFUrZKbOVMowH6Jq2FRWfmhcEw==" - }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -28974,9 +29018,9 @@ } }, "style-value-types": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-3.1.9.tgz", - "integrity": "sha512-050uqgB7WdvtgacoQKm+4EgKzJExVq0sieKBQQtJiU3Muh6MYcCp4T3M8+dfl6VOF2LR0NNwXBP1QYEed8DfIw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-3.2.0.tgz", + "integrity": "sha512-ih0mGsrYYmVvdDi++/66O6BaQPRPRMQHoZevNNdMMcPlP/cH28Rnfsqf1UEba/Bwfuw9T8BmIMwbGdzsPwQKrQ==", "requires": { "hey-listen": "^1.0.8", "tslib": "^1.10.0" @@ -30071,9 +30115,9 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", - "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==" + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, "v8-compile-cache": { "version": "2.1.1", @@ -30819,15 +30863,25 @@ } }, "which-boxed-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz", - "integrity": "sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", "requires": { - "is-bigint": "^1.0.0", - "is-boolean-object": "^1.0.0", - "is-number-object": "^1.0.3", - "is-string": "^1.0.4", - "is-symbol": "^1.0.2" + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "dependencies": { + "is-boolean-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz", + "integrity": "sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==", + "requires": { + "call-bind": "^1.0.0" + } + } } }, "which-collection": { @@ -30847,16 +30901,62 @@ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, "which-typed-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.2.tgz", - "integrity": "sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.4.tgz", + "integrity": "sha512-49E0SpUe90cjpoc7BOJwyPHRqSAd12c10Qm2amdEZrJPCY2NDxaW01zHITrem+rnETY3dwrbH3UUrUwagfCYDA==", "requires": { "available-typed-arrays": "^1.0.2", - "es-abstract": "^1.17.5", + "call-bind": "^1.0.0", + "es-abstract": "^1.18.0-next.1", "foreach": "^2.0.5", "function-bind": "^1.1.1", "has-symbols": "^1.0.1", "is-typed-array": "^1.1.3" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + } } }, "wide-align": { diff --git a/package.json b/package.json index c4ed6e0a..70eebf37 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dassana-io/web-components", - "version": "0.8.0", + "version": "0.8.1", "publishConfig": { "registry": "https://npm.pkg.github.com/dassana-io" }, @@ -11,10 +11,10 @@ }, "dependencies": { "@ant-design/icons": "^4.2.2", - "@dassana-io/web-utils": "^0.7.2", + "@dassana-io/web-utils": "^0.7.4", "@fortawesome/fontawesome-svg-core": "^1.2.32", "@fortawesome/free-solid-svg-icons": "^5.15.1", - "@fortawesome/react-fontawesome": "^0.1.13", + "@fortawesome/react-fontawesome": "^0.1.14", "@storybook/addon-cssresources": "^6.0.28", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.5.0", @@ -28,7 +28,7 @@ "bytes": "^3.1.0", "classnames": "^2.2.6", "color": "^3.1.3", - "framer-motion": "^2.9.4", + "framer-motion": "^2.9.5", "fuse.js": "^6.4.2", "lodash": "^4.17.20", "moment": "^2.29.1", @@ -38,7 +38,7 @@ "react-jss": "^10.4.0", "react-scripts": "^3.4.4", "typescript": "^3.9.7", - "uuid": "^8.3.1" + "uuid": "^8.3.2" }, "scripts": { "start": "npm run storybook", @@ -78,7 +78,7 @@ }, "devDependencies": { "@rollup/plugin-commonjs": "^15.1.0", - "@rollup/plugin-image": "^2.0.5", + "@rollup/plugin-image": "^2.0.6", "@rollup/plugin-node-resolve": "^9.0.0", "@storybook/addon-actions": "^6.0.28", "@storybook/addon-essentials": "^6.0.28", diff --git a/rollup.config.ts b/rollup.config.ts index c6955ad9..eec28ca5 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -30,9 +30,11 @@ const rootImport = options => ({ export default { external: [ 'antd', + '@ant-design/icons', '@fortawesome/fontawesome-svg-core', '@fortawesome/free-solid-svg-icons', '@fortawesome/react-fontawesome', + 'fuse.js', 'react', 'uuid' ], diff --git a/src/__snapshots__/storybook.test.ts.snap b/src/__snapshots__/storybook.test.ts.snap index 4fcfc3fd..cbdded40 100644 --- a/src/__snapshots__/storybook.test.ts.snap +++ b/src/__snapshots__/storybook.test.ts.snap @@ -113,14 +113,8 @@ exports[`Storyshots Button Pending 1`] = ` >
+ +
+`; + +exports[`Storyshots Checkbox Default Checked 1`] = ` +
+ +
+`; + +exports[`Storyshots Checkbox Disabled 1`] = ` +
+ +
+`; + +exports[`Storyshots Checkbox Disabled Checked 1`] = ` +
+ +
+`; + +exports[`Storyshots Checkbox Label 1`] = ` +
+ +
+`; + exports[`Storyshots ColoredDot Colored 1`] = `

Hover over colored dot to show tooltip.

@@ -234,15 +376,15 @@ exports[`Storyshots IconButton Default 1`] = ` className="light storyWrapper-0-1-2" >
+ +
+`; + exports[`Storyshots Input Error 1`] = `
  @@ -340,7 +501,7 @@ exports[`Storyshots Input Placeholder 1`] = ` className="light storyWrapper-0-1-2" >
 
 
  @@ -839,92 +1002,95 @@ exports[`Storyshots Select Default 1`] = ` className="light storyWrapper-0-1-2" >
- - + - + type="search" + unselectable="on" + value="" + /> + + + + +
- - -
- - - + + - +
@@ -935,97 +1101,104 @@ exports[`Storyshots Select Full Width 1`] = ` className="light storyWrapper-0-1-2" >
- - + - - -
+ + - - Lorem - -
-
-
- +
+ + Lorem + +
+
+
- + + - +
@@ -1036,109 +1209,116 @@ exports[`Storyshots Select Icon 1`] = ` className="light storyWrapper-0-1-2" >
- - + - - -
+ + - - - aws.svg - - - - AWS - -
-
-
- + + aws.svg + + + + AWS + +
+ +
- + + - +
@@ -1149,92 +1329,194 @@ exports[`Storyshots Select Search 1`] = ` className="light storyWrapper-0-1-2" >
- - + - + type="search" + unselectable={null} + value="" + /> + + + Choose a Cloud Provider + +
- + + +
- +
+ +`; + +exports[`Storyshots Select Selected Option Width 1`] = ` +
+
+
+
- - + + + Choose a Cloud Provider + +
+ + + + - +
@@ -1245,7 +1527,7 @@ exports[`Storyshots Skeleton Circle 1`] = ` className="light storyWrapper-0-1-2" >   @@ -1257,27 +1539,27 @@ exports[`Storyshots Skeleton Count 1`] = ` className="light storyWrapper-0-1-2" >           @@ -1289,7 +1571,7 @@ exports[`Storyshots Skeleton Default 1`] = ` className="light storyWrapper-0-1-2" >   @@ -1309,23 +1591,28 @@ exports[`Storyshots Tag Colored 1`] = ` } > Colored -
- -`; - -exports[`Storyshots Tag Colored Preset 1`] = ` -
- - Blue + + +
`; @@ -1343,6 +1630,28 @@ exports[`Storyshots Tag Default 1`] = ` } > Default + + + `; @@ -1429,6 +1738,7 @@ exports[`Storyshots Tooltip Default 1`] = ` } > = ({ > {pending && ( - - } - /> + )} {children} diff --git a/src/components/Checkbox/Checkbox.stories.tsx b/src/components/Checkbox/Checkbox.stories.tsx new file mode 100644 index 00000000..301a5fef --- /dev/null +++ b/src/components/Checkbox/Checkbox.stories.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import { Checkbox, CheckboxProps } from './index' +import { Meta, Story } from '@storybook/react/types-6-0' + +export default { + argTypes: { + checked: { control: { disable: true } }, + label: { control: 'text' } + }, + component: Checkbox, + title: 'Checkbox' +} as Meta + +const Template: Story = args => + +export const Default = Template.bind({}) + +export const DefaultChecked = Template.bind({}) +DefaultChecked.args = { + defaultChecked: true +} + +export const Label = Template.bind({}) +Label.args = { + label: 'Checkbox' +} + +export const Disabled = Template.bind({}) +Disabled.args = { + disabled: true +} + +export const DisabledChecked = Template.bind({}) +DisabledChecked.args = { + defaultChecked: true, + disabled: true +} diff --git a/src/components/Checkbox/index.tsx b/src/components/Checkbox/index.tsx new file mode 100644 index 00000000..16438fbb --- /dev/null +++ b/src/components/Checkbox/index.tsx @@ -0,0 +1,89 @@ +import '../assets/styles/antdAnimations.css' +import 'antd/lib/checkbox/style/index.css' +import { Checkbox as AntDCheckbox } from 'antd' +import { CheckboxChangeEvent } from 'antd/es/checkbox' +import cn from 'classnames' +import { CommonComponentProps } from '../types' +import { createUseStyles } from 'react-jss' +import { generateThemedCheckboxStyles } from './utils' +import { getDataTestAttributeProp } from '../utils' +import { ThemeType } from '../assets/styles/themes' +import React, { FC, ReactNode } from 'react' + +const { dark, light } = ThemeType + +const useStyles = createUseStyles({ + '@global': { + label: generateThemedCheckboxStyles(light), + [`.${dark}`]: { + '& $label': generateThemedCheckboxStyles(dark) + } + } +}) + +export interface CheckboxProps extends CommonComponentProps { + /** + * Determines whether the Checkbox is checked + */ + checked?: boolean + /** + * Array of classes to pass to element + * @default [] + */ + classes?: string[] + /** + * Determines whether the Checkbox is checked by default + */ + defaultChecked?: boolean + /** + * Whether Checkbox will be disabled + */ + disabled?: boolean + /** + * Label to render on the right of checkbox + */ + label?: ReactNode + /** + * Callback that runs when Checkbox is updated + */ + onChange?: (e: CheckboxChangeEvent) => void +} + +export const Checkbox: FC = ({ + checked, + classes = [], + dataTag, + defaultChecked, + disabled = false, + label, + onChange +}: CheckboxProps) => { + useStyles() + + let controlledCmpProps = {} + + if (onChange) { + controlledCmpProps = { + checked, + onChange + } + } + + if (checked && !onChange) { + throw new Error('Controlled checkboxes require an onChange prop') + } + + return ( + + {label} + + ) +} + +export type { CheckboxChangeEvent } diff --git a/src/components/Checkbox/utils.ts b/src/components/Checkbox/utils.ts new file mode 100644 index 00000000..02ee2743 --- /dev/null +++ b/src/components/Checkbox/utils.ts @@ -0,0 +1,82 @@ +import { styleguide } from 'components/assets/styles' +import { themedStyles, ThemeType } from '../assets/styles/themes' + +const { dark, light } = ThemeType + +const { + colors: { blacks, grays, whites } +} = styleguide + +const checkboxPalette = { + [dark]: { + checked: { + background: whites.base, + checkmark: blacks['lighten-30'] + }, + unchecked: { + background: blacks.base, + borderColor: blacks['lighten-10'] + } + }, + [light]: { + checked: { background: blacks['lighten-20'], checkmark: grays.base }, + unchecked: { + background: whites.base, + borderColor: blacks['lighten-80'] + } + } +} + +export const generateThemedCheckboxStyles = (themeType: ThemeType) => { + const { checked, unchecked } = checkboxPalette[themeType] + + const { + base: { color }, + disabled, + focus, + hover + } = themedStyles[themeType] + + return { + '&.ant-checkbox-wrapper': { + '& .ant-checkbox-checked': { + '& .ant-checkbox-input:focus + .ant-checkbox-inner': { + borderColor: checked.background + }, + '&.ant-checkbox-disabled': { + '& .ant-checkbox-inner': { + backgroundColor: disabled.backgroundColor, + borderColor: `${unchecked.borderColor} !important` + }, + '&:hover .ant-checkbox-inner, .ant-checkbox-inner': { + '&::after': { borderColor: disabled.color } + } + }, + '&:after': { border: 'none' }, + '&:hover .ant-checkbox-inner, .ant-checkbox-inner': { + '&::after': { borderColor: checked.checkmark }, + backgroundColor: checked.background, + borderColor: checked.background + } + }, + '& .ant-checkbox-disabled .ant-checkbox-inner': { + backgroundColor: disabled.backgroundColor, + borderColor: `${unchecked.borderColor} !important` + }, + '& .ant-checkbox-inner': { + background: unchecked.background, + borderColor: unchecked.borderColor + }, + '& .ant-checkbox-input:focus + .ant-checkbox-inner': { + borderColor: focus.borderColor + }, + '& .ant-checkbox:hover .ant-checkbox-inner': { + borderColor: checked.background + }, + '&:hover .ant-checkbox-inner': { + borderColor: hover.borderColor + }, + color + } + } +} diff --git a/src/components/Form/FormSelect/FormSelect.test.tsx b/src/components/Form/FormSelect/FormSelect.test.tsx index 5411104b..a6c489de 100644 --- a/src/components/Form/FormSelect/FormSelect.test.tsx +++ b/src/components/Form/FormSelect/FormSelect.test.tsx @@ -3,7 +3,7 @@ import FieldContext from '../FieldContext' import FieldLabel from '../FieldLabel' import { iconOptions } from 'components/Select/fixtures/sample_options' import React from 'react' -import { Select } from 'components/Select' +import { Select } from 'components/Select/SingleSelect' import FormSelect, { FormSelectProps } from './index' import { mount, ReactWrapper } from 'enzyme' diff --git a/src/components/Form/FormSelect/index.tsx b/src/components/Form/FormSelect/index.tsx index 4f770cdf..482ae90e 100644 --- a/src/components/Form/FormSelect/index.tsx +++ b/src/components/Form/FormSelect/index.tsx @@ -4,7 +4,7 @@ import { getFormFieldDataTag } from '../utils' import { Controller, useFormContext } from 'react-hook-form' import FieldContext, { FieldContextProps } from '../FieldContext' import React, { FC, useContext } from 'react' -import { Select, SelectProps } from 'components/Select' +import { Select, SelectProps } from 'components/Select/SingleSelect' export interface FormSelectProps extends BaseFieldProps, diff --git a/src/components/IconButton/IconButton.test.tsx b/src/components/IconButton/IconButton.test.tsx index c3b132c9..4c9b224d 100644 --- a/src/components/IconButton/IconButton.test.tsx +++ b/src/components/IconButton/IconButton.test.tsx @@ -1,4 +1,3 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import React from 'react' import { IconButton, IconSizes } from './index' import { mount, ReactWrapper } from 'enzyme' @@ -8,10 +7,11 @@ let mockOnClick: jest.Mock beforeEach(() => { mockOnClick = jest.fn() - wrapper = mount() + wrapper = mount() }) afterEach(() => { + jest.clearAllMocks() wrapper.unmount() }) @@ -22,18 +22,14 @@ describe('IconButton', () => { expect(iconBtn).toHaveLength(1) }) - it('passes onClick props to FontAwesomeIcon if one is provided', () => { - wrapper = mount() - - expect(wrapper.find(FontAwesomeIcon).props().onClick).toBeTruthy() + it('passes onClick props to FontAwesomeIcon', () => { + expect(wrapper.find(IconButton).props().onClick).toBeTruthy() }) it('runs onClick function when IconButton is clicked', () => { - wrapper = mount() - - const fontAwesomeIcon = wrapper.find(FontAwesomeIcon) + const iconBtn = wrapper.find(IconButton) - fontAwesomeIcon.simulate('click') + iconBtn.simulate('click') expect(mockOnClick).toHaveBeenCalledTimes(1) }) @@ -44,7 +40,9 @@ describe('IconButton', () => { }) it('renders IconButton with fontSize equal to size if size prop is provided', () => { - wrapper = mount() + wrapper = mount( + + ) const styles = window.getComputedStyle(wrapper.find('svg').getDOMNode()) diff --git a/src/components/IconButton/index.tsx b/src/components/IconButton/index.tsx index 225c73f9..b2e9fdae 100644 --- a/src/components/IconButton/index.tsx +++ b/src/components/IconButton/index.tsx @@ -13,6 +13,7 @@ import React, { FC, SyntheticEvent } from 'react' import { styleguide, ThemeType } from 'components/assets/styles' const { borderRadius, flexCenter, font, spacing } = styleguide + const { light, dark } = ThemeType const useStyles = createUseStyles({ @@ -51,7 +52,7 @@ export interface IconButtonProps { classes?: string[] hasBorder?: boolean icon?: FontAwesomeIconProps['icon'] - onClick?: (e?: SyntheticEvent) => void + onClick: (e?: SyntheticEvent) => void size?: number } @@ -72,19 +73,9 @@ export const IconButton: FC = ({ classes ) - const optionalProps: Pick = {} - - if (onClick) { - optionalProps.onClick = onClick - } - return ( - - + + ) } diff --git a/src/components/Input/Input.stories.tsx b/src/components/Input/Input.stories.tsx index 54ea78d1..36dede48 100644 --- a/src/components/Input/Input.stories.tsx +++ b/src/components/Input/Input.stories.tsx @@ -26,3 +26,6 @@ FullWidth.args = { fullWidth: true } export const Error = Template.bind({}) Error.args = { error: true } + +export const Disabled = Template.bind({}) +Disabled.args = { disabled: true } diff --git a/src/components/NotificationV2/NotificationContext.ts b/src/components/NotificationV2/NotificationContext.ts index a104b6e6..269b003b 100644 --- a/src/components/NotificationV2/NotificationContext.ts +++ b/src/components/NotificationV2/NotificationContext.ts @@ -9,6 +9,4 @@ const [useNotificationContext, NotificationCtxProvider] = createCtx< NotificationContextProps >() -const useNotification = (): NotificationContextProps => useNotificationContext() - -export { NotificationCtxProvider, useNotification } +export { NotificationCtxProvider, useNotificationContext as useNotification } diff --git a/src/components/Popover/index.tsx b/src/components/Popover/index.tsx index 84f87157..af05f805 100644 --- a/src/components/Popover/index.tsx +++ b/src/components/Popover/index.tsx @@ -37,6 +37,10 @@ export interface PopoverProps extends CommonComponentProps { * Content rendered inside of popover */ content: PopoverContent + /** + * Callback that runs when visibility of popover changes + */ + onVisibleChange?: (visible: boolean) => void /** * Selector of HTML element inside which to render the popup */ @@ -49,6 +53,14 @@ export interface PopoverProps extends CommonComponentProps { * Title of popover */ title?: PopoverContent + /** + * Action type that will trigger the popover to open + */ + trigger?: 'hover' | 'click' + /** + * Controlled open/close state of popover + */ + visible?: boolean } export const Popover: FC = ({ @@ -56,9 +68,12 @@ export const Popover: FC = ({ classes = [], content, dataTag, + onVisibleChange, placement = 'bottom', popupContainerSelector, - title + title, + trigger = 'click', + visible }: PopoverProps) => { useStyles() @@ -70,12 +85,24 @@ export const Popover: FC = ({ } } + let controlledCmpProps = {} + + if (onVisibleChange) { + controlledCmpProps = { onVisibleChange, visible } + } + + if (visible && !onVisibleChange) { + throw new Error('Controlled Popover requires an onVisibleChange prop') + } + return ( diff --git a/src/components/Select/BaseSelect.tsx b/src/components/Select/BaseSelect.tsx new file mode 100644 index 00000000..a427d0ea --- /dev/null +++ b/src/components/Select/BaseSelect.tsx @@ -0,0 +1,157 @@ +import '../assets/styles/antdAnimations.css' +import 'antd/lib/select/style/index.css' +import { Select as AntDSelect } from 'antd' +import { Checkbox } from '../Checkbox' +import cn from 'classnames' +import { getDataTestAttributeProp } from '../utils' +import { getPopupContainerProps } from './utils' +import { MultiSelectProps } from './MultiSelect/types' +import { NoContentFound } from './NoContentFound' +import noop from 'lodash/noop' +import { OptionChildren } from './OptionChildren' +import { SelectProps } from './SingleSelect/types' +import { SelectSkeleton } from './SingleSelect/SelectSkeleton' +import { Spin } from '../Spin' +import React, { FC, ReactNode } from 'react' + +const { Option } = AntDSelect + +interface CommonBaseSelectProps + extends Omit { + useStyles(data?: unknown): Record +} + +interface BaseMultiSelectProps + extends Pick< + MultiSelectProps, + 'onChange' | 'maxTagCount' | 'maxTagTextLength' | 'pending' + > { + defaultValue?: MultiSelectProps['defaultValues'] + dropdownRender: (menu: ReactNode) => ReactNode + localValues: string[] + mode: 'multiple' + value?: MultiSelectProps['values'] +} + +interface BaseSingleSelectProps + extends Pick { + mode: 'single' +} + +type BaseSelectProps = CommonBaseSelectProps & + (BaseSingleSelectProps | BaseMultiSelectProps) + +export const BaseSelect: FC = (props: BaseSelectProps) => { + const { + classes = [], + dataTag, + disabled = false, + error = false, + loading = false, + options, + optionsConfig = {}, + placeholder = '', + popupContainerSelector, + showSearch = false, + useStyles + } = props + + const componentClasses = useStyles(props) + + const inputClasses: string = cn( + { + [componentClasses.error]: error + }, + classes + ) + + let multiSelectProps = {} + let singleSelectProps = {} + + if (props.mode === 'multiple') { + const { + defaultValue, + dropdownRender, + maxTagCount = 2, + maxTagTextLength = 12, + onChange, + pending = false, + value + } = props + + multiSelectProps = { + defaultValue, + dropdownRender, + maxTagCount, + maxTagPlaceholder: (selectedOpts: any[]) => + `& ${selectedOpts.length} more`, + maxTagTextLength, + menuItemSelectedIcon: null, + mode: 'multiple', + notFoundContent: pending ? ( + + + + ) : ( + + ), + onChange, + optionLabelProp: 'label', + showSearch: false, + value, + ...getDataTestAttributeProp('multi-select', dataTag) + } + } else { + const { defaultValue, onChange, showSearch, value } = props + + singleSelectProps = { + defaultValue, + onChange, + showSearch, + value, + ...getDataTestAttributeProp('select', dataTag) + } + } + + return loading ? ( + + ) : ( +
+ } + placeholder={placeholder} + showArrow + showSearch={showSearch} + {...getPopupContainerProps(popupContainerSelector)} + {...multiSelectProps} + {...singleSelectProps} + > + {options.map(({ iconKey, text, value }) => ( + + ))} + +
+ ) +} diff --git a/src/components/Select/MultiSelect/MultiSelect.stories.tsx b/src/components/Select/MultiSelect/MultiSelect.stories.tsx new file mode 100644 index 00000000..cdb73056 --- /dev/null +++ b/src/components/Select/MultiSelect/MultiSelect.stories.tsx @@ -0,0 +1,85 @@ +import { action } from '@storybook/addon-actions' +import { iconOptions } from '../fixtures/sample_options' +import { SbTheme } from '../../../../.storybook/preview' +import { SecondaryBgDecorator } from '../../../../.storybook/utils' +import { SelectOption } from '../SingleSelect/types' +import { useTheme } from 'react-jss' +import { Meta, Story } from '@storybook/react/types-6-0' +import { MultiSelect, MultiSelectProps } from './index' +import React, { FC } from 'react' + +export default { + argTypes: { + onChange: { defaultValue: action('onChange') }, + popupContainerSelector: { control: { disable: true } }, + values: { control: { disable: true } } + }, + component: MultiSelect, + decorators: [SecondaryBgDecorator], + parameters: { + // disabled because shallow rendering gives warning, but MultiSelect only works with shallow render + storyshots: { disable: true } + }, + title: 'MultiSelect' +} as Meta + +const basicOptions: SelectOption[] = [ + { text: 'Lorem', value: '0' }, + { text: 'Incididunt', value: '1' }, + { text: 'Ipsum', value: '2' }, + { text: 'Dolor', value: '3' }, + { text: 'Sit', value: '4' }, + { text: 'Amet', value: '5' }, + { text: 'Consectetur', value: '6' }, + { text: 'Adipiscing elit sed do eiusmod', value: '7' }, + { text: 'consectetur', value: '8' } +] + +const ThemedMultiSelect: FC = (props: MultiSelectProps) => { + const theme: SbTheme = useTheme() + + const popupContainerSelector = `.${theme.type}` + + return ( + + ) +} + +const Template: Story = args => ( + +) + +export const Default = Template.bind({}) +Default.args = { + options: basicOptions, + placeholder: 'Pls select' +} + +export const Search = Template.bind({}) +Search.args = { + options: basicOptions, + searchPlaceholder: 'Search...', + showSearch: true +} + +export const SelectedContentWidth = Template.bind({}) +SelectedContentWidth.args = { + matchSelectedContentWidth: 125, + maxTagCount: 5, + options: basicOptions, + showSearch: true +} + +export const FullWidth = Template.bind({}) +FullWidth.args = { + fullWidth: true, + options: basicOptions +} + +export const Icon = Template.bind({}) +Icon.args = { + options: iconOptions +} diff --git a/src/components/Select/MultiSelect/index.tsx b/src/components/Select/MultiSelect/index.tsx new file mode 100644 index 00000000..def0a865 --- /dev/null +++ b/src/components/Select/MultiSelect/index.tsx @@ -0,0 +1,128 @@ +import '../../assets/styles/antdAnimations.css' +import 'antd/lib/select/style/index.css' +import { BaseSelect } from '../BaseSelect' +import Fuse from 'fuse.js' +import { Input } from '../../Input' +import { MultiSelectProps } from './types' +import { getSortedAndFilteredValues, useStyles } from './utils' +import React, { + ChangeEvent, + FC, + KeyboardEvent, + ReactNode, + useState +} from 'react' + +export const MultiSelect: FC = (props: MultiSelectProps) => { + const { + classes = [], + dataTag, + defaultValues = [], + disabled = false, + error = false, + fullWidth = false, + loading = false, + maxTagCount, + maxTagTextLength, + matchSelectedContentWidth = false, + onChange, + onSearch, + optionKeysToFilter = ['text'], + options, + optionsConfig = {}, + placeholder = '', + pending = false, + popupContainerSelector, + searchPlaceholder = '', + showSearch = false, + values + } = props + const [localValues, setLocalValues] = useState(values || defaultValues) + + const [searchTerm, setSearchTerm] = useState('') + + const componentClasses = useStyles(props) + + const dropdownRender = (menu: ReactNode) => ( + <> + {showSearch && ( + ) => { + if (onSearch) onSearch(e.target.value) + setSearchTerm(e.target.value) + }} + onKeyDown={(e: KeyboardEvent) => { + const keysToNotPropagate: KeyboardEvent['key'][] = [ + 'Enter', + 'Backspace' + ] + if (keysToNotPropagate.includes(e.key)) + e.stopPropagation() + }} + placeholder={searchPlaceholder} + /> + )} + {menu} + + ) + + const fuse = new Fuse(options, { + isCaseSensitive: false, + keys: optionKeysToFilter, + shouldSort: true, + threshold: 0.1 + }) + + const onChangeAntD = (values?: string[]) => { + const vals = values ? values : [] + + if (onChange) onChange(vals) + + setLocalValues(vals) + } + + let optionalProps = {} + + if (values) optionalProps = { value: values } + + if (values && !onChange) { + throw new Error('Controlled MultiSelect requires an onChange prop') + } + + const sortedAndFilteredValues = getSortedAndFilteredValues({ + fuse, + localValues, + onSearch, + options, + searchTerm + }) + + return ( + + ) +} + +export * from './types' diff --git a/src/components/Select/MultiSelect/types.ts b/src/components/Select/MultiSelect/types.ts new file mode 100644 index 00000000..11f9f639 --- /dev/null +++ b/src/components/Select/MultiSelect/types.ts @@ -0,0 +1,31 @@ +import { SelectProps } from '../SingleSelect/types' + +export interface MultiSelectProps + extends Omit { + /** + * Default values for select component. Without this, the select dropdown will be blank until an option is selected. Gets overwritten by values if both are provided + */ + defaultValues?: string[] + /** + * The number after which to show "& 'x' more" for selected tags. Setting it to 0 will always show all selected tags in the input + * @default 2 + */ + maxTagCount?: number + maxTagTextLength?: number + /** + * Array of options to be rendered in the dropdown + */ + onChange?: (values: string[]) => void + onSearch?: (value: string) => void + /** + * Only valid if showSearch is true and and onSearch is not passed. By default options are only filtered by text. To filter by other keys, pass an array of keys to filter. Eg. ['value'] + * @default ['text'] + */ + optionKeysToFilter?: string[] + pending?: boolean + searchPlaceholder?: string + /** + * Selected values for if component is controlled. Requires an onChange to be passed + */ + values?: string[] +} diff --git a/src/components/Select/MultiSelect/utils.ts b/src/components/Select/MultiSelect/utils.ts new file mode 100644 index 00000000..7a082d36 --- /dev/null +++ b/src/components/Select/MultiSelect/utils.ts @@ -0,0 +1,205 @@ +import { createUseStyles } from 'react-jss' +import Fuse from 'fuse.js' +import { MultiSelectProps } from './types' +import { SelectOption } from '../SingleSelect/types' +import sortBy from 'lodash/sortBy' +import { tagPalette } from '../../Tag/utils' +import { + defaultFieldWidth, + fieldErrorStyles, + styleguide +} from '../../assets/styles/styleguide' +import { + generateThemedDisabledStyles, + generateThemedDropdownStyles, + generateThemedFocusedStyles, + generateThemedInputStyles, + generateThemedOptionStyles, + generateThemedSelectStyles, + tooltipStyles +} from '../utils' +import { themedStyles, ThemeType } from '../../assets/styles/themes' + +const { dark, light } = ThemeType + +const { borderRadius, flexAlignCenter, fontWeight, spacing } = styleguide + +const filterOptions = ( + fuse: Fuse, + options: SelectOption[], + value?: string +) => { + if (!value) { + return options + } + + const filteredOptions = fuse + .search(value) + .map(({ item }: Fuse.FuseResult): SelectOption => item) + + return filteredOptions +} + +const generateThemedTagStyles = (themeType: ThemeType) => { + const { background, borderColor, color } = tagPalette[themeType] + + const { base, hover } = themedStyles[themeType] + + return { + '& .ant-select-selection-item': { + '& .ant-select-selection-item-remove': { + '&:hover': { + color: hover.color + }, + color: base.color + }, + background, + borderColor, + color, + fontWeight: fontWeight.light + } + } +} + +export const generateThemedMSOptionStyles = (themeType: ThemeType) => { + const optionClasses = '&.ant-select-item-option' + + const optionStyles = generateThemedOptionStyles(themeType)[optionClasses] + + return { + [optionClasses]: { + '& .ant-select-item-option-content': { ...flexAlignCenter }, + '&.ant-select-item-option-active': + optionStyles['&.ant-select-item-option-active'], + '&.ant-select-item-option-selected': { + ...optionStyles['&.ant-select-item-option-selected'], + background: 'transparent' + }, + color: optionStyles.color, + fontWeight: optionStyles.fontWeight + } + } +} + +const focusedClasses = + '&.ant-select-focused.ant-select-multiple:not(.ant-select-disabled) .ant-select-selector' + +export const useStyles = createUseStyles({ + checkbox: { marginRight: spacing.s }, + container: ({ fullWidth, matchSelectedContentWidth }) => ({ + '& .ant-select': { + '&$error > .ant-select-selector': { + border: `1px solid ${themedStyles[light].error.borderColor}` + }, + '&.ant-select-multiple': { + '&.ant-select-disabled': generateThemedDisabledStyles(light), + ...generateThemedSelectStyles(light), + ...generateThemedTagStyles(light), + '& .ant-select-selector': { + ...generateThemedInputStyles(light), + borderRadius + }, + [focusedClasses]: generateThemedFocusedStyles(light) + }, + minWidth: matchSelectedContentWidth + ? matchSelectedContentWidth + : 'unset', + width: matchSelectedContentWidth ? 'unset' : '100%' + }, + width: + fullWidth || matchSelectedContentWidth ? '100%' : defaultFieldWidth + }), + dropdown: generateThemedDropdownStyles(light), + error: { ...fieldErrorStyles.error }, + option: generateThemedMSOptionStyles(light), + searchBar: { + margin: 3 * spacing.xs, + width: `calc(100% - ${6 * spacing.xs}px)` + }, + tag: { + marginRight: spacing.xs + }, + tooltip: tooltipStyles, + tooltipTriggerClasses: { minWidth: 0 }, + // eslint-disable-next-line sort-keys + '@global': { + ...fieldErrorStyles['@global'], + [`.${dark}`]: { + '& $container': { + '& .ant-select': { + '&$error > .ant-select-selector': { + border: `1px solid ${themedStyles[dark].error.borderColor}` + }, + '&.ant-select-multiple': { + '&.ant-select-disabled': generateThemedDisabledStyles( + dark + ), + ...generateThemedSelectStyles(dark), + ...generateThemedTagStyles(dark), + '& .ant-select-selector': { + ...generateThemedInputStyles(dark) + }, + [focusedClasses]: generateThemedFocusedStyles(dark) + } + } + }, + '& $dropdown': generateThemedDropdownStyles(dark), + '& $option': generateThemedMSOptionStyles(dark) + } + } +}) + +export const groupOptions = ( + ungroupedOpts: SelectOption[], + localValues: string[] +) => { + const selected: SelectOption[] = [] + const unselected: SelectOption[] = [] + + for (const opt of ungroupedOpts) { + localValues.find(checkedVal => checkedVal === opt.value) + ? selected.push(opt) + : unselected.push(opt) + } + + return { selected, unselected } +} + +const groupAndSortOptions = ( + ungroupedOpts: SelectOption[], + localValues: string[] +) => { + const { selected, unselected } = groupOptions(ungroupedOpts, localValues) + + return [ + ...sortBy(selected, [option => option.text.toUpperCase()]), + ...sortBy(unselected, [option => option.text.toUpperCase()]) + ] +} + +// ---------------------------------------- + +interface GetSortedAndFilteredValuesArgs + extends Pick { + fuse: Fuse + localValues: string[] + searchTerm: string +} + +export const getSortedAndFilteredValues = ({ + fuse, + onSearch, + options, + localValues, + searchTerm +}: GetSortedAndFilteredValuesArgs) => { + const sortedValues = groupAndSortOptions(options, localValues) + + const sortedAndFilteredValues = onSearch + ? sortedValues + : filterOptions(fuse, sortedValues, searchTerm) + + return sortedAndFilteredValues +} + +// ---------------------------------------- diff --git a/src/components/Select/NoContentFound.tsx b/src/components/Select/NoContentFound.tsx new file mode 100644 index 00000000..9e30e3b6 --- /dev/null +++ b/src/components/Select/NoContentFound.tsx @@ -0,0 +1,23 @@ +import { createUseStyles } from 'react-jss' +import { styleguide } from '../assets/styles/styleguide' +import React, { FC, ReactNode } from 'react' + +const { flexCenter } = styleguide + +const useNoContentFoundStyles = createUseStyles({ + container: { + ...flexCenter + } +}) + +interface Props { + children?: ReactNode +} + +export const NoContentFound: FC = ({ + children = No Data +}: Props) => { + const classes = useNoContentFoundStyles() + + return
{children}
+} diff --git a/src/components/Select/OptionChildren.tsx b/src/components/Select/OptionChildren.tsx new file mode 100644 index 00000000..29c6ef82 --- /dev/null +++ b/src/components/Select/OptionChildren.tsx @@ -0,0 +1,92 @@ +import cn from 'classnames' +import { createUseStyles } from 'react-jss' +import { styleguide } from '../assets/styles/styleguide' +import { Tooltip } from '../Tooltip' +import { tooltipStyles } from './utils' +import { Icon, IconName, SharedIconProps } from '../Icon' +import React, { FC, ReactNode, SyntheticEvent, useState } from 'react' +import { + SelectOption, + SelectOptionsConfig, + SelectProps +} from './SingleSelect/types' + +const { flexAlignCenter } = styleguide + +const useOptionChildrenStyles = createUseStyles({ + icon: { + ...flexAlignCenter, + paddingRight: 7.5 + }, + option: { + ...flexAlignCenter, + minWidth: 0 + }, + optionText: { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap' + }, + tooltip: tooltipStyles +}) + +type OptionChildrenProps = Omit & + Pick & { children?: ReactNode } + +export const OptionChildren: FC = ({ + children, + iconKey, + optionsConfig = {}, + text +}: OptionChildrenProps) => { + const [hasTooltip, setHasTooltip] = useState(false) + const classes = useOptionChildrenStyles() + + const renderIcon = ( + iconKey: IconName, + optionsConfig: SelectOptionsConfig + ): JSX.Element => { + const commonIconProps: SharedIconProps = { + height: 15 + } + + const { iconMap } = optionsConfig + + return ( + + {iconMap ? ( + + ) : ( + + )} + + ) + } + + return ( +
+ {children && children} + {iconKey && renderIcon(iconKey, optionsConfig)} + { + const el = e.currentTarget as HTMLElement + + if (el.scrollWidth > el.offsetWidth) { + setHasTooltip(true) + } else { + setHasTooltip(false) + } + }} + > + {hasTooltip ? ( + + {text} + + ) : ( + text + )} + +
+ ) +} diff --git a/src/components/Select/Select.stories.tsx b/src/components/Select/Select.stories.tsx deleted file mode 100644 index 05df659c..00000000 --- a/src/components/Select/Select.stories.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react' -import { basicOptions, iconOptions } from './fixtures/sample_options' -import { Meta, Story } from '@storybook/react/types-6-0' -import { Select, SelectProps } from './index' - -export default { - argTypes: { - value: { control: { disable: true } } - }, - component: Select, - title: 'Select' -} as Meta - -const Template: Story = args => +} + +const Template: Story = args => + +export const Default = Template.bind({}) +Default.args = { + options: textOverflowOptions +} + +export const Icon = Template.bind({}) +Icon.args = { + defaultValue: 'aws', + options: iconOptions, + placeholder: 'Choose a Cloud Provider' +} + +export const FullWidth = Template.bind({}) +FullWidth.args = { + defaultValue: 'lorem', + fullWidth: true, + options: basicOptions +} + +export const Search = Template.bind({}) +Search.args = { + options: iconOptions, + placeholder: 'Choose a Cloud Provider', + showSearch: true +} + +export const SelectedOptionWidth = Template.bind({}) +SelectedOptionWidth.args = { + matchSelectedContentWidth: 75, + options: textOverflowOptions, + placeholder: 'Choose a Cloud Provider', + showSearch: true +} diff --git a/src/components/Select/Select.test.tsx b/src/components/Select/SingleSelect/Select.test.tsx similarity index 96% rename from src/components/Select/Select.test.tsx rename to src/components/Select/SingleSelect/Select.test.tsx index c57edad8..1949eb3f 100644 --- a/src/components/Select/Select.test.tsx +++ b/src/components/Select/SingleSelect/Select.test.tsx @@ -1,10 +1,10 @@ import { Select as AntDSelect } from 'antd' -import { Icon } from '../Icon' -import IconsMap from '../Icon/IconsMap' +import { Icon } from '../../Icon' +import IconsMap from '../../Icon/IconsMap' import React from 'react' import { Select } from './index' -import { Skeleton } from '../Skeleton' -import { basicOptions, iconOptions } from './fixtures/sample_options' +import { Skeleton } from '../../Skeleton' +import { basicOptions, iconOptions } from '../fixtures/sample_options' import { mount, ReactWrapper, shallow } from 'enzyme' let wrapper: ReactWrapper diff --git a/src/components/Select/SingleSelect/SelectSkeleton.tsx b/src/components/Select/SingleSelect/SelectSkeleton.tsx new file mode 100644 index 00000000..2242b1ef --- /dev/null +++ b/src/components/Select/SingleSelect/SelectSkeleton.tsx @@ -0,0 +1,25 @@ +import { BaseFormElementProps } from '../../types' +import { createUseStyles } from 'react-jss' +import { defaultFieldWidth } from '../../assets/styles/styleguide' +import { Skeleton } from '../../Skeleton' +import React, { FC } from 'react' + +const useStyles = createUseStyles({ + container: { + width: props => (props.fullWidth ? '100%' : defaultFieldWidth) + } +}) + +export type SelectSkeletonProps = Pick + +export const SelectSkeleton: FC = ( + props: SelectSkeletonProps +) => { + const classes = useStyles(props) + + return ( +
+ +
+ ) +} diff --git a/src/components/Select/SingleSelect/index.tsx b/src/components/Select/SingleSelect/index.tsx new file mode 100644 index 00000000..a8150297 --- /dev/null +++ b/src/components/Select/SingleSelect/index.tsx @@ -0,0 +1,63 @@ +import '../../assets/styles/antdAnimations.css' +import 'antd/lib/select/style/index.css' +import { BaseSelect } from '../BaseSelect' +import { SelectProps } from './types' +import { useStyles } from '../utils' +import React, { FC } from 'react' + +export const Select: FC = (props: SelectProps) => { + const { + classes = [], + dataTag, + // defaulting defaultValue to empty string doesn't render a placeholder if a placeholder is provided + defaultValue, + disabled = false, + error = false, + fullWidth = false, + loading = false, + matchSelectedContentWidth = false, + onChange, + options, + optionsConfig = {}, + popupContainerSelector, + placeholder = '', + showSearch = false, + value + } = props + + let controlledCmpProps = {} + + if (onChange) { + controlledCmpProps = { + onChange, + value + } + } + + if (value && !onChange) { + throw new Error('Controlled inputs require an onChange prop') + } + + return ( + + ) +} + +export * from './types' diff --git a/src/components/Select/SingleSelect/types.ts b/src/components/Select/SingleSelect/types.ts new file mode 100644 index 00000000..e6d07638 --- /dev/null +++ b/src/components/Select/SingleSelect/types.ts @@ -0,0 +1,45 @@ +import { BaseFormElementProps } from '../../types' +import { IconName } from '../../Icon' + +export interface SelectOption { + iconKey?: IconName + text: string + value: string +} + +export interface SelectOptionsConfig { + iconMap?: Record +} + +export interface SelectProps extends BaseFormElementProps { + /** + * Default value for select component. Without this, the select dropdown will be blank until an option is selected + */ + defaultValue?: string + /** + * Sets the width of the select to be same as the selected content width. Can be false or a number which will be used as the minimum width + */ + matchSelectedContentWidth?: false | number + /** + * Selector of HTML element inside which to render the popup/dropdown + */ + popupContainerSelector?: string + /** + * Array of options to be rendered in the dropdown + */ + options: SelectOption[] + /** + * Optional configuration that applies to the options. Ex: An icon map where each key in the map corresponds to the value of the option + * @default {} + */ + optionsConfig?: SelectOptionsConfig + /** + * Whether or not to show search input + * @default false + */ + showSearch?: boolean + /** + * Input content value for controlled inputs. Requires an onChange to be passed + */ + value?: string +} diff --git a/src/components/Select/fixtures/sample_options.ts b/src/components/Select/fixtures/sample_options.ts index 05b7e3fd..4c945d7e 100644 --- a/src/components/Select/fixtures/sample_options.ts +++ b/src/components/Select/fixtures/sample_options.ts @@ -1,12 +1,20 @@ -import { Options } from '../.' +import { SelectOption } from '../SingleSelect' -export const basicOptions: Options[] = [ +export const basicOptions: SelectOption[] = [ { text: 'Lorem', value: 'lorem' }, { text: 'Ipsum', value: 'ipsum' } ] -export const iconOptions: Options[] = [ +export const iconOptions: SelectOption[] = [ { iconKey: 'aws', text: 'AWS', value: 'aws' }, { iconKey: 'azure', text: 'Azure', value: 'azure' }, { iconKey: 'googleCloudService', text: 'GCP', value: 'gcp' } ] + +export const textOverflowOptions: SelectOption[] = [ + ...basicOptions, + { + text: 'Adipiscing elit sed do eiusmod tempor incididunt', + value: 'adipiscing' + } +] diff --git a/src/components/Select/index.ts b/src/components/Select/index.ts new file mode 100644 index 00000000..f20464bf --- /dev/null +++ b/src/components/Select/index.ts @@ -0,0 +1,2 @@ +export * from './MultiSelect' +export * from './SingleSelect' diff --git a/src/components/Select/index.tsx b/src/components/Select/index.tsx deleted file mode 100644 index 52d2b4c4..00000000 --- a/src/components/Select/index.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import '../assets/styles/antdAnimations.css' -import 'antd/lib/select/style/index.css' -import { Select as AntDSelect } from 'antd' -import { BaseFormElementProps } from '../types' -import cn from 'classnames' -import { createUseStyles } from 'react-jss' -import { getDataTestAttributeProp } from '../utils' -import { Skeleton } from '../Skeleton' -import { - defaultFieldWidth, - fieldErrorStyles, - styleguide -} from '../assets/styles/styleguide' -import { Icon, IconName, SharedIconProps } from '../Icon' -import React, { FC } from 'react' - -const { flexAlignCenter } = styleguide - -const { Option } = AntDSelect - -const useStyles = createUseStyles({ - ...fieldErrorStyles, - container: { - width: props => (props.fullWidth ? '100%' : defaultFieldWidth) - }, - dropdown: { - width: '100%' - }, - icon: { - ...flexAlignCenter, - paddingRight: 7.5 - }, - option: { - ...flexAlignCenter - } -}) - -const SelectSkeleton: FC = (props: SelectProps) => { - const classes = useStyles(props) - - return ( -
- -
- ) -} - -export interface Options { - iconKey?: IconName - text: string - value: string -} - -export interface OptionsConfig { - iconMap?: Record -} - -export interface SelectProps extends BaseFormElementProps { - /** - * Default value for select component. Without this, the select dropdown will be blank until an option is selected - */ - defaultValue?: string - /** - * Array of options to be rendered in the dropdown - */ - options: Options[] - /** - * Optional configuration that applies to the options. Ex: An icon map where each key in the map corresponds to the value of the option - * @default {} - */ - optionsConfig?: OptionsConfig - /** - * Whether or not to show search input - * @default false - */ - showSearch?: boolean - /** - * Input content value for controlled inputs. Requires an onChange to be passed - */ - value?: string -} - -export const Select: FC = (props: SelectProps) => { - const { - classes = [], - dataTag, - defaultValue = '', - disabled = false, - error = false, - loading = false, - onChange, - options, - optionsConfig = {}, - placeholder = '', - showSearch = false, - value - } = props - - const componentClasses = useStyles(props) - - const inputClasses: string = cn( - { - [componentClasses.error]: error, - [componentClasses.dropdown]: true - }, - classes - ) - - let controlledCmpProps = {} - - if (onChange) { - controlledCmpProps = { - onChange, - value - } - } - - if (value && !onChange) { - throw new Error('Controlled inputs require an onChange prop') - } - - const renderIcon = ( - iconKey: IconName, - optionsConfig: OptionsConfig - ): JSX.Element => { - const commonIconProps: SharedIconProps = { - height: 15 - } - - const { iconMap } = optionsConfig - - return ( - - {iconMap ? ( - - ) : ( - - )} - - ) - } - - return loading ? ( - - ) : ( -
- - {options.map(({ iconKey, text, value }) => ( - - ))} - -
- ) -} diff --git a/src/components/Select/utils.tsx b/src/components/Select/utils.tsx new file mode 100644 index 00000000..8f625543 --- /dev/null +++ b/src/components/Select/utils.tsx @@ -0,0 +1,253 @@ +import { createUseStyles } from 'react-jss' +import { generatePopupSelector } from 'components/utils' +import { + defaultFieldWidth, + fieldErrorStyles, + styleguide +} from '../assets/styles/styleguide' +import { themedStyles, ThemeType } from '../assets/styles/themes' + +const { dark, light } = ThemeType + +const { + borderRadius, + colors: { blacks, grays, whites }, + fontWeight +} = styleguide + +const selectPalette = { + [dark]: { + base: { + background: blacks['darken-40'] + }, + input: { + default: { + borderColor: blacks['lighten-20'] + }, + disabled: { + background: blacks['darken-20'], + color: blacks['lighten-30'] + } + }, + option: { + hover: { + background: blacks['lighten-10'] + }, + selected: { + background: blacks.base, + color: grays.base + } + } + }, + [light]: { + base: { + background: whites.base + }, + input: { + default: { + borderColor: blacks['lighten-80'] + }, + disabled: { + background: grays['lighten-70'], + color: blacks['lighten-70'] + } + }, + option: { + hover: { + background: grays['lighten-40'] + }, + selected: { + background: grays.base, + color: blacks.base + } + } + } +} + +export const generateThemedDisabledStyles = (themeType: ThemeType) => { + const { + disabled: { borderColor } + } = themedStyles[themeType] + + const { background, color } = selectPalette[themeType].input.disabled + + return { + '& .ant-select-arrow': { color }, + '& .ant-select-selector': { + background, + borderColor, + color + } + } +} + +export const generateThemedDropdownStyles = (themeType: ThemeType) => { + const { + disabled: { color } + } = themedStyles[themeType] + const { background } = selectPalette[themeType].base + + return { + '&.ant-select-dropdown-empty .ant-select-item-empty, .ant-select-item-empty': { + color + }, + background + } +} + +export const generateThemedFocusedStyles = (themeType: ThemeType) => { + const { focus } = themedStyles[themeType] + + return { + borderColor: focus.borderColor, + boxShadow: 'none' + } +} + +export const generateThemedInputStyles = (themeType: ThemeType) => { + const { + base: { color } + } = themedStyles[themeType] + + const { background } = selectPalette[themeType].base + const { borderColor } = selectPalette[themeType].input.default + + return { + '& .ant-select-selection-search > .ant-select-selection-search-input': { + color + }, + background, + borderColor, + color + } +} + +export const generateThemedOptionStyles = (themeType: ThemeType) => { + const { + base: { color } + } = themedStyles[themeType] + + const { hover, selected } = selectPalette[themeType].option + + return { + '&.ant-select-item-option': { + '&.ant-select-item-option-active': { + background: hover.background + }, + '&.ant-select-item-option-selected': { + background: selected.background, + color: selected.color, + fontWeight: fontWeight.regular + }, + color, + fontWeight: fontWeight.light + } + } +} + +export const generateThemedSelectStyles = (themeType: ThemeType) => { + const { + base: { color }, + hover, + placeholder + } = themedStyles[themeType] + + return { + '& .ant-select-arrow': { color }, + '& .ant-select-selection-placeholder': { color: placeholder.color }, + '&:not(.ant-select-disabled):hover .ant-select-selector': { + borderColor: hover.borderColor + }, + color, + fontWeight: fontWeight.light + } +} + +const generateThemedSelectedItemStyles = (themeType: ThemeType) => { + const { + base: { color } + } = themedStyles[themeType] + + return { + '&.ant-select-single.ant-select-open .ant-select-selection-item': { + color + } + } +} + +const disabledClasses = + '&.ant-select-disabled.ant-select-single:not(.ant-select-customize-input)' +const focusedClasses = + '&.ant-select-focused:not(.ant-select-disabled).ant-select-single:not(.ant-select-customize-input) .ant-select-selector' + +export const tooltipStyles = { + '&.ant-tooltip': { + '& > .ant-tooltip-content > .ant-tooltip-inner': { + overflowWrap: 'normal' + }, + maxWidth: 'unset' + } +} + +export const useStyles = createUseStyles({ + container: ({ fullWidth, matchSelectedContentWidth }) => ({ + '& .ant-select': { + borderRadius, + ...generateThemedSelectStyles(light), + ...generateThemedSelectedItemStyles(light), + '& .ant-select-selector': { + ...generateThemedInputStyles(light), + borderRadius + }, + '&$error > .ant-select-selector': { + border: `1px solid ${themedStyles[light].error.borderColor}` + }, + [disabledClasses]: generateThemedDisabledStyles(light), + [focusedClasses]: generateThemedFocusedStyles(light), + minWidth: matchSelectedContentWidth + ? matchSelectedContentWidth + : 'unset', + width: matchSelectedContentWidth ? 'unset' : '100%' + }, + width: + fullWidth || matchSelectedContentWidth ? '100%' : defaultFieldWidth + }), + dropdown: generateThemedDropdownStyles(light), + error: { ...fieldErrorStyles.error }, + option: { + ...generateThemedOptionStyles(light) + }, + // eslint-disable-next-line sort-keys + '@global': { + ...fieldErrorStyles['@global'], + [`.${dark}`]: { + '& $container': { + '& .ant-select': { + ...generateThemedSelectStyles(dark), + ...generateThemedSelectedItemStyles(dark), + '& .ant-select-selector': { + ...generateThemedInputStyles(dark) + }, + '&$error > .ant-select-selector': { + border: `1px solid ${themedStyles[dark].error.borderColor}` + }, + [disabledClasses]: generateThemedDisabledStyles(dark), + [focusedClasses]: generateThemedFocusedStyles(dark) + } + }, + '& $dropdown': generateThemedDropdownStyles(dark), + '& $option': generateThemedOptionStyles(dark) + } + } +}) + +export const getPopupContainerProps = (popupContainerSelector = '') => { + let popupContainerProps = {} + + if (popupContainerSelector) + popupContainerProps = { + getPopupContainer: generatePopupSelector(popupContainerSelector) + } + + return popupContainerProps +} diff --git a/src/components/Spin/index.tsx b/src/components/Spin/index.tsx new file mode 100644 index 00000000..8dcce321 --- /dev/null +++ b/src/components/Spin/index.tsx @@ -0,0 +1,32 @@ +import { Spin as AntDSpin } from 'antd' +import { createUseStyles } from 'react-jss' +import { LoadingOutlined } from '@ant-design/icons' +import { styleguide } from 'components/assets/styles' +import React, { FC } from 'react' + +const { + colors: { blacks } +} = styleguide + +const useStyles = createUseStyles({ + loading: { + '&.anticon': { + color: blacks['lighten-50'], + fontSize: ({ size }) => size + } + } +}) + +interface SpinProps { + size?: number +} + +export const Spin: FC = ({ size = 16 }) => { + const classes = useStyles({ size }) + + return ( + } + /> + ) +} diff --git a/src/components/Table/Table.stories.mdx b/src/components/Table/Table.stories.mdx index 6a981faa..3fe4e389 100644 --- a/src/components/Table/Table.stories.mdx +++ b/src/components/Table/Table.stories.mdx @@ -1,4 +1,5 @@ import * as stories from './Table.stories.tsx' +import { SecondaryBgDecorator } from '../../../.storybook/utils' import { Table } from '.' import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs/blocks' @@ -8,7 +9,7 @@ The `Table` component creates a table from a provided data source. It allows for The following examples start from a basic table and don't show all possible types of data that the table can render. If you'd like to view all possible column types and formats in one place, [click here.](?path=/docs/table--mixed#columntype--all-column-types-and-formats) - + ## Simple Usage diff --git a/src/components/Table/styles.ts b/src/components/Table/styles.ts index b38246c5..6770c4d6 100644 --- a/src/components/Table/styles.ts +++ b/src/components/Table/styles.ts @@ -12,30 +12,18 @@ const { const { dark, light } = ThemeType -const paginationPalette = { - [dark]: { - disabledBgColor: blacks.base, - hoverColor: blacks['lighten-80'] - }, - [light]: { - disabledBgColor: grays.base, - hoverColor: blacks['darken-20'] - } -} - export const generatePaginationStyles = (themeType: ThemeType) => { const { base: { backgroundColor, borderColor, color }, - disabled + disabled, + hover } = themedStyles[themeType] - const { disabledBgColor, hoverColor } = paginationPalette[themeType] - return { '& .ant-pagination.ant-table-pagination > li': { '&.ant-pagination-disabled, &.ant-pagination-disabled:hover': { '& button.ant-pagination-item-link': { - backgroundColor: disabledBgColor, + backgroundColor: disabled.backgroundColor, borderColor, color: disabled.color } @@ -48,12 +36,12 @@ export const generatePaginationStyles = (themeType: ThemeType) => { color }, '&.ant-pagination-item.ant-pagination-item-active, &.ant-pagination-item:hover, &:hover': { - '& a': { color: hoverColor }, + '& a': { color: hover.color }, '& button.ant-pagination-item-link': { - borderColor: hoverColor, - color: hoverColor + borderColor: hover.borderColor, + color: hover.color }, - borderColor: hoverColor + borderColor: hover.borderColor }, borderRadius } diff --git a/src/components/Tag/Tag.stories.tsx b/src/components/Tag/Tag.stories.tsx index d7abdce1..e148c4d2 100644 --- a/src/components/Tag/Tag.stories.tsx +++ b/src/components/Tag/Tag.stories.tsx @@ -1,3 +1,4 @@ +import { action } from '@storybook/addon-actions' import React from 'react' import { Meta, Story } from '@storybook/react/types-6-0' import { Tag, TagProps } from '.' @@ -7,7 +8,8 @@ export default { children: { control: 'text' }, color: { control: 'color' - } + }, + onClose: { defaultValue: action('onClose') } }, component: Tag, title: 'Tag' @@ -20,26 +22,3 @@ Default.args = { children: 'Default' } export const Colored = Template.bind({}) Colored.args = { children: 'Colored', color: '#108ee9' } - -export const ColoredPreset = Template.bind({}) -ColoredPreset.args = { children: 'Blue', color: 'blue' } -ColoredPreset.argTypes = { - color: { - control: { - options: [ - 'magenta', - 'red', - 'volcano', - 'orange', - 'gold', - 'lime', - 'green', - 'cyan', - 'blue', - 'geekblue', - 'purple' - ], - type: 'select' - } - } -} diff --git a/src/components/Tag/index.tsx b/src/components/Tag/index.tsx index dd100700..5b3cd0c7 100644 --- a/src/components/Tag/index.tsx +++ b/src/components/Tag/index.tsx @@ -1,18 +1,64 @@ import 'antd/lib/tag/style/index.css' import { Tag as AntDTag } from 'antd' +import cn from 'classnames' +import { createUseStyles } from 'react-jss' +import { generateThemedTagStyles } from './utils' +import { ThemeType } from '../assets/styles/themes' import React, { FC, ReactNode } from 'react' +const { dark, light } = ThemeType + +const useStyles = createUseStyles({ + '@global': { + span: generateThemedTagStyles(light), + [`.${dark}`]: { + '& $span': generateThemedTagStyles(dark) + } + } +}) + export interface TagProps { /** - * Tag children to render including tag text. + * Tag children to render including tag text */ children: ReactNode /** - * Color of tag - either a preset (`red`, `blue`, `green` etc.), a hex color code(eg. `#ffffff`) or a rgb color value(eg. `rgb(255, 0, 0)`). + * Array of classes to pass to element + * @default [] + */ + classes?: string[] + /** + * Color of tag - rgb color value(eg. `rgb(255, 0, 0)`) */ color?: string + /** + * Whether the Tag can be deleted or not. A deletable Tag is rendered with a clickable 'X' at the end + * @default true + */ + deletable?: boolean + /** + * Optional callback that runs when Tag is closed if it is deletable + */ + onDelete?: Function } -export const Tag: FC = ({ children, color }: TagProps) => { - return {children} +export const Tag: FC = ({ + children, + classes = [], + deletable = true, + color, + onDelete +}: TagProps) => { + useStyles() + + return ( + + {children} + + ) } diff --git a/src/components/Tag/utils.ts b/src/components/Tag/utils.ts new file mode 100644 index 00000000..7099f51f --- /dev/null +++ b/src/components/Tag/utils.ts @@ -0,0 +1,45 @@ +import { styleguide } from 'components/assets/styles' +import { themedStyles, ThemeType } from '../assets/styles/themes' + +const { dark, light } = ThemeType + +const { + colors: { blacks, grays } +} = styleguide + +export const tagPalette = { + [dark]: { + background: blacks['lighten-10'], + borderColor: blacks['lighten-10'], + color: blacks['lighten-50'] + }, + [light]: { + background: grays.base, + borderColor: blacks['lighten-80'], + color: blacks.base + } +} + +export const generateThemedTagStyles = (themeType: ThemeType) => { + const { background, borderColor, color } = tagPalette[themeType] + + const { base, hover } = themedStyles[themeType] + + return { + '&.ant-tag': { + '& .ant-tag-close-icon': { + '&:hover': { + color: hover.color + }, + color: base.color + }, + '&:hover': { + opacity: 'unset' + }, + background, + borderColor, + color, + cursor: 'auto' + } + } +} diff --git a/src/components/Tooltip/index.tsx b/src/components/Tooltip/index.tsx index b217a8fd..f4456d0d 100644 --- a/src/components/Tooltip/index.tsx +++ b/src/components/Tooltip/index.tsx @@ -45,6 +45,7 @@ export interface TooltipProps extends CommonComponentProps { * Text shown in the tooltip */ title: TooltipTitle + tooltipTriggerClasses?: string[] } export const Tooltip: FC = ({ @@ -53,7 +54,8 @@ export const Tooltip: FC = ({ dataTag, placement = 'right', popupContainerSelector, - title + title, + tooltipTriggerClasses = [] }: TooltipProps) => { useStyles() @@ -73,7 +75,10 @@ export const Tooltip: FC = ({ title={title} {...popupContainerProps} > - + {children} diff --git a/src/components/assets/styles/themes.ts b/src/components/assets/styles/themes.ts index 2299416b..dac854e4 100644 --- a/src/components/assets/styles/themes.ts +++ b/src/components/assets/styles/themes.ts @@ -19,7 +19,7 @@ export interface Theme { secondary: string state: { active: string - disabled: string + disabled: { background: string; border: string } error: string hover: string inactive: string @@ -43,7 +43,7 @@ const lightPalette: Theme = { secondary: blacks['lighten-30'], state: { active: blacks.base, - disabled: grays.base, + disabled: { background: grays['lighten-40'], border: grays.base }, error: reds.base, hover: blacks.base, inactive: blacks['lighten-70'], @@ -55,7 +55,7 @@ const lightPalette: Theme = { warning: oranges.base }, text: { - disabled: blacks['lighten-70'], + disabled: blacks['lighten-80'], primary: blacks['lighten-30'] } } @@ -70,7 +70,10 @@ const darkPalette: Theme = { secondary: blacks['lighten-30'], state: { active: grays.base, - disabled: blacks['lighten-10'], + disabled: { + background: blacks['darken-20'], + border: blacks['darken-20'] + }, error: reds.base, hover: blacks['lighten-80'], inactive: blacks['lighten-20'], @@ -95,7 +98,8 @@ const generateThemedStyles = ({ state, background, border, text }: Theme) => { } const disabled = { - backgroundColor: state.disabled, + backgroundColor: state.disabled.background, + borderColor: state.disabled.border, color: text.disabled } diff --git a/src/components/index.ts b/src/components/index.ts index 26056f88..cd8ca457 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,6 +1,7 @@ export * from './Avatar' export * from './assets/styles' export * from './Button' +export * from './Checkbox' export * from './ColoredDot' export * from './Form' export * from './Input'