diff --git a/.eslintrc.js b/.eslintrc.js index 0312732643520a..1e0ef899ce3a92 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -248,5 +248,12 @@ module.exports = { 'jsdoc/valid-types': 'off', }, }, + { + files: [ '**/@(storybook|stories)/*' ], + rules: { + // Useful to add story descriptions via JSdoc without specifying params. + 'jsdoc/require-param': 'off', + }, + }, ], }; diff --git a/package-lock.json b/package-lock.json index 37ed00efb43bc8..c57b06d2706111 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8255,6 +8255,333 @@ } } }, + "@storybook/addon-actions": { + "version": "6.4.19", + "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.4.19.tgz", + "integrity": "sha512-GpSvP8xV8GfNkmtGJjfCgaOx6mbjtyTK0aT9FqX9pU0s+KVMmoCTrBh43b7dWrwxxas01yleBK9VpYggzhi/Fw==", + "dev": true, + "requires": { + "@storybook/addons": "6.4.19", + "@storybook/api": "6.4.19", + "@storybook/components": "6.4.19", + "@storybook/core-events": "6.4.19", + "@storybook/csf": "0.0.2--canary.87bc651.0", + "@storybook/theming": "6.4.19", + "core-js": "^3.8.2", + "fast-deep-equal": "^3.1.3", + "global": "^4.4.0", + "lodash": "^4.17.21", + "polished": "^4.0.5", + "prop-types": "^15.7.2", + "react-inspector": "^5.1.0", + "regenerator-runtime": "^0.13.7", + "telejson": "^5.3.2", + "ts-dedent": "^2.0.0", + "util-deprecate": "^1.0.2", + "uuid-browser": "^3.1.0" + }, + "dependencies": { + "@emotion/serialize": { + "version": "0.11.16", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz", + "integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==", + "dev": true, + "requires": { + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/unitless": "0.7.5", + "@emotion/utils": "0.11.3", + "csstype": "^2.5.7" + } + }, + "@emotion/styled": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-10.3.0.tgz", + "integrity": "sha512-GgcUpXBBEU5ido+/p/mCT2/Xx+Oqmp9JzQRuC+a4lYM4i4LBBn/dWvc0rQ19N9ObA8/T4NWMrPNe79kMBDJqoQ==", + "dev": true, + "requires": { + "@emotion/styled-base": "^10.3.0", + "babel-plugin-emotion": "^10.0.27" + } + }, + "@emotion/styled-base": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@emotion/styled-base/-/styled-base-10.3.0.tgz", + "integrity": "sha512-PBRqsVKR7QRNkmfH78hTSSwHWcwDpecH9W6heujWAcyp2wdz/64PP73s7fWS1dIPm8/Exc8JAzYS8dEWXjv60w==", + "dev": true, + "requires": { + "@babel/runtime": "^7.5.5", + "@emotion/is-prop-valid": "0.8.8", + "@emotion/serialize": "^0.11.15", + "@emotion/utils": "0.11.3" + } + }, + "@emotion/utils": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz", + "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==", + "dev": true + }, + "@storybook/addons": { + "version": "6.4.19", + "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-6.4.19.tgz", + "integrity": "sha512-QNyRYhpqmHV8oJxxTBdkRlLSbDFhpBvfvMfIrIT1UXb/eemdBZTaCGVvXZ9UixoEEI7f8VwAQ44IvkU5B1509w==", + "dev": true, + "requires": { + "@storybook/api": "6.4.19", + "@storybook/channels": "6.4.19", + "@storybook/client-logger": "6.4.19", + "@storybook/core-events": "6.4.19", + "@storybook/csf": "0.0.2--canary.87bc651.0", + "@storybook/router": "6.4.19", + "@storybook/theming": "6.4.19", + "@types/webpack-env": "^1.16.0", + "core-js": "^3.8.2", + "global": "^4.4.0", + "regenerator-runtime": "^0.13.7" + } + }, + "@storybook/api": { + "version": "6.4.19", + "resolved": "https://registry.npmjs.org/@storybook/api/-/api-6.4.19.tgz", + "integrity": "sha512-aDvea+NpQCBjpNp9YidO1Pr7fzzCp15FSdkG+2ihGQfv5raxrN+IIJnGUXecpe71nvlYiB+29UXBVK7AL0j51Q==", + "dev": true, + "requires": { + "@storybook/channels": "6.4.19", + "@storybook/client-logger": "6.4.19", + "@storybook/core-events": "6.4.19", + "@storybook/csf": "0.0.2--canary.87bc651.0", + "@storybook/router": "6.4.19", + "@storybook/semver": "^7.3.2", + "@storybook/theming": "6.4.19", + "core-js": "^3.8.2", + "fast-deep-equal": "^3.1.3", + "global": "^4.4.0", + "lodash": "^4.17.21", + "memoizerific": "^1.11.3", + "regenerator-runtime": "^0.13.7", + "store2": "^2.12.0", + "telejson": "^5.3.2", + "ts-dedent": "^2.0.0", + "util-deprecate": "^1.0.2" + } + }, + "@storybook/channels": { + "version": "6.4.19", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-6.4.19.tgz", + "integrity": "sha512-EwyoncFvTfmIlfsy8jTfayCxo2XchPkZk/9txipugWSmc057HdklMKPLOHWP0z5hLH0IbVIKXzdNISABm36jwQ==", + "dev": true, + "requires": { + "core-js": "^3.8.2", + "ts-dedent": "^2.0.0", + "util-deprecate": "^1.0.2" + } + }, + "@storybook/client-logger": { + "version": "6.4.19", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.4.19.tgz", + "integrity": "sha512-zmg/2wyc9W3uZrvxaW4BfHcr40J0v7AGslqYXk9H+ERLVwIvrR4NhxQFaS6uITjBENyRDxwzfU3Va634WcmdDQ==", + "dev": true, + "requires": { + "core-js": "^3.8.2", + "global": "^4.4.0" + } + }, + "@storybook/components": { + "version": "6.4.19", + "resolved": "https://registry.npmjs.org/@storybook/components/-/components-6.4.19.tgz", + "integrity": "sha512-q/0V37YAJA7CNc+wSiiefeM9+3XVk8ixBNylY36QCGJgIeGQ5/79vPyUe6K4lLmsQwpmZsIq1s1Ad5+VbboeOA==", + "dev": true, + "requires": { + "@popperjs/core": "^2.6.0", + "@storybook/client-logger": "6.4.19", + "@storybook/csf": "0.0.2--canary.87bc651.0", + "@storybook/theming": "6.4.19", + "@types/color-convert": "^2.0.0", + "@types/overlayscrollbars": "^1.12.0", + "@types/react-syntax-highlighter": "11.0.5", + "color-convert": "^2.0.1", + "core-js": "^3.8.2", + "fast-deep-equal": "^3.1.3", + "global": "^4.4.0", + "lodash": "^4.17.21", + "markdown-to-jsx": "^7.1.3", + "memoizerific": "^1.11.3", + "overlayscrollbars": "^1.13.1", + "polished": "^4.0.5", + "prop-types": "^15.7.2", + "react-colorful": "^5.1.2", + "react-popper-tooltip": "^3.1.1", + "react-syntax-highlighter": "^13.5.3", + "react-textarea-autosize": "^8.3.0", + "regenerator-runtime": "^0.13.7", + "ts-dedent": "^2.0.0", + "util-deprecate": "^1.0.2" + } + }, + "@storybook/core-events": { + "version": "6.4.19", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.4.19.tgz", + "integrity": "sha512-KICzUw6XVQUJzFSCXfvhfHAuyhn4Q5J4IZEfuZkcGJS4ODkrO6tmpdYE5Cfr+so95Nfp0ErWiLUuodBsW9/rtA==", + "dev": true, + "requires": { + "core-js": "^3.8.2" + } + }, + "@storybook/csf": { + "version": "0.0.2--canary.87bc651.0", + "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.0.2--canary.87bc651.0.tgz", + "integrity": "sha512-ajk1Uxa+rBpFQHKrCcTmJyQBXZ5slfwHVEaKlkuFaW77it8RgbPJp/ccna3sgoi8oZ7FkkOyvv1Ve4SmwFqRqw==", + "dev": true, + "requires": { + "lodash": "^4.17.15" + } + }, + "@storybook/router": { + "version": "6.4.19", + "resolved": "https://registry.npmjs.org/@storybook/router/-/router-6.4.19.tgz", + "integrity": "sha512-KWWwIzuyeEIWVezkCihwY2A76Il9tUNg0I410g9qT7NrEsKyqXGRYOijWub7c1GGyNjLqz0jtrrehtixMcJkuA==", + "dev": true, + "requires": { + "@storybook/client-logger": "6.4.19", + "core-js": "^3.8.2", + "fast-deep-equal": "^3.1.3", + "global": "^4.4.0", + "history": "5.0.0", + "lodash": "^4.17.21", + "memoizerific": "^1.11.3", + "qs": "^6.10.0", + "react-router": "^6.0.0", + "react-router-dom": "^6.0.0", + "ts-dedent": "^2.0.0" + } + }, + "@storybook/semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@storybook/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-SWeszlsiPsMI0Ps0jVNtH64cI5c0UF3f7KgjVKJoNP30crQ6wUSddY2hsdeczZXEKVJGEn50Q60flcGsQGIcrg==", + "dev": true, + "requires": { + "core-js": "^3.6.5", + "find-up": "^4.1.0" + } + }, + "@storybook/theming": { + "version": "6.4.19", + "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-6.4.19.tgz", + "integrity": "sha512-V4pWmTvAxmbHR6B3jA4hPkaxZPyExHvCToy7b76DpUTpuHihijNDMAn85KhOQYIeL9q14zP/aiz899tOHsOidg==", + "dev": true, + "requires": { + "@emotion/core": "^10.1.1", + "@emotion/is-prop-valid": "^0.8.6", + "@emotion/styled": "^10.0.27", + "@storybook/client-logger": "6.4.19", + "core-js": "^3.8.2", + "deep-object-diff": "^1.1.0", + "emotion-theming": "^10.0.27", + "global": "^4.4.0", + "memoizerific": "^1.11.3", + "polished": "^4.0.5", + "resolve-from": "^5.0.0", + "ts-dedent": "^2.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "csstype": { + "version": "2.6.20", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz", + "integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "history": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.0.0.tgz", + "integrity": "sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.7.6" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, "@storybook/addon-controls": { "version": "6.4.9", "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-6.4.9.tgz", @@ -22926,6 +23253,25 @@ "find-up": "^4.0.0" } }, + "plist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.2.tgz", + "integrity": "sha512-MSrkwZBdQ6YapHy87/8hDU8MnIcyxBKjeF+McXnr5A9MtffPewTs7G3hlpodT5TacyfIyFTaJEhh3GGcmasTgQ==", + "dev": true, + "requires": { + "base64-js": "^1.5.1", + "xmlbuilder": "^9.0.7", + "xmldom": "^0.5.0" + }, + "dependencies": { + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", + "dev": true + } + } + }, "pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -37303,6 +37649,16 @@ "integrity": "sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==", "dev": true }, + "is-dom": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-dom/-/is-dom-1.1.0.tgz", + "integrity": "sha512-u82f6mvhYxRPKpw8V1N0W8ce1xXwOrQtgGcxl6UCL5zBmZu3is/18K0rR7uFCnMDuAsS/3W54mGL4vsaFUQlEQ==", + "dev": true, + "requires": { + "is-object": "^1.0.1", + "is-window": "^1.0.2" + } + }, "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -37416,6 +37772,12 @@ "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, + "is-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", + "dev": true + }, "is-observable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", @@ -37592,6 +37954,12 @@ "integrity": "sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==", "dev": true }, + "is-window": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-window/-/is-window-1.0.2.tgz", + "integrity": "sha1-LIlspT25feRdPDMTOmXYyfVjSA0=", + "dev": true + }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -49517,6 +49885,17 @@ "prop-types": "^15.5.8" } }, + "react-inspector": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-5.1.1.tgz", + "integrity": "sha512-GURDaYzoLbW8pMGXwYPDBIv6nqei4kK7LPRZ9q9HCZF54wqXz/dnylBp/kfE9XmekBhHvLDdcYeyIwSrvtOiWg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.0.0", + "is-dom": "^1.0.0", + "prop-types": "^15.0.0" + } + }, "react-is": { "version": "16.8.4", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.4.tgz", @@ -56725,6 +57104,12 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==" }, + "uuid-browser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid-browser/-/uuid-browser-3.1.0.tgz", + "integrity": "sha1-DwWkCu90+eWVHiDvv0SxGHHlZBA=", + "dev": true + }, "v8-compile-cache": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", diff --git a/package.json b/package.json index f4b537366996a2..61ca7d77786f84 100755 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "@octokit/rest": "16.26.0", "@pmmmwh/react-refresh-webpack-plugin": "0.5.2", "@storybook/addon-a11y": "6.4.9", + "@storybook/addon-actions": "6.4.19", "@storybook/addon-controls": "6.4.9", "@storybook/addon-docs": "6.4.9", "@storybook/addon-knobs": "6.2.9", diff --git a/packages/components/src/unit-control/README.md b/packages/components/src/unit-control/README.md index f9aab741f18a97..aae09f063d3c19 100644 --- a/packages/components/src/unit-control/README.md +++ b/packages/components/src/unit-control/README.md @@ -4,7 +4,7 @@ This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes. -UnitControl allows the user to set a value as well as a unit (e.g. `px`). +`UnitControl` allows the user to set a numeric quantity as well as a unit (e.g. `px`). ## Usage @@ -66,14 +66,12 @@ The position of the label (`top`, `side`, `bottom`, or `edge`). Callback when the `value` changes. - Required: No -- Default: `noop` ### `onUnitChange`: `UnitControlOnChangeCallback` Callback when the `unit` changes. - Required: No -- Default: `noop` ### `size`: `string` diff --git a/packages/components/src/unit-control/index.tsx b/packages/components/src/unit-control/index.tsx index e9cafb69c6846a..52c450b6845132 100644 --- a/packages/components/src/unit-control/index.tsx +++ b/packages/components/src/unit-control/index.tsx @@ -8,7 +8,7 @@ import type { SyntheticEvent, ChangeEvent, } from 'react'; -import { noop, omit } from 'lodash'; +import { omit } from 'lodash'; import classnames from 'classnames'; /** @@ -35,7 +35,7 @@ import { useControlledState } from '../utils/hooks'; import type { UnitControlProps, UnitControlOnChangeCallback } from './types'; import type { StateReducer } from '../input-control/reducer/state'; -function UnitControl( +function UnforwardedUnitControl( { __unstableStateReducer: stateReducerProp, autoComplete = 'off', @@ -46,8 +46,8 @@ function UnitControl( isResetValueOnUnitChange = false, isUnitSelectTabbable = true, label, - onChange = noop, - onUnitChange = noop, + onChange, + onUnitChange, size = 'default', style, unit: unitProp, @@ -97,7 +97,7 @@ function UnitControl( typeof nextQuantityValue === 'undefined' || nextQuantityValue === null ) { - onChange( '', changeProps ); + onChange?.( '', changeProps ); return; } @@ -112,7 +112,7 @@ function UnitControl( unit ).join( '' ); - onChange( onChangeValue, changeProps ); + onChange?.( onChangeValue, changeProps ); }; const handleOnUnitChange: UnitControlOnChangeCallback = ( @@ -127,8 +127,8 @@ function UnitControl( nextValue = `${ data.default }${ nextUnitValue }`; } - onChange( nextValue, changeProps ); - onUnitChange( nextUnitValue, changeProps ); + onChange?.( nextValue, changeProps ); + onUnitChange?.( nextUnitValue, changeProps ); setUnit( nextUnitValue ); }; @@ -156,11 +156,11 @@ function UnitControl( : undefined; const changeProps = { event, data }; - onChange( + onChange?.( `${ validParsedQuantity ?? '' }${ validParsedUnit }`, changeProps ); - onUnitChange( validParsedUnit, changeProps ); + onUnitChange?.( validParsedUnit, changeProps ); setUnit( validParsedUnit ); } @@ -262,7 +262,7 @@ function UnitControl( } /** - * `UnitControl` allows the user to set a value as well as a unit (e.g. `px`). + * `UnitControl` allows the user to set a numeric quantity as well as a unit (e.g. `px`). * * * @example @@ -277,7 +277,7 @@ function UnitControl( * }; * ``` */ -const ForwardedUnitControl = forwardRef( UnitControl ); +export const UnitControl = forwardRef( UnforwardedUnitControl ); export { parseQuantityAndUnitFromRawValue, useCustomUnits } from './utils'; -export default ForwardedUnitControl; +export default UnitControl; diff --git a/packages/components/src/unit-control/stories/index.js b/packages/components/src/unit-control/stories/index.js deleted file mode 100644 index 9603a05e3941a1..00000000000000 --- a/packages/components/src/unit-control/stories/index.js +++ /dev/null @@ -1,127 +0,0 @@ -/** - * External dependencies - */ -import { boolean, number, select, text, object } from '@storybook/addon-knobs'; -import styled from '@emotion/styled'; - -/** - * WordPress dependencies - */ -import { useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import UnitControl from '../'; -import { CSS_UNITS } from '../utils'; - -export default { - title: 'Components (Experimental)/UnitControl', - component: UnitControl, - parameters: { - knobs: { disable: false }, - }, -}; - -const ControlWrapperView = styled.div` - max-width: 80px; -`; - -function Example() { - const [ value, setValue ] = useState( '10px' ); - - const props = { - disableUnits: boolean( 'disableUnits', false ), - hideLabelFromVision: boolean( 'hideLabelFromVision', false ), - isPressEnterToChange: boolean( 'isPressEnterToChange', true ), - isShiftStepEnabled: boolean( 'isShiftStepEnabled', true ), - isUnitSelectTabbable: boolean( 'isUnitSelectTabbable', true ), - label: text( 'label', 'Value' ), - min: number( 'min', 0 ), - max: number( 'max', 100 ), - shiftStep: number( 'shiftStep', 10 ), - size: select( - 'size', - { - default: 'default', - small: 'small', - '__unstable-large': '__unstable-large', - }, - 'default' - ), - step: number( 'step', 1 ), - units: object( 'units', CSS_UNITS ), - }; - - return ( - - setValue( v ) } - /> - - ); -} - -export const _default = () => { - return ; -}; - -export const WithSingleUnit = ( props ) => { - const [ value, setValue ] = useState( '10px' ); - return ( - - setValue( v ) } - /> - - ); -}; -WithSingleUnit.args = { - label: 'Value', - units: CSS_UNITS.slice( 0, 1 ), -}; - -export function WithCustomUnits() { - const [ value, setValue ] = useState( '10km' ); - - const props = { - isResetValueOnUnitChange: boolean( 'isResetValueOnUnitChange', true ), - label: text( 'label', 'Distance' ), - units: object( 'units', [ - { - value: 'km', - label: 'km', - default: 1, - }, - { - value: 'mi', - label: 'mi', - default: 1, - }, - { - value: 'm', - label: 'm', - default: 1000, - }, - { - value: 'yd', - label: 'yd', - default: 1760, - }, - ] ), - }; - - return ( - - setValue( v ) } - /> - - ); -} diff --git a/packages/components/src/unit-control/stories/index.tsx b/packages/components/src/unit-control/stories/index.tsx new file mode 100644 index 00000000000000..87dfd3a84c602d --- /dev/null +++ b/packages/components/src/unit-control/stories/index.tsx @@ -0,0 +1,170 @@ +/** + * External dependencies + */ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; + +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { UnitControl } from '../'; +import { CSS_UNITS } from '../utils'; + +const meta: ComponentMeta< typeof UnitControl > = { + component: UnitControl, + title: 'Components (Experimental)/UnitControl', + argTypes: { + __unstableInputWidth: { + control: { type: 'text' }, + }, + __unstableStateReducer: { + control: { type: null }, + }, + size: { + control: { type: 'select' }, + }, + onChange: { + action: 'onChange', + control: { type: null }, + }, + onUnitChange: { + control: { type: null }, + }, + value: { + control: { type: null }, + }, + }, + parameters: { + controls: { + expanded: true, + }, + docs: { source: { state: 'open' } }, + }, +}; +export default meta; + +const DefaultTemplate: ComponentStory< typeof UnitControl > = ( { + onChange, + ...args +} ) => { + const [ value, setValue ] = useState< string | undefined >( '10px' ); + + return ( +
+ { + setValue( v ); + onChange?.( v, extra ); + } } + /> +
+ ); +}; + +export const Default: ComponentStory< + typeof UnitControl +> = DefaultTemplate.bind( {} ); +Default.args = { + label: 'Label', +}; + +/** + * If the `isPressEnterToChange` prop is set to `true`, the `onChange` callback + * will not fire while a new value is typed in the input field (you can verify this + * behavior by inspecting the console's output). + */ +export const PressEnterToChange: ComponentStory< + typeof UnitControl +> = DefaultTemplate.bind( {} ); +PressEnterToChange.args = { + ...Default.args, + isPressEnterToChange: true, + onChange: ( v ) => { + // eslint-disable-next-line no-console + console.log( v ); + }, +}; + +/** + * Most of `NumberControl`'s props can be passed to `UnitControl`, and they will + * affect its numeric input field. + */ +export const TweakingTheNumberInput: ComponentStory< + typeof UnitControl +> = DefaultTemplate.bind( {} ); +TweakingTheNumberInput.args = { + ...Default.args, + min: 0, + max: 100, + step: 'any', + label: 'Custom label', +}; + +/** + * When only one unit is available, the unit selection dropdown becomes static text. + */ +export const WithSingleUnit: ComponentStory< + typeof UnitControl +> = DefaultTemplate.bind( {} ); +WithSingleUnit.args = { + ...Default.args, + units: CSS_UNITS.slice( 0, 1 ), +}; + +/** + * It is possible to pass a custom list of units. Every time the unit changes, + * if the `isResetValueOnUnitChange` is set to `true`, the input's quantity is + * reset to the new unit's default value. + */ +export const WithCustomUnits: ComponentStory< typeof UnitControl > = ( { + onChange, + ...args +} ) => { + const [ value, setValue ] = useState< string | undefined >( '80km' ); + + return ( +
+ { + setValue( v ); + onChange?.( v, extra ); + } } + /> +
+ ); +}; +WithCustomUnits.args = { + ...Default.args, + isResetValueOnUnitChange: true, + min: 0, + units: [ + { + value: 'km', + label: 'km', + default: 1, + }, + { + value: 'mi', + label: 'mi', + default: 1, + }, + { + value: 'm', + label: 'm', + default: 1000, + }, + { + value: 'yd', + label: 'yd', + default: 1760, + }, + ], +}; diff --git a/packages/components/src/unit-control/types.ts b/packages/components/src/unit-control/types.ts index 83a381dc2b4cb2..46d8707b9639ff 100644 --- a/packages/components/src/unit-control/types.ts +++ b/packages/components/src/unit-control/types.ts @@ -9,6 +9,7 @@ import type { CSSProperties, SyntheticEvent } from 'react'; import type { StateReducer } from '../input-control/reducer/state'; import type { InputChangeCallback, + InputControlProps, Size as InputSize, } from '../input-control/types'; @@ -51,8 +52,6 @@ export type UnitSelectControlProps = { isUnitSelectTabbable?: boolean; /** * A callback function invoked when the value is changed. - * - * @default noop */ onChange?: UnitControlOnChangeCallback; /** @@ -74,42 +73,56 @@ export type UnitSelectControlProps = { }; // TODO: when available, should (partially) extend `NumberControl` props. -export type UnitControlProps = UnitSelectControlProps & { - __unstableStateReducer?: StateReducer; - __unstableInputWidth?: CSSProperties[ 'width' ]; - /** - * If `true`, the unit `` is hidden. + * + * @default false + */ + disableUnits?: boolean; + /** + * If `true`, the `ENTER` key press is required in order to trigger an `onChange`. + * If enabled, a change is also triggered when tabbing away (`onBlur`). + * + * @default false + */ + isPressEnterToChange?: boolean; + /** + * If `true`, and the selected unit provides a `default` value, this value is set + * when changing units. + * + * @default false + */ + isResetValueOnUnitChange?: boolean; + /** + * If this property is added, a label will be generated using label property as the content. + */ + label?: string; + /** + * Callback when the `unit` changes. + */ + onUnitChange?: UnitControlOnChangeCallback; + /** + * Current value. If passed as a string, the current unit will be inferred from this value. + * For example, a `value` of "50%" will set the current unit to `%`. + */ + value?: string | number; + /** + * If true, pressing `UP` or `DOWN` along with the `SHIFT` key will increment + * the value by the `shiftStep` value. + * + * @default true + */ + isShiftStepEnabled?: boolean; + /** + * Amount to increment by when the `SHIFT` key is held down. This shift value + * is a multiplier to the `step` value. For example, if the `step` value is `5`, + * and `shiftStep` is `10`, each jump would increment/decrement by `50`. + * + * @default 10 + */ + shiftStep?: number; + }; diff --git a/packages/components/src/unit-control/unit-select-control.tsx b/packages/components/src/unit-control/unit-select-control.tsx index 775c032db8f632..844415fd0e4263 100644 --- a/packages/components/src/unit-control/unit-select-control.tsx +++ b/packages/components/src/unit-control/unit-select-control.tsx @@ -1,7 +1,6 @@ /** * External dependencies */ -import { noop } from 'lodash'; import classnames from 'classnames'; import type { ChangeEvent } from 'react'; @@ -16,7 +15,7 @@ import type { UnitSelectControlProps } from './types'; export default function UnitSelectControl( { className, isUnitSelectTabbable: isTabbable = true, - onChange = noop, + onChange, size = 'default', unit = 'px', units = CSS_UNITS, @@ -37,7 +36,7 @@ export default function UnitSelectControl( { const { value: unitValue } = event.target; const data = units.find( ( option ) => option.value === unitValue ); - onChange( unitValue, { event, data } ); + onChange?.( unitValue, { event, data } ); }; const classes = classnames( 'components-unit-control__select', className ); diff --git a/storybook/main.js b/storybook/main.js index d8c0efc5a5273a..99c8c2ea84bec1 100644 --- a/storybook/main.js +++ b/storybook/main.js @@ -23,6 +23,7 @@ module.exports = { '@storybook/addon-viewport', '@storybook/addon-a11y', '@storybook/addon-toolbars', + '@storybook/addon-actions', ], features: { babelModeV7: true,